From be29644ec9def1723952ef46a33eef7afe23f469 Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:12:13 +0000 Subject: [PATCH] fix(mobile): open a worktree on the tab the desktop is showing (#31215) --- .../client-session-tab-selection.test.ts | 24 ++++++++++++++----- .../runtime/client-session-tab-selection.ts | 13 ++++++---- ...t-navigation-isolation.integration.test.ts | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/main/runtime/client-session-tab-selection.test.ts b/src/main/runtime/client-session-tab-selection.test.ts index 1c9e0e4a575..3b7d6771245 100644 --- a/src/main/runtime/client-session-tab-selection.test.ts +++ b/src/main/runtime/client-session-tab-selection.test.ts @@ -130,15 +130,27 @@ describe('client session-tab selection', () => { expect(store.project(initial, 'device-a').activeTabId).toBe('terminal-a::leaf-a') }) - it('does not expose host focus when initializing a new paired device', () => { + it('lands a device that has not picked a tab on the host focus', () => { const store = new ClientSessionTabSelectionStore() - const hostFocusedBrowser = snapshot('browser-unified') - const projected = store.project(hostFocusedBrowser, 'new-device') + const projected = store.project(snapshot('browser-unified'), 'new-device') - expect(projected.activeTabId).toBe('terminal-a::leaf-a') - expect(projected.activeGroupId).toBe('group-left') - expect(projected.tabs.find((tab) => tab.isActive)?.id).toBe('terminal-a::leaf-a') + expect(projected.activeTabId).toBe('browser-unified') + expect(projected.activeGroupId).toBe('group-right') + expect(projected.tabs.find((tab) => tab.isActive)?.id).toBe('browser-unified') + // A later open follows where the host moved, not the first projection. + expect(store.project(snapshot('terminal-a::leaf-b'), 'new-device').activeTabId).toBe( + 'terminal-a::leaf-b' + ) + }) + + it('keeps a device on its own pick when the host moves', () => { + const store = new ClientSessionTabSelectionStore() + store.activate(snapshot(), 'device-a', 'browser-unified') + + expect(store.project(snapshot('terminal-a::leaf-b'), 'device-a').activeTabId).toBe( + 'browser-unified' + ) }) it('persists activations and restores them across a store rebuild (host restart)', () => { diff --git a/src/main/runtime/client-session-tab-selection.ts b/src/main/runtime/client-session-tab-selection.ts index 30dc97c77f9..d40dca54a3d 100644 --- a/src/main/runtime/client-session-tab-selection.ts +++ b/src/main/runtime/client-session-tab-selection.ts @@ -193,7 +193,6 @@ export class ClientSessionTabSelectionStore { const statesByWorktree = this.getStatesByWorktree(clientNavigationId) const existingState = statesByWorktree.get(snapshot.worktree) const state = existingState ?? { - // Why: host focus is private navigation; a new paired device starts from deterministic topology instead of inheriting it. selection: emptyClientSessionTabSelection(), revision: 0, shouldPersist: false, @@ -214,14 +213,18 @@ export class ClientSessionTabSelectionStore { snapshotVersion: snapshot.snapshotVersion + state.revision } } - const projected = projectClientSessionTabSelection(closed.snapshot, state.selection) + // Why: until the device picks a tab here, opening the worktree lands on what the host shows; + // clients keep their own tab while open, so this never moves a device mid-visit. + const selection = state.shouldPersist + ? state.selection + : deriveClientSessionTabSelection(closed.snapshot) + const projected = projectClientSessionTabSelection(closed.snapshot, selection) // Why: a browser guest process swap drops its tab for one snapshot; the topology fallback // must not overwrite (and persist) the device's explicit pick, or focus never returns. const selectionSurvived = - !state.selection.activeTabId || - closed.snapshot.tabs.some((tab) => tab.id === state.selection.activeTabId) + !selection.activeTabId || closed.snapshot.tabs.some((tab) => tab.id === selection.activeTabId) statesByWorktree.set(snapshot.worktree, { - selection: selectionSurvived ? projected.selection : state.selection, + selection: selectionSurvived ? projected.selection : selection, revision: state.revision, shouldPersist: state.shouldPersist, closedTabIds: closed.retainedClosedTabIds diff --git a/src/main/runtime/multi-client-navigation-isolation.integration.test.ts b/src/main/runtime/multi-client-navigation-isolation.integration.test.ts index 289e7ccb0d6..7fbbbc4803f 100644 --- a/src/main/runtime/multi-client-navigation-isolation.integration.test.ts +++ b/src/main/runtime/multi-client-navigation-isolation.integration.test.ts @@ -807,7 +807,7 @@ describe('paired runtime navigation isolation', () => { ) expect(remembered.activeTabId).toBe('client-a-tab') expect(remembered.tabs.find((tab) => tab.isActive)?.id).toBe('client-a-tab') - // Why: an unknown device must still start from deterministic topology, not inherit another device's restored state. + // Why: an unknown device starts from the host's view, not another device's restored state. const freshDevice = await restarted.listMobileSessionTabs( `id:${SESSION_WORKTREE_ID}`, 'device-b'