diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 4255fcad222..ee84290430a 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -1378,35 +1378,31 @@ "providers": ["remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["remote-runtime"], - "coverageNotes": "A headed macOS desktop host and separate paired web client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, terminal focus transitions, and browser close. Unit contracts cover routing, one-shot split placement, manual moves, overlapping opens, teardown, non-secure LAN UUID generation, and capability-gated deterministic headless page creation after response loss. Linux, Windows, and a live headless-server journey remain gaps.", + "coverageNotes": "A headed macOS desktop host and separate paired web client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, terminal focus transitions, and browser close. Unit contracts cover owning-runtime routing, one-shot split placement, focus ordering against the exact predecessor tab, manual moves, overlapping opens, failure cleanup, and teardown. Linux, Windows, and a live headless-server journey remain gaps.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-3872/unable-to-open-html-files-in-remote-server", "https://github.com/stablyai/orca/pull/13876" ], "invariant": "Opening one HTML preview from a paired client creates exactly one browser page on the worktree-owning runtime and exactly one mirrored client browser workspace. The preview remains in the requested client-side split, activating it remains authoritative after a terminal was active, and closing it removes only the browser identities while the source editor and terminal survive.", - "oracle": "Open a real HTML file through Explorer on a separate client paired to an isolated headed desktop host. Require one new host browser tab, one client browser workspace and unified tab, two visible split groups, an exact client-handle-to-host-page identity, rendered fixture DOM, host and client browser focus after a terminal-to-browser click, and browser-only close convergence with the editor and terminal still visible. Separately drop create responses and retry the same requested page identity, requiring one physical headless page. Run the byte-identical Electron test on latest main, the candidate, and the candidate with its fix reverted.", + "oracle": "Open a real HTML file through Explorer on a separate client paired to an isolated headed desktop host. Require one new host browser tab, one client browser workspace and unified tab, two visible split groups, an exact client-handle-to-host-page identity, rendered fixture DOM, host and client browser focus after a terminal-to-browser click, and browser-only close convergence with the editor and terminal still visible. Run the byte-identical Electron test on latest main, the candidate, and the candidate with owning-runtime routing disabled.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/offscreen-browser-backend.test.ts src/main/runtime/orca-runtime-browser.test.ts src/main/runtime/rpc/methods/browser.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/runtime/web-session-browser-placement.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/main/runtime/rpc/methods/browser.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/runtime/web-session-browser-placement.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts src/renderer/src/runtime/remote-server-parity.test.ts", "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --repeat-each=3", "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" ], "testFiles": [ - "src/main/browser/offscreen-browser-backend.test.ts", "src/main/runtime/orca-runtime-browser.test.ts", "src/main/runtime/rpc/methods/browser.test.ts", "src/renderer/src/lib/file-preview.test.ts", "src/renderer/src/runtime/web-session-browser-placement.test.ts", "src/renderer/src/runtime/web-runtime-session.test.ts", "src/renderer/src/runtime/web-session-tabs-sync.test.ts", + "src/renderer/src/runtime/remote-server-parity.test.ts", "tests/e2e/paired-remote-html-browser-focus.spec.ts" ], "assertionRefs": [ - { - "file": "src/main/browser/offscreen-browser-backend.test.ts", - "assertions": ["retrying one requested page identity returns one physical offscreen page"] - }, { "file": "src/renderer/src/runtime/web-session-browser-placement.test.ts", "assertions": [ @@ -1422,6 +1418,12 @@ "later client moves supersede initial preview placement" ] }, + { + "file": "src/renderer/src/runtime/remote-server-parity.test.ts", + "assertions": [ + "a caller-created browser gains focus when the exact predecessor tab is still visible" + ] + }, { "file": "tests/e2e/paired-remote-html-browser-focus.spec.ts", "assertions": [ @@ -1439,7 +1441,7 @@ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --repeat-each=3", "result": "passed", "durationSeconds": 16.1, - "summary": "Deterministic-identity candidate on merge-base 7319d59a103311a5fbf3affb5d293f3ba1b9594a passed exact host/client cardinality, split stability, rendered DOM, focus authority, close convergence, and surviving editor and terminal." + "summary": "The candidate passed exact host/client cardinality, split stability, rendered DOM, focus authority, close convergence, and surviving editor and terminal." }, { "date": "2026-08-12", @@ -1471,7 +1473,7 @@ ], "runtimeBudget": { "p95Seconds": 60, - "scope": "six focused host/renderer contracts plus one isolated headed paired-runtime journey" + "scope": "seven focused host/renderer contracts plus one isolated headed paired-runtime journey" }, "flakeHistory": { "status": "unknown", @@ -1483,16 +1485,16 @@ }, "performanceBudget": { "required": true, - "evidence": "The change adds no timer, polling, subprocess, RPC method, stream opcode, receipt ledger, or host publication. One optional requested-page field is capability-gated for mixed versions. Pending client placements are capped at 128 and cleared on adoption or worktree/environment teardown." + "evidence": "The change adds no timer, polling, retry, subprocess, RPC method, stream opcode, receipt ledger, or host publication. It performs one bounded scan of the current worktree's local tabs at user-initiated browser creation. Pending client placements are capped at 128 and cleared on adoption, failure, or worktree/environment teardown." }, "promotionCriteria": [ "Collect Linux and physical Windows paired-server evidence.", - "Collect a live headless-serve response-loss journey.", + "Collect a live headless-serve HTML preview journey.", "Collect 100 consecutive focused CI passes or 14 days without an unexplained flake." ], "knownGaps": [ "Live paired-server validation currently covers macOS only.", - "A live headless-serve response-loss journey and multiple simultaneous viewers are not exercised by the Electron journey; deterministic host/renderer contracts cover response-loss replay.", + "A live headless-serve journey and multiple simultaneous viewers are not exercised by the Electron journey.", "Direct SSH remote file URLs remain intentionally unsupported and local file previews retain their existing path." ], "demotionRule": "Demote if one paired HTML action can create duplicate client or host browser identities, lose the requested split, bounce browser focus to another surface, or remove unrelated editor or terminal state on close." diff --git a/src/main/browser/browser-backend.ts b/src/main/browser/browser-backend.ts index 939684e49b3..697eb7c178f 100644 --- a/src/main/browser/browser-backend.ts +++ b/src/main/browser/browser-backend.ts @@ -10,7 +10,6 @@ export type BrowserBackendCreateTab = { url: string worktreeId?: string profileId?: string - browserPageId?: string } export type BrowserBackend = { diff --git a/src/main/browser/offscreen-browser-backend.test.ts b/src/main/browser/offscreen-browser-backend.test.ts deleted file mode 100644 index 145ccbeca3d..00000000000 --- a/src/main/browser/offscreen-browser-backend.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { BrowserManager } from './browser-manager' - -const mocks = vi.hoisted(() => ({ - constructed: vi.fn(), - nextWebContentsId: 1 -})) - -vi.mock('electron', () => ({ - BrowserWindow: class { - private destroyed = false - readonly webContents = { - id: mocks.nextWebContentsId++, - once: vi.fn(), - on: vi.fn((event: string, listener: () => void) => { - if (event === 'did-finish-load') { - listener() - } - }), - removeListener: vi.fn(), - loadURL: vi.fn(async () => undefined) - } - - constructor() { - mocks.constructed() - } - - isDestroyed(): boolean { - return this.destroyed - } - - destroy(): void { - this.destroyed = true - } - } -})) - -vi.mock('./browser-session-registry', () => ({ - browserSessionRegistry: { - getDefaultProfile: vi.fn(() => null), - getProfile: vi.fn(() => null) - } -})) - -import { OffscreenBrowserBackend } from './offscreen-browser-backend' - -describe('OffscreenBrowserBackend deterministic page creation', () => { - beforeEach(() => { - mocks.constructed.mockClear() - mocks.nextWebContentsId = 1 - }) - - it('returns the existing page when the same requested identity is retried', async () => { - const browserManager = { - registerOffscreenGuest: vi.fn(), - unregisterGuest: vi.fn() - } as unknown as BrowserManager - const backend = new OffscreenBrowserBackend(browserManager) - const browserPageId = '00000000-0000-4000-8000-000000000001' - - await expect( - backend.createTab({ url: 'about:blank', worktreeId: 'worktree-1', browserPageId }) - ).resolves.toEqual({ browserPageId }) - await expect( - backend.createTab({ url: 'about:blank', worktreeId: 'worktree-1', browserPageId }) - ).resolves.toEqual({ browserPageId }) - - expect(mocks.constructed).toHaveBeenCalledOnce() - expect(browserManager.registerOffscreenGuest).toHaveBeenCalledOnce() - }) -}) diff --git a/src/main/browser/offscreen-browser-backend.ts b/src/main/browser/offscreen-browser-backend.ts index d36cd2aba9a..5eb6e9b8435 100644 --- a/src/main/browser/offscreen-browser-backend.ts +++ b/src/main/browser/offscreen-browser-backend.ts @@ -23,11 +23,7 @@ export class OffscreenBrowserBackend implements BrowserBackend { constructor(private readonly browserManager: BrowserManager) {} async createTab(params: BrowserBackendCreateTab): Promise<{ browserPageId: string }> { - const browserPageId = params.browserPageId ?? randomUUID() - const existing = this.windowsByPageId.get(browserPageId) - if (existing && !existing.isDestroyed()) { - return { browserPageId } - } + const browserPageId = randomUUID() // Why: profiles map to Electron partitions; using the profile's partition // makes cookies/storage persist in the same SQLite DB the desktop path uses. const profile = params.profileId @@ -56,10 +52,8 @@ export class OffscreenBrowserBackend implements BrowserBackend { // teardown), drop the registry entry so commands fail cleanly instead of // resolving a dead WebContents. win.webContents.once('destroyed', () => { - if (this.windowsByPageId.get(browserPageId) === win) { - this.windowsByPageId.delete(browserPageId) - this.browserManager.unregisterGuest(browserPageId) - } + this.windowsByPageId.delete(browserPageId) + this.browserManager.unregisterGuest(browserPageId) }) // Why: register the guest and return immediately so the new tab appears diff --git a/src/main/runtime/orca-runtime-browser.test.ts b/src/main/runtime/orca-runtime-browser.test.ts index 6cca51789c3..5cc74aa4ad4 100644 --- a/src/main/runtime/orca-runtime-browser.test.ts +++ b/src/main/runtime/orca-runtime-browser.test.ts @@ -95,7 +95,6 @@ function createHost(overrides: Partial = {}): Runtime })) } as unknown as AgentBrowserBridge) return { - getRuntimeId: () => 'runtime-1', resolveWorktreeSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }), getAuthoritativeWindow: vi.fn(), getAvailableAuthoritativeWindow: vi.fn(() => null), @@ -642,42 +641,13 @@ describe('RuntimeBrowserCommands headless offscreen routing', () => { expect(createTab).toHaveBeenCalledWith({ url: 'https://example.com', worktreeId: 'wt-1', - profileId: undefined, - browserPageId: undefined + profileId: undefined }) // No renderer round-trip in headless mode. expect(waitForTabRegistrationMock).not.toHaveBeenCalled() expect(setActiveTab).toHaveBeenCalledWith(202, 'wt-1') }) - it('forwards deterministic page identity to the headless backend', async () => { - const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') - const pageId = '00000000-0000-4000-8000-000000000001' - const createTab = vi.fn(async () => ({ browserPageId: pageId })) - const bridge = { - getRegisteredTabs: vi.fn(() => new Map([[pageId, 202]])), - setActiveTab: vi.fn() - } as unknown as AgentBrowserBridge - const commands = new RuntimeBrowserCommands( - createHost({ - getAgentBrowserBridge: () => bridge, - getAvailableAuthoritativeWindow: vi.fn(() => null), - getOffscreenBrowserBackend: vi.fn(() => ({ createTab, closeTab: vi.fn() })) - }) - ) - - await expect( - commands.browserTabCreate({ url: 'https://example.com', requestedPageId: pageId }) - ).resolves.toEqual({ browserPageId: pageId }) - - expect(createTab).toHaveBeenCalledWith({ - url: 'https://example.com', - worktreeId: undefined, - profileId: undefined, - browserPageId: pageId - }) - }) - it('rejects tab creation when neither a renderer nor an offscreen backend is available', async () => { const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') const commands = new RuntimeBrowserCommands( diff --git a/src/main/runtime/orca-runtime-browser.ts b/src/main/runtime/orca-runtime-browser.ts index ef2f8076d0c..6070cefd374 100644 --- a/src/main/runtime/orca-runtime-browser.ts +++ b/src/main/runtime/orca-runtime-browser.ts @@ -162,6 +162,7 @@ export class RuntimeBrowserCommands { private readonly activeScreencastPageIds = new Set() private readonly activeScreencastsByPageId = new Map() private readonly stoppingScreencastPageIds = new Map>() + constructor(private readonly host: RuntimeBrowserCommandHost) {} private requireAgentBrowserBridge(): AgentBrowserBridge { @@ -1297,7 +1298,6 @@ export class RuntimeBrowserCommands { waitForRegistration?: boolean activate?: boolean targetGroupId?: string - requestedPageId?: string }): Promise<{ browserPageId: string }> { const url = params.url ?? 'about:blank' const worktreeId = params.worktree @@ -1322,8 +1322,7 @@ export class RuntimeBrowserCommands { worktreeId, params.profileId, params.activate, - params.targetGroupId, - params.requestedPageId + params.targetGroupId ) } const { browserPageId } = await this.createBrowserTabInRenderer( @@ -1707,15 +1706,9 @@ export class RuntimeBrowserCommands { worktreeId?: string, profileId?: string, activate?: boolean, - targetGroupId?: string, - requestedPageId?: string + targetGroupId?: string ): Promise<{ browserPageId: string }> { - const { browserPageId } = await offscreen.createTab({ - url, - worktreeId, - profileId, - browserPageId: requestedPageId - }) + const { browserPageId } = await offscreen.createTab({ url, worktreeId, profileId }) const bridge = this.host.getAgentBrowserBridge() const wcId = bridge?.getRegisteredTabs(worktreeId).get(browserPageId) if (bridge && wcId != null) { diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 7871eeb6e02..ec4d930ad79 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -2182,7 +2182,6 @@ describe('OrcaRuntimeService', () => { expect(capabilities).toContain('browser.screencast.v1') // ...and the headless marker tells clients not to fall back to a local tab. expect(capabilities).toContain('browser.headless.v1') - expect(capabilities).toContain('browser.deterministic-page-create.v1') expect(capabilities).toContain('browser.certificate-trust.v1') }) it('surfaces live offscreen load failures in headless browser snapshots', () => { @@ -2308,7 +2307,6 @@ describe('OrcaRuntimeService', () => { runtime.setOffscreenBrowserBackend({ createTab: vi.fn(), closeTab: vi.fn() }) expect(runtime.getStatus().capabilities).not.toContain('browser.headless.v1') - expect(runtime.getStatus().capabilities).not.toContain('browser.deterministic-page-create.v1') // Desktop webviews still host certificate trust, so the proceed capability stays advertised for remote clients controlling those pages. expect(runtime.getStatus().capabilities).toContain('browser.certificate-trust.v1') }) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index b9ed1a649a3..6e81da239ce 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -492,7 +492,6 @@ import { } from '../../shared/agent-scratch-worktrees' import { BROWSER_HEADLESS_RUNTIME_CAPABILITY, - BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY, BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY, MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION, ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY, @@ -4878,7 +4877,6 @@ export class OrcaRuntimeService { ) if (hasOffscreen) { capabilities.push(BROWSER_HEADLESS_RUNTIME_CAPABILITY) - capabilities.push(BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY) } // Why: certificate proceed is owned by the browser-hosting process for both // desktop webviews and offscreen pages. Advertise whenever either backend diff --git a/src/main/runtime/rpc/methods/browser-core.ts b/src/main/runtime/rpc/methods/browser-core.ts index 84a0cf2ab05..41484d20768 100644 --- a/src/main/runtime/rpc/methods/browser-core.ts +++ b/src/main/runtime/rpc/methods/browser-core.ts @@ -21,6 +21,7 @@ import { Scroll, Select, SelectorPath, + TabCurrent, TabSetProfile, TabClose, TabCreate, @@ -101,7 +102,7 @@ export const BROWSER_CORE_METHODS: RpcMethod[] = [ }), defineMethod({ name: 'browser.tabCurrent', - params: TabList, + params: TabCurrent, handler: async (params, { runtime }) => runtime.browserTabCurrent(params) }), defineMethod({ diff --git a/src/main/runtime/rpc/methods/browser-schemas.ts b/src/main/runtime/rpc/methods/browser-schemas.ts index e13fae9128b..a9a2a440f8f 100644 --- a/src/main/runtime/rpc/methods/browser-schemas.ts +++ b/src/main/runtime/rpc/methods/browser-schemas.ts @@ -1,4 +1,5 @@ -// Why: separate schemas keep dispatch wiring scannable and both files under the cap. +// Why: browser schemas stay separate from handler registration so both sides +// remain under the line cap and dispatch wiring stays scannable. import { z } from 'zod' import { BrowserTarget, @@ -115,10 +116,11 @@ export const TabCreate = z.object({ worktree: OptionalString, profileId: OptionalString, waitForRegistration: z.boolean().optional(), + // User-initiated opens focus the tab; agent/automation opens stay background. activate: z.boolean().optional(), - // Why: headless hosts need the clicked split instead of their first/active group. - targetGroupId: OptionalString, - requestedPageId: z.uuid().optional() + // Why: the split group whose "+" was clicked, so a headless host places the + // new browser tab there instead of coalescing into the first/active group. + targetGroupId: OptionalString }) export const TabShow = z.object({ @@ -126,6 +128,8 @@ export const TabShow = z.object({ worktree: OptionalString }) +export const TabCurrent = z.object({ worktree: OptionalString }) + export const TabClose = z.object({ index: z .unknown() diff --git a/src/main/runtime/rpc/methods/browser.test.ts b/src/main/runtime/rpc/methods/browser.test.ts index 466e8e679c2..57095000146 100644 --- a/src/main/runtime/rpc/methods/browser.test.ts +++ b/src/main/runtime/rpc/methods/browser.test.ts @@ -53,8 +53,7 @@ describe('browser RPC methods', () => { makeRequest('browser.tabCreate', { worktree: 'id:wt-1', url: 'https://example.com', - profileId: 'profile-1', - requestedPageId: '00000000-0000-4000-8000-000000000001' + profileId: 'profile-1' }) ) await dispatcher.dispatch( @@ -82,8 +81,7 @@ describe('browser RPC methods', () => { expect(runtime.browserTabCreate).toHaveBeenCalledWith({ worktree: 'id:wt-1', url: 'https://example.com', - profileId: 'profile-1', - requestedPageId: '00000000-0000-4000-8000-000000000001' + profileId: 'profile-1' }) expect(runtime.browserTabSwitch).toHaveBeenCalledWith({ worktree: 'id:wt-1', diff --git a/src/renderer/src/runtime/remote-server-parity.test.ts b/src/renderer/src/runtime/remote-server-parity.test.ts index 8e9be06da0c..41318a7248e 100644 --- a/src/renderer/src/runtime/remote-server-parity.test.ts +++ b/src/renderer/src/runtime/remote-server-parity.test.ts @@ -304,7 +304,13 @@ describe('parity ยง11: remote browser create focuses the new browser tab', () => it('honors focus intent for a newly created browser session tab', () => { const pageId = 'browser-page-1' const prior = stateWithLocalTerminals([localTerminal('host-tab-1', 0, true)]) - recordWebSessionFocusIntent({ environmentId: ENV }, WT, pageId) + recordWebSessionFocusIntent( + { environmentId: ENV }, + WT, + pageId, + undefined, + toWebTerminalSurfaceTabId('host-tab-1') + ) const patch = applyWebSessionTabsSnapshot( prior, makeSnapshot([{ parentTab: 'host-tab-1', leaf: LEAF_A, active: false }], { diff --git a/src/renderer/src/runtime/web-runtime-session.test.ts b/src/renderer/src/runtime/web-runtime-session.test.ts index 712a4588837..eb20243e1ff 100644 --- a/src/renderer/src/runtime/web-runtime-session.test.ts +++ b/src/renderer/src/runtime/web-runtime-session.test.ts @@ -1,7 +1,6 @@ /* eslint-disable max-lines */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' -import type * as RuntimeRpcClientModule from './runtime-rpc-client' import { activateWebRuntimeSessionWorktree, activateWebRuntimeSessionTab, @@ -35,7 +34,6 @@ import { resetWebAgentSessionHandoffsForTests } from './web-agent-session-handoff' import { toRuntimeExecutionHostId } from '../../../shared/execution-host' -import { BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import { isWebSessionBrowserPlacementGroupReserved, resetWebSessionBrowserPlacementsForTests @@ -56,8 +54,7 @@ const mocks = vi.hoisted(() => ({ trackTerminalPaneSplit: vi.fn(), deliverLaunchPromptToAgentTab: vi.fn(), seedNativeChatLaunchDraftForAgentTab: vi.fn(), - getRuntimeEnvironmentIdForWorktree: vi.fn(), - getRuntimeEnvironmentStatus: vi.fn() + getRuntimeEnvironmentIdForWorktree: vi.fn() })) vi.mock('../store', () => ({ @@ -75,11 +72,6 @@ vi.mock('./web-session-tabs-sync', () => ({ resolveHostSessionTabIdForWebSessionTab: mocks.resolveHostSessionTabIdForWebSessionTab })) -vi.mock('./runtime-rpc-client', async (importOriginal) => ({ - ...(await importOriginal()), - getRuntimeEnvironmentStatus: mocks.getRuntimeEnvironmentStatus -})) - vi.mock('@/lib/feature-education-telemetry', () => ({ trackTerminalPaneSplit: mocks.trackTerminalPaneSplit })) @@ -264,9 +256,20 @@ describe('createWebRuntimeSessionBrowserTab', () => { activeRuntimeEnvironmentId: ENVIRONMENT_ID }, activeWorktreeId: WORKTREE_ID, + activeTabType: 'editor', + activeTabTypeByWorktree: { [WORKTREE_ID]: 'editor' }, + activeFileIdByWorktree: { [WORKTREE_ID]: '/worktree/index.html' }, browserPagesByWorkspace: {}, remoteBrowserPageHandlesByPageId: {}, - unifiedTabsByWorktree: {}, + unifiedTabsByWorktree: { + [WORKTREE_ID]: [ + { + id: 'local-editor-tab', + entityId: '/worktree/index.html', + contentType: 'editor' + } + ] + }, createBrowserTab: mocks.createBrowserTab, closeEmptyGroup: mocks.closeEmptyGroup, moveUnifiedTabToGroup: mocks.moveUnifiedTabToGroup, @@ -289,10 +292,6 @@ describe('createWebRuntimeSessionBrowserTab', () => { mocks.applyFreshWebSessionTabsSnapshot.mockReturnValue({ state: 'after' }) mocks.resolveHostSessionTabIdForWebSessionTab.mockReturnValue(null) mocks.deliverLaunchPromptToAgentTab.mockResolvedValue(true) - mocks.getRuntimeEnvironmentStatus.mockResolvedValue({ - runtimeId: 'runtime-1', - capabilities: [BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY] - }) }) afterEach(() => { @@ -346,8 +345,7 @@ describe('createWebRuntimeSessionBrowserTab', () => { // Why: a user-initiated "New Browser Tab" focuses the new tab, which on a // headless host marks it active in the session snapshot. activate: true, - waitForRegistration: false, - requestedPageId: expect.any(String) + waitForRegistration: false }, timeoutMs: 15_000 }) @@ -364,78 +362,11 @@ describe('createWebRuntimeSessionBrowserTab', () => { snapshot, ENVIRONMENT_ID ) - expect(mocks.getRuntimeEnvironmentStatus).toHaveBeenCalledWith(ENVIRONMENT_ID, 15_000) - expect(mocks.createBrowserTab).not.toHaveBeenCalled() - }) - - it('retries an ambiguous create with the same page identity on a capable host', async () => { - let createAttempts = 0 - const runtimeCall = vi.fn( - async (request: { method: string; params?: Record }) => { - if (request.method === 'browser.tabCreate' && createAttempts++ === 0) { - return { - id: 'create-lost', - ok: false, - error: { code: 'remote_runtime_unavailable', message: 'response lost' } - } - } - if (request.method === 'browser.tabCreate') { - return { - id: 'create-replayed', - ok: true, - result: { browserPageId: 'remote-browser-page-1' } - } - } - return { id: 'list', ok: true, result: makeSnapshot() } - } - ) - vi.stubGlobal('window', { api: { runtimeEnvironments: { call: runtimeCall } } }) - - await expect( - createWebRuntimeSessionBrowserTab({ worktreeId: WORKTREE_ID, url: 'https://example.com/' }) - ).resolves.toBe(true) - - const createCalls = runtimeCall.mock.calls.filter( - ([request]) => request.method === 'browser.tabCreate' - ) - expect(createCalls).toHaveLength(2) - expect(createCalls[1]?.[0].params?.requestedPageId).toBe( - createCalls[0]?.[0].params?.requestedPageId - ) - expect(mocks.createBrowserTab).not.toHaveBeenCalled() - }) - - it('creates a deterministic page in non-secure LAN contexts without randomUUID', async () => { - vi.stubGlobal('crypto', { - getRandomValues: (bytes: Uint8Array) => { - bytes.fill(7) - return bytes - } + expect(peekWebSessionFocusIntent({ environmentId: ENVIRONMENT_ID }, WORKTREE_ID)).toEqual({ + hostTabId: 'remote-browser-page-1', + expectedCurrentLocalTabId: 'local-editor-tab' }) - const runtimeCall = vi.fn( - async (request: { method: string; params?: { requestedPageId?: string } }) => { - if (request.method === 'browser.tabCreate') { - return { - id: 'create', - ok: true, - result: { browserPageId: request.params?.requestedPageId } - } - } - return { id: 'list', ok: true, result: makeSnapshot() } - } - ) - vi.stubGlobal('window', { api: { runtimeEnvironments: { call: runtimeCall } } }) - - await expect( - createWebRuntimeSessionBrowserTab({ worktreeId: WORKTREE_ID, url: 'https://example.com/' }) - ).resolves.toBe(true) - - const createRequest = runtimeCall.mock.calls.find( - ([request]) => request.method === 'browser.tabCreate' - ) - expect(createRequest?.[0].params?.requestedPageId).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ - ) + expect(mocks.createBrowserTab).not.toHaveBeenCalled() }) it('does not let a slow browser create replace a newer browser selection', async () => { @@ -477,11 +408,7 @@ describe('createWebRuntimeSessionBrowserTab', () => { }) }) - it('does not retry an ambiguous create on a host without deterministic identity', async () => { - mocks.getRuntimeEnvironmentStatus.mockResolvedValue({ - runtimeId: 'runtime-1', - capabilities: [] - }) + it('does not retry a failed browser create', async () => { const runtimeCall = vi.fn().mockResolvedValue({ id: 'create-lost', ok: false, @@ -497,58 +424,6 @@ describe('createWebRuntimeSessionBrowserTab', () => { expect(mocks.createBrowserTab).not.toHaveBeenCalled() }) - it('adopts the deterministic page after both create replies are lost', async () => { - let snapshotApplied = false - mocks.getState.mockImplementation(() => ({ - settings: { activeRuntimeEnvironmentId: ENVIRONMENT_ID }, - activeWorktreeId: WORKTREE_ID, - browserPagesByWorkspace: snapshotApplied - ? { 'browser-workspace': [{ id: 'local-page', workspaceId: 'browser-workspace' }] } - : {}, - remoteBrowserPageHandlesByPageId: snapshotApplied - ? { - 'local-page': { - environmentId: ENVIRONMENT_ID, - remotePageId: '07070707-0707-4707-8707-070707070707' - } - } - : {}, - closeEmptyGroup: mocks.closeEmptyGroup, - setActiveWorktree: mocks.setActiveWorktree - })) - vi.stubGlobal('crypto', { - getRandomValues: (bytes: Uint8Array) => { - bytes.fill(7) - return bytes - } - }) - let createAttempts = 0 - const runtimeCall = vi.fn(async (request: { method: string }) => { - if (request.method === 'browser.tabCreate') { - createAttempts += 1 - return { - id: `create-lost-${createAttempts}`, - ok: false, - error: { code: 'remote_runtime_unavailable', message: 'response lost' } - } - } - snapshotApplied = true - return { id: 'list', ok: true, result: makeSnapshot() } - }) - vi.stubGlobal('window', { api: { runtimeEnvironments: { call: runtimeCall } } }) - - await expect( - createWebRuntimeSessionBrowserTab({ - worktreeId: WORKTREE_ID, - clientTargetGroupId: 'preview-group', - clientTargetGroupCreated: true - }) - ).resolves.toBe(true) - - expect(createAttempts).toBe(2) - expect(mocks.closeEmptyGroup).not.toHaveBeenCalled() - }) - it('keeps the requested split reserved until a pre-published browser gains a unified tab', async () => { mocks.getState.mockReturnValue({ settings: { activeRuntimeEnvironmentId: ENVIRONMENT_ID }, diff --git a/src/renderer/src/runtime/web-runtime-session.ts b/src/renderer/src/runtime/web-runtime-session.ts index 3b14af35f6c..ee04e2ff7d7 100644 --- a/src/renderer/src/runtime/web-runtime-session.ts +++ b/src/renderer/src/runtime/web-runtime-session.ts @@ -18,11 +18,7 @@ import type { SleepingAgentLaunchConfig, AgentProviderSessionMetadata } from '../../../shared/agent-session-resume' -import { - AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY, - BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY -} from '../../../shared/protocol-version' -import { isRecoverableRemoteRuntimeConnectionError } from '../../../shared/remote-runtime-client-error-classification' +import { AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import type { AgentLaunchPreferences, AgentPromptDelivery, @@ -30,15 +26,10 @@ import type { RuntimeEnsureAgentSessionResult } from '../../../shared/agent-session-host-authority' import type { TerminalPaneLayoutNode, TuiAgent } from '../../../shared/types' -import type { AppState } from '../store/types' import { createBrowserUuid } from '../lib/browser-uuid' import { getRuntimeEnvironmentIdForWorktree } from '../lib/worktree-runtime-owner' import { useAppStore } from '../store' -import { - RuntimeRpcCallError, - getRuntimeEnvironmentStatus, - unwrapRuntimeRpcResult -} from './runtime-rpc-client' +import { unwrapRuntimeRpcResult } from './runtime-rpc-client' import { createAgentSessionCreateOperation, withAgentSessionCreateOperationId @@ -48,7 +39,8 @@ import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' import { clearWebSessionFocusIntentIfMatches, peekWebSessionFocusIntent, - recordWebSessionFocusIntent + recordWebSessionFocusIntent, + resolveWebSessionVisibleTabId } from './web-session-focus-intent' import { clearWebSessionCloseIntent, recordWebSessionCloseIntent } from './web-session-close-intent' import { @@ -459,12 +451,16 @@ export async function createWebRuntimeSessionBrowserTab(args: { const intentOwner = captureWebSessionIntentOwner(environmentId) const callEnvironment = captureRuntimeEnvironmentCall(environmentId, intentOwner.pairingRevision) const shouldSelectWorktree = args.selectWorktree !== false - const requestedPageId = createBrowserUuid() + const provisionalPageId = createBrowserUuid() + const expectedCurrentLocalTabId = resolveWebSessionVisibleTabId( + useAppStore.getState(), + args.worktreeId + ) if (args.clientTargetGroupId) { recordWebSessionBrowserPlacement({ environmentId, worktreeId: args.worktreeId, - remotePageId: requestedPageId, + remotePageId: provisionalPageId, groupId: args.clientTargetGroupId }) } @@ -472,74 +468,48 @@ export async function createWebRuntimeSessionBrowserTab(args: { selectWebRuntimeSessionBrowserWorktree(args.worktreeId, environmentId) } try { - const status = await getRuntimeEnvironmentStatus(environmentId, 15_000) - const supportsDeterministicCreate = - status.capabilities?.includes(BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY) === true if (matchesWebSessionIntentOwner(intentOwner)) { - recordWebSessionFocusIntent(intentOwner, args.worktreeId, requestedPageId) - } - const create = async (): Promise => - unwrapRuntimeRpcResult( - (await callEnvironment({ - method: 'browser.tabCreate', - params: { - worktree: toRuntimeWorktreeSelector(args.worktreeId), - url: args.url, - profileId: args.profileId ?? undefined, - // Why: user clicked "New Browser Tab", so mark it active in the snapshot, else the reconcile snaps back to a terminal. - activate: true, - // Why: place the new browser in the clicked split group so the host snapshot is authoritative for it (no left-snap). - ...(args.targetGroupId ? { targetGroupId: args.targetGroupId } : {}), - // Why: web clients need the local tab now; waiting for host webview registration makes the workspace appear to close. - waitForRegistration: false, - ...(supportsDeterministicCreate ? { requestedPageId } : {}) - }, - timeoutMs: 15_000 - })) as RuntimeRpcResponse + recordWebSessionFocusIntent( + intentOwner, + args.worktreeId, + provisionalPageId, + undefined, + expectedCurrentLocalTabId ) - let created: BrowserTabCreateResult - try { - created = await create() - } catch (error) { - const retryable = - error instanceof RuntimeRpcCallError && isRecoverableRemoteRuntimeConnectionError(error) - if (!retryable || !supportsDeterministicCreate) { - throw error - } - try { - created = await create() - } catch (retryError) { - const retryOutcomeUnknown = - retryError instanceof RuntimeRpcCallError && - isRecoverableRemoteRuntimeConnectionError(retryError) - if (!retryOutcomeUnknown) { - throw retryError - } - try { - await refreshWebRuntimeSessionTabsSnapshot(environmentId, args.worktreeId, { - expectedEnvironmentPairingRevision: intentOwner.pairingRevision - }) - } catch { - // Why: the deterministic page may already exist; its next snapshot adopts it without duplicating. - return true - } - if ( - hasLocalBrowserPageForRemotePage(useAppStore.getState(), environmentId, requestedPageId) - ) { - return true - } - throw retryError - } } - if (created.browserPageId !== requestedPageId) { + const created = unwrapRuntimeRpcResult( + (await callEnvironment({ + method: 'browser.tabCreate', + params: { + worktree: toRuntimeWorktreeSelector(args.worktreeId), + url: args.url, + profileId: args.profileId ?? undefined, + // Why: user clicked "New Browser Tab", so mark it active in the snapshot, else the reconcile snaps back to a terminal. + activate: true, + // Why: place the new browser in the clicked split group so the host snapshot is authoritative for it (no left-snap). + ...(args.targetGroupId ? { targetGroupId: args.targetGroupId } : {}), + // Why: web clients need the local tab now; waiting for host webview registration makes the workspace appear to close. + waitForRegistration: false + }, + timeoutMs: 15_000 + })) as RuntimeRpcResponse + ) + if (created.browserPageId !== provisionalPageId) { moveWebSessionBrowserPlacement({ environmentId, worktreeId: args.worktreeId, - fromRemotePageId: requestedPageId, + fromRemotePageId: provisionalPageId, toRemotePageId: created.browserPageId }) - if (peekWebSessionFocusIntent(intentOwner, args.worktreeId)?.hostTabId === requestedPageId) { - recordWebSessionFocusIntent(intentOwner, args.worktreeId, created.browserPageId) + const focusIntent = peekWebSessionFocusIntent(intentOwner, args.worktreeId) + if (focusIntent?.hostTabId === provisionalPageId) { + recordWebSessionFocusIntent( + intentOwner, + args.worktreeId, + created.browserPageId, + undefined, + focusIntent.expectedCurrentLocalTabId + ) } } try { @@ -557,9 +527,9 @@ export async function createWebRuntimeSessionBrowserTab(args: { forgetWebSessionBrowserPlacement({ environmentId, worktreeId: args.worktreeId, - remotePageId: requestedPageId + remotePageId: provisionalPageId }) - clearWebSessionFocusIntentIfMatches(intentOwner, args.worktreeId, requestedPageId) + clearWebSessionFocusIntentIfMatches(intentOwner, args.worktreeId, provisionalPageId) if (args.clientTargetGroupId && args.clientTargetGroupCreated) { const reserved = isWebSessionBrowserPlacementGroupReserved({ environmentId, @@ -596,22 +566,6 @@ function selectWebRuntimeSessionBrowserWorktree(worktreeId: string, environmentI } } -function hasLocalBrowserPageForRemotePage( - state: AppState, - environmentId: string, - remotePageId: string -): boolean { - for (const pages of Object.values(state.browserPagesByWorkspace)) { - for (const page of pages) { - const handle = state.remoteBrowserPageHandlesByPageId[page.id] - if (handle?.environmentId === environmentId && handle.remotePageId === remotePageId) { - return true - } - } - } - return false -} - export async function refreshWebRuntimeSessionTabsSnapshot( environmentId: string, worktreeId: string, diff --git a/src/renderer/src/runtime/web-session-focus-intent.ts b/src/renderer/src/runtime/web-session-focus-intent.ts index e4d94e12f0f..e185250372f 100644 --- a/src/renderer/src/runtime/web-session-focus-intent.ts +++ b/src/renderer/src/runtime/web-session-focus-intent.ts @@ -10,14 +10,50 @@ // snapshots, unlike a transient per-snapshot flag). import { webSessionIntentOwnerKey, type WebSessionIntentOwner } from './web-session-intent-owner' +import type { AppState } from '../store/types' export type WebSessionFocusIntent = { hostTabId: string leafId?: string + expectedCurrentLocalTabId?: string | null } const pendingFocusByOwnerAndWorktree = new Map() +type WebSessionVisibleTabState = Pick< + AppState, + | 'activeBrowserTabIdByWorktree' + | 'activeFileIdByWorktree' + | 'activeTabIdByWorktree' + | 'activeTabType' + | 'activeTabTypeByWorktree' + | 'activeWorktreeId' + | 'unifiedTabsByWorktree' +> + +export function resolveWebSessionVisibleTabId( + state: WebSessionVisibleTabState, + worktreeId: string, + tabs = state.unifiedTabsByWorktree?.[worktreeId] ?? [] +): string | null { + const currentType = + state.activeTabTypeByWorktree?.[worktreeId] ?? + (state.activeWorktreeId === worktreeId ? state.activeTabType : null) + if (currentType === 'terminal') { + const tabId = state.activeTabIdByWorktree?.[worktreeId] + return tabId && tabs.some((tab) => tab.id === tabId) ? tabId : null + } + const entityId = + currentType === 'browser' + ? state.activeBrowserTabIdByWorktree?.[worktreeId] + : currentType === 'editor' + ? state.activeFileIdByWorktree?.[worktreeId] + : null + return ( + tabs.find((tab) => tab.contentType === currentType && tab.entityId === entityId)?.id ?? null + ) +} + function focusIntentPartitionKey(owner: WebSessionIntentOwner, worktreeId: string): string { return `${webSessionIntentOwnerKey(owner)}\0${worktreeId}` } @@ -26,7 +62,8 @@ export function recordWebSessionFocusIntent( owner: WebSessionIntentOwner, worktreeId: string, hostTabId: string, - leafId?: string + leafId?: string, + expectedCurrentLocalTabId?: string | null ): void { const trimmed = hostTabId.trim() if (!worktreeId || !trimmed) { @@ -35,7 +72,8 @@ export function recordWebSessionFocusIntent( const trimmedLeafId = leafId?.trim() pendingFocusByOwnerAndWorktree.set(focusIntentPartitionKey(owner, worktreeId), { hostTabId: trimmed, - ...(trimmedLeafId ? { leafId: trimmedLeafId } : {}) + ...(trimmedLeafId ? { leafId: trimmedLeafId } : {}), + ...(expectedCurrentLocalTabId !== undefined ? { expectedCurrentLocalTabId } : {}) }) } diff --git a/src/renderer/src/runtime/web-session-tabs-sync.test.ts b/src/renderer/src/runtime/web-session-tabs-sync.test.ts index 42db0b23415..ba8e00758f8 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.test.ts @@ -3380,7 +3380,13 @@ describe('applyWebSessionTabsSnapshot', () => { isPreview: false, isPinned: false } - recordWebSessionFocusIntent({ environmentId: ENV }, WT, 'host-browser') + recordWebSessionFocusIntent( + { environmentId: ENV }, + WT, + 'host-browser', + undefined, + 'previous-local-tab' + ) const patch = applyWebSessionTabsSnapshot( makeState({ @@ -3467,7 +3473,13 @@ describe('applyWebSessionTabsSnapshot', () => { isPreview: false, isPinned: false } - recordWebSessionFocusIntent({ environmentId: ENV }, WT, 'host-browser') + recordWebSessionFocusIntent( + { environmentId: ENV }, + WT, + 'host-browser', + undefined, + 'previous-local-tab' + ) const patch = applyWebSessionTabsSnapshot( makeState({ diff --git a/src/renderer/src/runtime/web-session-tabs-sync.ts b/src/renderer/src/runtime/web-session-tabs-sync.ts index f4cf5f809f0..d0ce2e2c6d5 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.ts @@ -54,7 +54,8 @@ import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' import { clearWebSessionFocusIntent, clearWebSessionFocusIntentsForOwner, - peekWebSessionFocusIntent + peekWebSessionFocusIntent, + resolveWebSessionVisibleTabId } from './web-session-focus-intent' import { clearWebSessionCloseIntentsForOwner, @@ -2310,52 +2311,6 @@ function toVisibleTabType(tab: Tab): WebSessionTabsSyncState['activeTabType'] { return 'editor' } -function runtimeTabMatchesVisibleType( - tab: RuntimeMobileSessionTabsResult['tabs'][number], - visibleType: WebSessionTabsSyncState['activeTabType'] | null -): boolean { - if (tab.type === 'browser' || tab.type === 'terminal') { - return tab.type === visibleType - } - return visibleType === 'editor' -} - -function findCurrentVisibleUnifiedTabId(args: { - state: WebSessionTabsSyncState - worktreeId: string - nextUnifiedTabs: readonly Tab[] | null -}): string | null { - const { state, worktreeId, nextUnifiedTabs } = args - if (!nextUnifiedTabs) { - return null - } - const currentVisibleType = - state.activeTabTypeByWorktree[worktreeId] ?? - (state.activeWorktreeId === worktreeId ? state.activeTabType : null) - if (currentVisibleType === 'terminal') { - const terminalTabId = state.activeTabIdByWorktree[worktreeId] - return terminalTabId && nextUnifiedTabs.some((tab) => tab.id === terminalTabId) - ? terminalTabId - : null - } - if (currentVisibleType === 'browser') { - const browserWorkspaceId = state.activeBrowserTabIdByWorktree[worktreeId] - return ( - nextUnifiedTabs.find( - (tab) => tab.contentType === 'browser' && tab.entityId === browserWorkspaceId - )?.id ?? null - ) - } - if (currentVisibleType === 'editor') { - const fileId = state.activeFileIdByWorktree[worktreeId] - return ( - nextUnifiedTabs.find((tab) => tab.contentType === 'editor' && tab.entityId === fileId)?.id ?? - null - ) - } - return null -} - function applyWebSessionTabsSnapshotWithContext( state: WebSessionTabsSyncState, rawSnapshot: RuntimeMobileSessionTabsResult, @@ -2411,12 +2366,12 @@ function applyWebSessionTabsSnapshotWithContext( (tab.type === 'terminal' && tab.parentTabId === focusIntentHostTabId) || (tab.type === 'browser' && tab.browserPageId === focusIntentHostTabId) ) ?? null) - const currentVisibleType = - state.activeTabTypeByWorktree[worktreeId] ?? - (state.activeWorktreeId === worktreeId ? state.activeTabType : null) + const expectedCurrentLocalTabId = focusIntent?.expectedCurrentLocalTabId + const currentVisibleLocalTabId = resolveWebSessionVisibleTabId(state, worktreeId) const callerFocusIntentTab = matchingFocusIntentTab && - runtimeTabMatchesVisibleType(matchingFocusIntentTab, currentVisibleType) + (expectedCurrentLocalTabId === undefined || + expectedCurrentLocalTabId === currentVisibleLocalTabId) ? matchingFocusIntentTab : null const followIntentTab = @@ -2743,11 +2698,11 @@ function applyWebSessionTabsSnapshotWithContext( ? (activeMirroredEditorFileId ?? mirroredEditorTabs[0]?.file.id) : mirroredEditorTabs[0]?.file.id) ?? null - const currentVisibleUnifiedTabId = findCurrentVisibleUnifiedTabId({ + const currentVisibleUnifiedTabId = resolveWebSessionVisibleTabId( state, worktreeId, - nextUnifiedTabs - }) + nextUnifiedTabs ?? [] + ) // Why: a client-initiated activation also drives the visible unified tab, overriding the sticky current-visible tab. const intentUnifiedTabId = honorSnapshotActiveFocus ? navigationIntentTab?.type === 'browser' diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 5ad0a774067..900f6541e49 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -55,8 +55,6 @@ export const AI_VAULT_SESSION_TITLES_RUNTIME_CAPABILITY = 'aiVault.session-title // clients never fall back to a local desktop browser tab for a remote-owned page. export const BROWSER_HEADLESS_RUNTIME_CAPABILITY = 'browser.headless.v1' as const export const BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY = 'browser.certificate-trust.v1' as const -export const BROWSER_DETERMINISTIC_PAGE_CREATE_RUNTIME_CAPABILITY = - 'browser.deterministic-page-create.v1' as const // Why: hosts without this strip terminal.send's inputKind (zod object drops // unknown keys), so a mobile xterm query reply would land as ordinary // floor-taking input. Mobile must not forward replies unless advertised.