Skip to content

fix(mobile): highlight the workspace this phone last opened on the host, else the desktop's selection - #31081

Merged
Jinwoo-H merged 7 commits into
sidebar-flip-afrom
phone-own-highlight
Oct 20, 2026
Merged

Jinwoo-H merged 7 commits into
sidebar-flip-afrom
phone-own-highlight

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

On the phone's list of workspaces for a computer, the highlighted row is now the workspace you last opened on this phone. When that row isn't on screen, it shows the computer's selected workspace instead. That includes when you have never opened one there, so a first visit still shows where the desktop is.

What Changed

Before (with the sidebar switch from #30790 on): the phone always highlighted the desktop's selected workspace. Open w2 on the phone while the desktop has w10 selected, come back, and w10 is highlighted.

After:

  • The highlighted row is the workspace this phone last opened on that host, whenever that row is shown. The desktop's selection is not highlighted alongside it.
  • When the phone's row is not shown, the desktop's selected workspace is highlighted instead. That covers a phone that has never opened anything there, and a row that is hidden by a filter, hidden as sleeping, deleted, or narrowed out by search. The phone keeps remembering its row, so the highlight goes back to it if the row returns. A sleeping row that is still listed stays highlighted.
  • While the phone is still reading its saved visit (a moment on open), nothing is highlighted, so the list doesn't flash the desktop's row first.
  • Each paired host remembers its own row, and it survives an app restart.
  • The scroll-to-highlighted-row behaviour follows the same highlight. It stays still while a search is in progress, so a search that moves the highlight to the desktop's row doesn't move the list (and doesn't scroll back when the search clears).

How it works:

  • Tapping a row in the list marks it right away, in the list's own page. The shell opens the session as a separate page on top of the list, so this is what makes Back show the row you just opened.
  • The stored record: the session screen already saves orca:last-visited-worktree whenever the phone shows a workspace. That record now also carries a small per-host map, byHost: { [hostId]: { worktreeId, executionHost? } }, capped at 16 hosts. The top-level fields are unchanged, so home's Resume card and the New Workspace drawer read it as before.
  • Who writes it: only the session screen. Its page loads when the session opens, so its copy of the record is current. The list's tap does not write. A list page loaded long ago holds an older copy, and writing from it could overwrite newer visits to other hosts.
  • Lists drawn by the app itself: these are the native list, including the tablet sidebar next to a session page. They also update when a session page's save reaches the app. That covers taps and openings from notifications and deep links.
  • Server workspaces: executionHost is recorded only for server (runtime:) workspaces, which share ids with the desktop's own rows.
  • Rule: the builders mark the desktop's selection as before: the feed's activeRowKeys, or worktree.ps's isActive before the feed arrives. Then highlightPhoneVisit in mobile/src/worktree/host-list-highlight.ts runs over the rows as shown, after search narrowing. It moves the highlight to the phone's row when that row is shown. That module has no storage or React Native imports, because the e2e phone-list helper builds sections through it.

Phone only. Nothing changed in the wire format, the RPCs, the sidebar feed, the shared row builder, or the desktop. The old switch-off path is untouched.

Known limits:

  • A list running as a web page only sees visits made in other pages when it reloads. Examples are a session opened from a notification or the home Resume card. A tap in the list itself always shows. Fixing the rest needs a shell (bridge) change, and shells are frozen.
  • If an older desktop's page bundle saves the record, it saves it in the old shape and drops the per-host map. The host that record names keeps its visit. Other hosts show the desktop's selection until the phone opens something there again. A separate storage key would avoid this, but the page's storage allowlist lives in the shipped shell, which is frozen.

Why

The phone and the desktop are two screens with two people's attention. Showing the desktop's selection over the phone's own choice made the highlight jump for reasons the phone user couldn't see. When the phone has nothing of its own to show, the desktop's selection is the most useful thing to point at.

I reused the existing "last visited" record instead of a new store. Its only writer is already the session screen, which covers every way of opening a workspace. The page is also already allowed to read and write that key. A new key would need the frozen shell to allow it. I didn't use sidebarInputs.reportView: it reports what is on screen right now to the host, it isn't remembered on the phone, and the phone doesn't send it yet in this stack.

Linked Issue

Follow-up to #30790 (base branch sidebar-flip-a).

Visual Proof

The real phone page bundle, at a 393x852 viewport, talking to a hidden Orca desktop (ORCA_BACKGROUND_LAUNCH=1) whose selected workspace is w10. The page gets the route grants the shell sends, so a tap opens the session as a separate page and the list page stays as Back leaves it. "Before" is the page built from sidebar-flip-a (746e214); "after" is this branch. The desktop build is the same for both.

Scene Before After
Phone never opened anything here: desktop's w10
Tap w2, session opens on top, Back
Phone last opened w2 (record from an earlier visit)
Phone last opened a workspace since deleted: desktop's w10

Testing

  • I manually tested these changes locally (phone page flows above, macOS host)

  • Automated tests added/updated

  • use-last-visited-worktree-on-host.test.tsx runs the list and the session as two pages, each with its own module graph and its own load-time copy of storage, the way the shell does. It checks three things:

    • tap, then the session page saves, then Back still highlights the tapped row, and neither page overwrote a newer visit to another host;
    • a reloaded list reads the saved visit;
    • a list drawn by the app follows a session page's save, and keeps its row when an older-shape save names another host.
  • last-visited-worktree-by-host.test.ts covers the per-host record: a host never opened, per-host memory, a record from before this change, the cap, the server/desktop id match, and a refused save.

  • host-sidebar-feed-sections.test.ts covers the rule:

    • never opened shows the desktop's row; nothing while still reading;
    • the phone's row, when shown, replaces the desktop's; this covers local and SSH rows;
    • hidden by search, then shown again: the desktop's row, then the phone's;
    • deleted: the desktop's row;
    • a server row with the same id does not count as the phone's;
    • pinned rows shown twice; the pre-feed list.
  • use-active-worktree-scroll.test.tsx: the list scrolls to the highlighted row and follows a move, but not a move made during a search, and does not scroll back when the search clears.

  • Local checks, all passing:

    • pnpm tc;
    • mobile tsc --noEmit, and the mobile test-typecheck ratchet;
    • mobile vitest for src/host-screen, src/worktree, src/session, src/mobile-web-shell, src/storage, src/hooks;
    • oxfmt --check (from mobile/);
    • every step of the PR workflow's static-analysis job, including the e2e type-check ratchet (cloud/ packages installed).
  • Not verified: the native app on a device or emulator.

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH: SSH rows are matched by worktree id, which is unique per desktop. Server (runtime:) rows must also match their server.
  • Mixed versions: see Known limits above.
  • Cross-platform: phone-only TypeScript, no platform branches.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

E2E Test and others added 4 commits October 20, 2026 05:18
…ighlight

# Conflicts:
#	mobile/src/host-screen/host-sidebar-feed-sections.ts
#	mobile/src/session/use-mobile-session-keyboard-state.test.tsx
#	mobile/src/session/use-mobile-session-keyboard-state.ts
…d an app list follows a session page's write
@Jinwoo-H Jinwoo-H changed the title fix(mobile): highlight the workspace this phone last opened on the host, never the desktop's selection fix(mobile): highlight the workspace this phone last opened on the host, the desktop's selection only until then Oct 20, 2026
…ow is not shown, and keep storage off the e2e list helper's imports
@Jinwoo-H Jinwoo-H changed the title fix(mobile): highlight the workspace this phone last opened on the host, the desktop's selection only until then fix(mobile): highlight the workspace this phone last opened on the host, else the desktop's selection Oct 20, 2026
@Jinwoo-H
Jinwoo-H merged commit d94cc40 into sidebar-flip-a Oct 20, 2026
40 of 43 checks passed
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