fix(browser): an agent's new server tab takes the selection from the user's This-device page (#41953)

Pageless agent commands prefer the workspace's active client page, and creating
an offscreen or renderer tab never cleared it, so the next command after
`orca tab create` acted on the user's page. Yield client selection wherever a
create moves the bridge's active tab, as orcad already does.
This commit is contained in:
Kelvin Amoaba
2026-11-13 11:18:51 +00:00
committed by GitHub
parent 94e84e4044
commit f4d294dac2
5 changed files with 59 additions and 8 deletions
@@ -158,11 +158,12 @@ export async function yieldClientPageSelection(
worktree: string | undefined
): Promise<void> {
const pages = host.getRuntimeBrowserPageRegistry()
if (worktree) {
pages.yieldSelection((await host.resolveBrowserWorkspace(worktree)).id)
return
}
pages.deactivateGlobal()
const workspaceIds = worktree
? [(await host.resolveBrowserWorkspace(worktree)).id]
: [...new Set(pages.listPages().map((page) => page.workspaceId))]
for (const workspaceId of workspaceIds) {
for (const workspaceId of new Set(pages.listPages().map((page) => page.workspaceId))) {
pages.deactivateWorkspace(workspaceId)
}
}
@@ -191,4 +191,42 @@ describe('RuntimeBrowserCommands browser screencast', () => {
expect(waitForTabRegistrationMock).toHaveBeenCalledWith('page-new')
expect(bridge.setActiveTab).toHaveBeenCalledWith(101, 'wt-1')
})
// Pageless agent commands prefer an active client page; an agent's new server tab must take that over.
it('moves the workspace selection off a client page when a server tab is created', async () => {
const { RuntimeBrowserCommands } = await import('../commands/browser-commands')
const pages = new RuntimeBrowserPageRegistry()
pages.publishClientPage({
browserPageId: 'user-page',
workspaceId: 'wt-1',
browserProfileId: 'default',
executionHostKey: 'local',
placement: {
kind: 'client',
browserHostClientId: 'desktop-1',
browserHostGeneration: 1,
pageHostGeneration: 1
},
url: 'https://user.example/',
loading: false,
active: true
})
const bridge = {
getRegisteredTabs: vi.fn(() => new Map([['agent-page', 102]])),
setActiveTab: vi.fn()
}
const offscreen = { createTab: vi.fn(async () => ({ browserPageId: 'agent-page' })) }
const host = createHost({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: create reaches only these two bridge methods.
getAgentBrowserBridge: () => bridge as unknown as AgentBrowserBridge,
getRuntimeBrowserPageRegistry: () => pages,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: create reaches only createTab.
getOffscreenBrowserBackend: () => offscreen as never
})
const commands = new RuntimeBrowserCommands(host)
await commands.browserTabCreate({ worktree: 'id:wt-1', url: 'https://agent.example/' })
expect(bridge.setActiveTab).toHaveBeenCalledWith(102, 'wt-1')
expect(pages.listPages('wt-1').find((page) => page.active)).toBeUndefined()
})
})
@@ -199,6 +199,14 @@ export class RuntimeBrowserPageRegistry {
this.globalActivePageId = null
}
/** An engine tab took the selection; unscoped creates and switches move only the global pick. */
yieldSelection(workspaceId: string | undefined): void {
this.deactivateGlobal()
if (workspaceId !== undefined) {
this.deactivateWorkspace(workspaceId)
}
}
deactivateWorkspace(workspaceId: string): void {
for (const [browserPageId, page] of this.pages) {
if (page.workspaceId === workspaceId && page.active) {
@@ -1,4 +1,5 @@
import type { AgentBrowserBridge } from '../../../browser/agent-browser/agent-browser-bridge'
import type { RuntimeBrowserPageRegistry } from '../client-pages/runtime-browser-page-registry'
import {
navigationTargetsHost,
resolveRuntimeNavigationTarget,
@@ -20,6 +21,7 @@ type BrowserTabCreatePublicationBridge = Pick<
export type BrowserTabCreatePublicationHost = {
getAgentBrowserBridge(): BrowserTabCreatePublicationBridge | null
getRuntimeBrowserPageRegistry(): Pick<RuntimeBrowserPageRegistry, 'yieldSelection'>
markHeadlessBrowserSessionTabActive?(
worktreeId: string | undefined,
browserPageId: string,
@@ -194,6 +196,11 @@ export function publishCreatedBrowserSessionTab(
?.getRegisteredTabs(publication.worktreeId)
.get(publication.browserPageId)
if (bridge && webContentsId != null) {
// Pageless commands prefer an active client page, so the new engine tab must take over from it.
// Yield first so the bridge's own tabs-changed publication carries the cleared client row.
if (publication.focus.startsActive) {
host.getRuntimeBrowserPageRegistry().yieldSelection(publication.worktreeId)
}
bridge.setActiveTab(webContentsId, publication.worktreeId)
}
}
@@ -166,10 +166,7 @@ export class RuntimeBrowserTabQueries {
? (await this.host.resolveBrowserWorkspace(params.worktree)).id
: undefined
const result = await bridge.tabSwitch(undefined, worktreeId, selected.browserPageId)
this.host.getRuntimeBrowserPageRegistry().deactivateGlobal()
if (worktreeId) {
this.host.getRuntimeBrowserPageRegistry().deactivateWorkspace(worktreeId)
}
this.host.getRuntimeBrowserPageRegistry().yieldSelection(worktreeId)
// Scope focus to the owning worktree; never switch the user's workspace.
const focusWorktreeId =
worktreeId ?? browserManager.getWorktreeIdForTab(result.browserPageId) ?? undefined