fix(remote): guard browser focus context

This commit is contained in:
Jinwoo-H
2026-08-12 04:38:02 -04:00
parent f4a8d93879
commit 60af48c1c6
2 changed files with 88 additions and 5 deletions
@@ -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<string, unknown>) => ({
...state,
activeWorktreeId: 'repo::/other-worktree'
}),
returnToStart: (state: Record<string, unknown>) => ({
...state,
activeWorktreeId: WORKTREE_ID
})
},
{
label: 'runtime owner',
leave: (state: Record<string, unknown>) => ({
...state,
activeWorkspaceExecutionHostId: 'local'
}),
returnToStart: (state: Record<string, unknown>) => ({
...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<string, unknown> = { state: 'before-stage', activeWorktreeId: 'landing' }
mocks.setState.mockImplementation((updater: (state: unknown) => unknown) => {
const result = updater(mockState)
@@ -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)