From 60af48c1c6b1c2e1bf702c04eb689441dace40c7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 12 Aug 2026 04:08:05 -0400 Subject: [PATCH] fix(remote): guard browser focus context --- .../src/runtime/web-runtime-session.test.ts | 75 +++++++++++++++++++ .../src/runtime/web-runtime-session.ts | 18 +++-- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/runtime/web-runtime-session.test.ts b/src/renderer/src/runtime/web-runtime-session.test.ts index 8c3f02c5e48..a50918658b5 100644 --- a/src/renderer/src/runtime/web-runtime-session.test.ts +++ b/src/renderer/src/runtime/web-runtime-session.test.ts @@ -258,6 +258,7 @@ describe('createWebRuntimeSessionBrowserTab', () => { activeRuntimeEnvironmentId: ENVIRONMENT_ID }, activeWorktreeId: WORKTREE_ID, + activeWorkspaceExecutionHostId: RUNTIME_EXECUTION_HOST_ID, activeTabType: 'editor', activeTabTypeByWorktree: { [WORKTREE_ID]: 'editor' }, activeFileIdByWorktree: { [WORKTREE_ID]: '/worktree/index.html' }, @@ -519,6 +520,67 @@ describe('createWebRuntimeSessionBrowserTab', () => { expect(peekWebSessionFocusIntent({ environmentId: ENVIRONMENT_ID }, WORKTREE_ID)).toBeNull() }) + it.each([ + { + label: 'worktree', + leave: (state: Record) => ({ + ...state, + activeWorktreeId: 'repo::/other-worktree' + }), + returnToStart: (state: Record) => ({ + ...state, + activeWorktreeId: WORKTREE_ID + }) + }, + { + label: 'runtime owner', + leave: (state: Record) => ({ + ...state, + activeWorkspaceExecutionHostId: 'local' + }), + returnToStart: (state: Record) => ({ + ...state, + activeWorkspaceExecutionHostId: RUNTIME_EXECUTION_HOST_ID + }) + } + ])( + 'does not focus after leaving and returning to the $label during reconciliation', + async ({ leave, returnToStart }) => { + let resolveList!: (response: unknown) => void + const listResponse = new Promise((resolve) => { + resolveList = resolve + }) + let state = mocks.getState() + let listener: ((next: typeof state, previous: typeof state) => void) | null = null + mocks.getState.mockImplementation(() => state) + mocks.subscribe.mockImplementation((nextListener) => { + listener = nextListener + return vi.fn() + }) + const runtimeCall = vi + .fn() + .mockResolvedValueOnce({ + id: 'create', + ok: true, + result: { browserPageId: 'created-host-browser' } + }) + .mockReturnValueOnce(listResponse) + vi.stubGlobal('window', { api: { runtimeEnvironments: { call: runtimeCall } } }) + + const pendingCreate = createWebRuntimeSessionBrowserTab({ worktreeId: WORKTREE_ID }) + await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(2)) + const awayState = leave(state) + listener!(awayState, state) + const returnedState = returnToStart(awayState) + listener!(returnedState, awayState) + state = returnedState + resolveList({ id: 'list', ok: true, result: makeSnapshot() }) + await expect(pendingCreate).resolves.toBe(true) + + expect(peekWebSessionFocusIntent({ environmentId: ENVIRONMENT_ID }, WORKTREE_ID)).toBeNull() + } + ) + it('does not retry a failed browser create', async () => { const runtimeCall = vi.fn().mockResolvedValue({ id: 'create-lost', @@ -618,6 +680,19 @@ describe('createWebRuntimeSessionBrowserTab', () => { it('keeps the requested worktree selected while the browser snapshot catches up', async () => { const snapshot = makeSnapshot() const setStateResults: unknown[] = [] + let focusState = { + ...mocks.getState(), + activeWorktreeId: 'landing', + activeWorkspaceExecutionHostId: 'local' + } + mocks.getState.mockImplementation(() => focusState) + mocks.setActiveWorktree.mockImplementation((worktreeId, executionHostId) => { + focusState = { + ...focusState, + activeWorktreeId: worktreeId, + activeWorkspaceExecutionHostId: executionHostId + } + }) let mockState: Record = { state: 'before-stage', activeWorktreeId: 'landing' } mocks.setState.mockImplementation((updater: (state: unknown) => unknown) => { const result = updater(mockState) diff --git a/src/renderer/src/runtime/web-runtime-session.ts b/src/renderer/src/runtime/web-runtime-session.ts index 896052a2830..b48e8e68270 100644 --- a/src/renderer/src/runtime/web-runtime-session.ts +++ b/src/renderer/src/runtime/web-runtime-session.ts @@ -452,10 +452,6 @@ export async function createWebRuntimeSessionBrowserTab(args: { const callEnvironment = captureRuntimeEnvironmentCall(environmentId, intentOwner.pairingRevision) const shouldSelectWorktree = args.selectWorktree !== false const provisionalPageId = createBrowserUuid() - const expectedCurrentLocalTabId = resolveWebSessionVisibleTabId( - useAppStore.getState(), - args.worktreeId - ) if (args.clientTargetGroupId) { recordWebSessionBrowserPlacement({ environmentId, @@ -467,6 +463,13 @@ export async function createWebRuntimeSessionBrowserTab(args: { if (shouldSelectWorktree) { selectWebRuntimeSessionBrowserWorktree(args.worktreeId, environmentId) } + const initialFocusState = useAppStore.getState() + const expectedActiveWorktreeId = initialFocusState.activeWorktreeId + const expectedActiveWorkspaceExecutionHostId = initialFocusState.activeWorkspaceExecutionHostId + const expectedCurrentLocalTabId = resolveWebSessionVisibleTabId( + initialFocusState, + args.worktreeId + ) let unsubscribeFocusGuard = (): void => {} let guardedPageId = provisionalPageId try { @@ -486,11 +489,16 @@ export async function createWebRuntimeSessionBrowserTab(args: { state.activeTabType === previousState.activeTabType && state.activeTabTypeByWorktree === previousState.activeTabTypeByWorktree && state.activeWorktreeId === previousState.activeWorktreeId && + state.activeWorkspaceExecutionHostId === previousState.activeWorkspaceExecutionHostId && state.unifiedTabsByWorktree === previousState.unifiedTabsByWorktree ) { return } - if (resolveWebSessionVisibleTabId(state, args.worktreeId) === expectedCurrentLocalTabId) { + if ( + state.activeWorktreeId === expectedActiveWorktreeId && + state.activeWorkspaceExecutionHostId === expectedActiveWorkspaceExecutionHostId && + resolveWebSessionVisibleTabId(state, args.worktreeId) === expectedCurrentLocalTabId + ) { return } clearWebSessionFocusIntentIfMatches(intentOwner, args.worktreeId, guardedPageId)