Skip to content

fix(mobile): rename sessions from the list and stop inventing titles - #3919

Open
braindeadz wants to merge 1 commit into
openBitFun:mainfrom
braindeadz:fix/mobile-session-rename
Open

braindeadz wants to merge 1 commit into
openBitFun:mainfrom
braindeadz:fix/mobile-session-rename

Conversation

@braindeadz

@braindeadz braindeadz commented Nov 14, 2026 •

Copy link
Copy Markdown

Summary

Let Android users rename a remote session from the session list, the action
sheet/popup and the sidebar rail, and stop the shared mobile layer from
inventing placeholder titles for sessions the desktop has not named.

Root cause, verified in code on the base commit: the rename path already
existed end to end — the conversation header dispatched
RemoteSessionIntent.RenameSession, RemoteSessionStore.renameSession handled
it, sending update_session_title — but SessionActionPolicy
never exposed a rename capability, and the action sheet, the session list and
the sidebar never drew a row for it. Separately, three different English
placeholders made an unnamed session look named and made a successful rename
look like it had not been applied:

  • shared SessionNaming.fallbackTitle returned "New Session";
  • shared RemoteResponseMapper fabricated "Session <id-prefix>" from the id;
  • the Android conversation header fell back to "Remote session".

A host that has not named a session sends no title at all. The shared layer now
keeps that title blank and each surface renders its own localized "untitled"
wording (sidebar_untitled on Android).

Fixes #

This PR is related to #3809 but does not close it. #3809 is about the legacy Android APK persisting "Remote Code Session" as a user-edited title on 1.0.7+ hosts. This change stops this client from inventing titles and adds the rename action, but it cannot clear a title the host already holds, and it does not add a host-side compatibility path or ship a new APK. That work remains open in #3809.

Type and Areas

Type:

  • Bug fix (with a small feature: a previously unreachable action is now
    surfaced).

Areas:

  • Mobile client (Kotlin Multiplatform shared core + Android Compose app).
  • No Rust core, desktop, web UI, server or relay changes.

Motivation / Impact

What was broken for a remote-session user. Opening the overflow menu on a
session offered View details, Archive/Export (general chat only) and
Delete — never Rename. To rename, the user had to open the conversation and
use the header's title editor. Worse, a session the desktop had not named yet
showed a fabricated English title ("New Session", "Session <id-prefix>",
"Remote session" depending on the surface), so an untitled session looked
titled, and after a rename the header and the list could disagree until a
refresh.

What changes.

  • The action sheet, the anchored popup and the sidebar rail now offer a
    Rename row that opens an inline title field seeded with the current title.
  • A blank host title is rendered as the localized sidebar_untitled label on
    every Android surface (list row, rail, header, sheet, home recent row) instead
    of an invented English name.
  • The shared layer no longer fabricates a title; it keeps the host's word or an
    empty string.

Design.

  • SessionActionCapabilities gains an appended canRename flag rather than
    folding rename into canArchive/canExport. For a remote session rename is a
    round trip to the desktop; for a general chat it rewrites the stored row. It
    is neither of the local-storage operations those flags describe, so a separate
    flag reads correctly in both scopes. Appending it keeps the four existing
    positional arguments meaning what they meant.
  • The sheet/popup/sidebar take an optional onRename callback. A shell that
    cannot dispatch a rename renders no row rather than a row that does nothing —
    the same pattern archive and export already use.
  • The host keeps authority. The surface only names the new title; the store
    keeps the old title in the list until the desktop answers
    update_session_title (it updates the local row only after the command is
    sent, and re-reads busy so a command that started while the field was open
    cannot submit).
  • The editor trims, refuses an empty or unchanged draft, and caps the draft at
    120 characters client-side (the host normalizes and refuses an empty title;
    the cap is the client's own guard against pasting a document into a one-line
    row). A session whose existing host title already exceeds 120 characters
    therefore cannot be saved without shortening it, and Save is disabled with no
    explanation of its own.
  • A blank title is rendered per surface via sidebar_untitled, not replaced by
    a fabricated English string.

Verification

All commands were run on Windows with the project's local Android build
environment (node 22.23.5, JDK 17, Android SDK). Test counts are read from the
Gradle JUnit XML reports.

Baseline (base commit, no changes applied)

  • From src/apps/mobile/shared:
    .\gradlew.bat --console=plain :core-domain:jvmTest :core-feature:jvmTest
    → 655 tests, 0 failures, 0 errors, 0 skipped (BUILD SUCCESSFUL).
  • From src/apps/mobile/android:
    .\gradlew.bat --console=plain :app:testDebugUnitTest
    → 65 tests, 1 failure, 0 errors, 0 skipped. The single failure is
    pre-existing on the official base commit:
    com.openbitfun.mobile.app.ui.chat.ProcessPresentationTest.completedWorkSummarizesWithoutHidingLiveOrFailedTools
    with java.lang.AssertionError: expected:<3> but was:<1>
    (ProcessPresentationTest.kt:44).

After the change

  • Shared, same command → 657 tests, 0 failures, 0 errors, 0 skipped
    (BUILD SUCCESSFUL). The two extra tests are the new/renamed ones below.
  • Android unit, same command → 65 tests, 1 failure, 0 errors, 0 skipped.
    The failure is the same ProcessPresentationTest.completedWorkSummarizesWithoutHidingLiveOrFailedTools
    test with the same java.lang.AssertionError: expected:<3> but was:<1>
    message — an unchanged baseline failure, not a regression. This suite is not
    green on the base commit and this PR does not make it green.
  • Instrumented tests: the 10 tests in SessionRenameActionTest were executed on a
    real Android device against the original revision of this branch and passed
    10/10 (author-reported; not run by the AI). The amended revision adds
    aSurfaceWithoutARenameCallbackDrawsNoRenameRow and drops the unused
    conversation_title_default string resource; that amended revision is
    compile-verified only, from src/apps/mobile/android,
    .\gradlew.bat --console=plain :app:compileDebugAndroidTestKotlin
    → BUILD SUCCESSFUL. No instrumented test was re-run here, and this does not
    claim the added test passed. AppSidebar's onRenameRemoteSession id+title
    adaptation is covered by compilation only (no widget test renders the sidebar
    rail).

Node guards (from the repo root)

  • node scripts/check-mobile-architecture.mjs → PASS:
    "Mobile shared core architecture contracts are satisfied."
  • node --test scripts/mobile-client-build.contract.test.mjs → PASS:
    2 tests, 0 failures.
  • node scripts/mobile-ui-design-system.mjs --check → PASS:
    "Contract and generated files are in sync."
  • node scripts/check-git-object-sizes.mjs --base upstream/main --head HEAD
    → PASS: "Checked 16 Git blobs; no unapproved objects exceed 5242880
    bytes."
  • node scripts/mobile-ui-preview.mjs --check → SKIPPED:
    "Missing canonical design-system asset for /design-system-tokens.css." The
    script needs the generated design-system/packages/design-tokens/dist/tokens.css
    (produced by a design-system build); this checkout has no node_modules and
    pnpm is unavailable, so the asset is absent. Nothing was installed.
  • node scripts/i18n-audit.mjs → SKIPPED:
    "Failed to load TypeScript for i18n audit: Cannot find module 'typescript'
    … Run pnpm install before running pnpm run i18n:audit." Missing dependency;
    nothing was installed.
  • node scripts/audit-frontend-colors.mjs --kind native-mobile → SKIPPED:
    "exclusions[2].path does not exist: src/web-ui/public/monaco-editor". The
    script needs a vendored/generated web-ui asset that a full install would
    provide; absent here.

None of the three skipped scripts read the files changed by this PR (verified by
grep), and all three fail for missing build output or missing packages, not for
anything in this diff.

Verification evidence (for the reviewer)

New/updated shared unit tests:

  • SessionListPolicyTest.fallbackTitleStaysBlankForEveryMode (replaces
    fallbackTitleIsIndependentOfMode).
  • SessionActionPolicyTest.aRemoteSessionOffersRenameButNeverTheLocalStorageActions
    (renamed from aRemoteSessionNeverOffersTheLocalStorageActions).
  • SessionActionPolicyTest.theRenameFlagWasAppendedSoEarlierPositionsKeepTheirMeaning
    (new — pins that the appended flag does not shift the earlier positions).
  • SessionActionPolicyTest.archiveAndExportBelongToGeneralChatAlone (updated to
    the new capability shape).
  • SessionActionPolicyTest.aCommandInFlightWithdrawsEverything (updated).
  • RemoteResponseMapperTest.aHostTitleThatIsMissingStaysBlankAndATitledSessionKeepsItsName
    (new — blank/missing title stays blank, a real title is kept).

New instrumented tests (SessionRenameActionTest, test tags
session-rename-row and session-rename-field):

  • theRemoteMenuOffersRenameAndSeedsTheFieldWithTheCurrentTitle
  • submittingSendsOneRenameForThisSessionAndDismisses
  • theSubmittedTitleIsTheTrimmedDraft
  • aBlankHostTitleIsShownAsUntitledAndCanBeGivenAName
  • cancellingReturnsToTheActionListAndSendsNothing
  • anUnchangedOrBlankTitleCannotBeSubmitted
  • aCommandStartingWhileTheEditorIsOpenCannotBeSubmitted
  • aCommandInFlightOffersNoRename
  • theWideSidebarPopupOffersTheSameStep
  • theHomeRecentRowShowsTheUntitledLabelForABlankHostTitle
  • aSurfaceWithoutARenameCallbackDrawsNoRenameRow

Updated instrumented test: ConversationHeaderTest now asserts a blank title
renders sidebar_untitled.

The now-unreferenced conversation_title_default string was deleted from both
src/apps/mobile/android/app/src/main/res/values/strings.xml and
src/apps/mobile/android/app/src/main/res/values-zh/strings.xml; no production
or test code reads it any more.

Reviewer Notes

This PR closes no issue. (Fixes # is deliberately left empty; see the Summary note about #3809.)

Compatibility (this touches shared code used by other clients)

  • SessionActionCapabilities is a Kotlin data class consumed only by Kotlin
    code (the Android app and shared tests). The HarmonyOS client has its own
    parallel SessionActionCapabilities class in
    pages/policy/SessionActionPolicy.ets and does not consume the Kotlin one, so
    the appended field does not touch it. The iOS client has no caller of the
    Kotlin SessionActionCapabilities, so the appended field is not a compile
    break for it.
  • SessionNaming.fallbackTitle now returns "". Its only Kotlin caller is the
    shared RemoteSessionStore (when a new session has no title). HarmonyOS uses
    its own ArkTS RemoteResponseMapper and is unaffected. On iOS, several
    surfaces already render an empty title as a localized "untitled" label
    (SessionActionComponents, MobileAppModel+RemoteSession,
    ConversationHeader), but a few raw Text(session.title) sites (e.g. the
    home recent row and some sidebar rows) will now show a blank line where a
    fabricated "Session <id>" used to appear. That is a rendering difference on
    iOS, not a crash or compile break; a follow-up may want to route those raw
    sites through the same localized "untitled" label.
  • RemoteSessionUiState.Ready(...) is constructed only inside the shared store
    and shared tests; iOS/HarmonyOS reach it through their bridges, not by
    constructing it directly, so no break.
  • RemoteSessionIntent.RenameSession already existed on the base commit
    (handled in RemoteSessionStore.renameSession, dispatched by the Android
    conversation header, sending update_session_title). The claim of this PR is
    therefore "the rename path existed but no list/sheet/sidebar UI offered it",
    not "rename was implemented".

What a remote-session user sees

  • Before: an untitled session showed a fabricated English name; rename was only
    reachable from the conversation header.
  • After: an untitled session reads as the localized "Untitled session" on every
    Android surface; Rename is available from the list, the sheet, the popup and
    the sidebar rail; the row keeps its old title until the desktop acknowledges
    the change; if the desktop refuses, the title is unchanged and the failure is
    reported only through the connection state (see Limitations).

Limitations / risks to attack

  • No live host round trip was run by the AI. The AI did not run a live desktop host, so update_session_title against a real (and especially an older or third-party) host is unverified here. A host that rejects update_session_title does not merely leave the old title: the store's shared handleFailure discards the rejection message and, for a RemoteRejected/CLI_NOT_IMPLEMENTED answer, sets the connection phase to FAILED (RemoteSessionStore.kt:2215-2233). The session list survives, but until the 10 s health probe recovers, the status bar reads as a connection error and the remote composer is disabled. The user is told nothing about the rename itself. This is pre-existing store behaviour that this PR makes reachable from the list, and it is not fixed here.
  • The amendment is the unverified delta. The real-device run covers the
    original revision (10/10 SessionRenameActionTest tests, author-reported). The
    amended revision — the added aSurfaceWithoutARenameCallbackDrawsNoRenameRow
    test and the removed conversation_title_default resource — has only been
    compile-verified here, not re-run on a device, so changes in that delta are not
    device-proven.
  • The rename editor caps drafts at 120 characters, while
    ui/chat/ConversationHeader.TitleEditor caps nothing. The two editors differ
    here; that inconsistency is known and deliberately not widened by this PR.
  • A pre-existing delete-confirmation branch reads no capability (so an
    already-open confirmation can still submit after busy flips). It is out of
    scope and is left untouched, with a code comment marking it.
  • Three node guards are skipped for missing build output / missing packages (see
    Verification); none read the files in this diff.

Honesty disclosure

AI-assisted implementation; testing level: lightly tested. The AI ran no
device and no live desktop host. A real-device run of the original instrumented
suite exists and is author-reported (see Verification and Limitations); the AI
only compiled the amended revision.

Author-reported (not independently verified by the AI): the contributor
manually smoke-tested the rename on a real desktop host from one Android device,
and the new title appeared both in the session list and in the conversation
header. A screenshot of that run is withheld because it shows a private
workspace name.

Checklist

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

… inventing placeholder titles

The session action menu offered view details, archive, export and delete but
never rename, even though the rename path already existed: the conversation
header dispatched RemoteSessionIntent.RenameSession, the shared store handled
it, and a remote session's title was already sent to the desktop as
update_session_title. The list, action sheet, popup and sidebar simply never
drew a row for it, because SessionActionPolicy exposed no rename capability.

Three separate English placeholders made unnamed sessions look named and made a
rename look unapplied: the shared fallbackTitle returned "New Session", the
shared RemoteResponseMapper invented "Session <id>" from the id prefix, and the
Android header fell back to "Remote session". A host that names no title sends
none, so the shared layer now keeps the title blank and each surface renders its
own localized "untitled" wording (sidebar_untitled on Android).

Design:
- SessionActionCapabilities gains an appended canRename flag rather than folding
  rename into archive/export: for a remote session rename is a round trip to the
  desktop, for a general chat it rewrites the local row, so it is neither of the
  local-storage operations those flags describe. The flag is appended so the
  existing four positional arguments keep their meaning.
- The sheet, popup and sidebar take an optional onRename callback: a shell that
  cannot dispatch a rename renders no row instead of a row that does nothing.
- The store keeps authority: the list keeps the old title until the desktop
  answers update_session_title. The editor enforces a 120-character client-side
  limit and refuses a blank or unchanged draft; the host normalizes the title and
  refuses an empty one.
- A blank title is rendered per surface via sidebar_untitled rather than a
  fabricated English name.

Compatibility: SessionActionCapabilities is a Kotlin data class consumed only by
Kotlin code; the HarmonyOS client has its own parallel ETS
SessionActionCapabilities and does not consume it, and the iOS client has no
caller of it, so the appended field breaks neither. fallbackTitle's only Kotlin
caller is the shared RemoteSessionStore; HarmonyOS uses its own ArkTS mapper.
iOS surfaces that render the title raw will show a blank row where a fabricated
"Session <id>" used to be, while most iOS surfaces already localize empty titles.

Tests: new shared unit tests cover the appended flag position and the blank-title
mapping. SessionRenameActionTest (10 instrumented tests) was executed on a real
Android device against the original revision of this branch and passed 10/10
(author-reported, run outside this environment). The amended revision adds
aSurfaceWithoutARenameCallbackDrawsNoRenameRow and removes the unused
conversation_title_default string resource; that amended revision is
compile-checked only (:app:compileDebugAndroidTestKotlin BUILD SUCCESSFUL) with
no device run here, and makes no claim that the added test passed.

Co-authored-by: Driss <[email protected]>
@braindeadz
braindeadz force-pushed the fix/mobile-session-rename branch from 6d7e621 to 5401aef Compare November 14, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant