From 3aed34a60ab4bc64a734783a144f45de1beda6a6 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:06:40 -0700 Subject: [PATCH] fix(browser): reuse capability verdict and exclude floating terminals Cache the runtime browser capability check across multiple remote port opens instead of checking for each port independently. Prevent floating-terminal workspaces from routing to remote browsers. --- .../right-sidebar/PortsPanel.test.tsx | 12 +++- .../lib/workspace-browser-tab-open.test.ts | 71 +++++++++++++++++++ .../src/lib/workspace-browser-tab-open.ts | 22 +++++- .../src/runtime/runtime-rpc-client.ts | 3 +- 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx b/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx index 1943532cb52..9cb86404dd8 100644 --- a/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx @@ -319,7 +319,7 @@ describe('PortsPanel runtime routing', () => { ).toEqual(['runtime-repo::/srv/app', 'repo::/workspace/app']) }) - it('opens remote workspace ports in the server-side browser and binds the local page handle', async () => { + it('reuses the capability verdict across remote port opens', async () => { runtimeEnvironmentCall.mockImplementation(({ method }: { method: string }) => Promise.resolve({ id: method, @@ -332,6 +332,14 @@ describe('PortsPanel runtime routing', () => { const createBrowserTab = vi.fn(() => ({ activePageId: 'local-page-1' })) const setRemoteBrowserPageHandle = vi.fn() + await expect( + openWorkspacePortInBrowser({ + port: workspacePort, + runtimeTarget: { kind: 'environment', environmentId: 'env-1' }, + createBrowserTab: createBrowserTab as never, + setRemoteBrowserPageHandle: setRemoteBrowserPageHandle as never + }) + ).resolves.toEqual({ ok: true }) await expect( openWorkspacePortInBrowser({ port: workspacePort, @@ -341,9 +349,11 @@ describe('PortsPanel runtime routing', () => { }) ).resolves.toEqual({ ok: true }) + expect(activateAndRevealWorktreeMock).toHaveBeenCalledTimes(2) expect(activateAndRevealWorktreeMock).toHaveBeenCalledWith('repo::/workspace/app') expect(runtimeEnvironmentCall.mock.calls.map((call) => call[0].method)).toEqual([ 'status.get', + 'browser.tabCreate', 'browser.tabCreate' ]) expect(runtimeEnvironmentCall.mock.calls[1][0].params).toEqual({ diff --git a/src/renderer/src/lib/workspace-browser-tab-open.test.ts b/src/renderer/src/lib/workspace-browser-tab-open.test.ts index 765f452f8f7..05fd79eefb3 100644 --- a/src/renderer/src/lib/workspace-browser-tab-open.test.ts +++ b/src/renderer/src/lib/workspace-browser-tab-open.test.ts @@ -4,6 +4,7 @@ import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { BROWSER_SCREENCAST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import { canOpenWorkspaceBrowserTabOnRuntime, @@ -13,6 +14,7 @@ import { const mocks = vi.hoisted(() => ({ createRemote: vi.fn(), getState: vi.fn(), + pairedWeb: false, state: {} as Record })) @@ -24,6 +26,10 @@ vi.mock('@/runtime/web-runtime-session', () => ({ createWebRuntimeSessionBrowserTab: (...args: unknown[]) => mocks.createRemote(...args) })) +vi.mock('./desktop-window-chrome', () => ({ + isPairedWebClientWindow: () => mocks.pairedWeb +})) + const WORKSPACE_ID = 'repo-1::/repo/worktree' function ownerState(hostId?: string, runtimeOwnerEnvironmentId?: string): Record { @@ -55,6 +61,7 @@ function browserCapableRuntime(environmentId: string): Record { beforeEach(() => { mocks.createRemote.mockReset().mockResolvedValue(true) mocks.getState.mockReset().mockImplementation(() => mocks.state) + mocks.pairedWeb = false mocks.state = {} }) @@ -186,6 +193,70 @@ describe('openWorkspaceBrowserTab', () => { expectedRuntimeEnvironmentId: 'hub-a' }) ).rejects.toThrow('Unable to open URL.') + expect(mocks.createRemote).toHaveBeenCalledWith( + expect.objectContaining({ environmentId: 'hub-a' }) + ) + expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() + }) + + it('does not route an asserted floating-terminal source to either runtime or local browser', async () => { + const createBrowserTab = vi.fn() + mocks.state = { + activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID, + activeWorkspaceExecutionHostId: toRuntimeExecutionHostId('hub-a'), + ...browserCapableRuntime('hub-a'), + createBrowserTab, + defaultBrowserSessionProfileId: 'client-profile', + defaultBrowserSessionProfileIdByHostId: {} + } + + expect( + canOpenWorkspaceBrowserTabOnRuntime( + mocks.state as never, + FLOATING_TERMINAL_WORKTREE_ID, + 'hub-a' + ) + ).toBe(false) + + await expect( + openWorkspaceBrowserTab({ + workspaceId: FLOATING_TERMINAL_WORKTREE_ID, + url: 'http://localhost:3000/', + intent: { kind: 'url' }, + expectedRuntimeEnvironmentId: 'hub-a' + }) + ).rejects.toThrow('Unable to open URL.') + expect(mocks.createRemote).not.toHaveBeenCalled() + expect(createBrowserTab).not.toHaveBeenCalled() + }) + + it('keeps runtime browser actions unavailable in a paired-web floating workspace', async () => { + mocks.pairedWeb = true + mocks.state = { + activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID, + activeWorkspaceExecutionHostId: toRuntimeExecutionHostId('hub-a'), + ...browserCapableRuntime('hub-a'), + createBrowserTab: vi.fn(), + defaultBrowserSessionProfileId: 'client-profile', + defaultBrowserSessionProfileIdByHostId: {} + } + + expect( + canOpenWorkspaceBrowserTabOnRuntime( + mocks.state as never, + FLOATING_TERMINAL_WORKTREE_ID, + 'hub-a' + ) + ).toBe(false) + await expect( + openWorkspaceBrowserTab({ + workspaceId: FLOATING_TERMINAL_WORKTREE_ID, + url: 'http://localhost:3000/', + intent: { kind: 'url' }, + expectedRuntimeEnvironmentId: 'hub-a' + }) + ).rejects.toThrow('Unable to open URL.') + expect(mocks.createRemote).not.toHaveBeenCalled() expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/lib/workspace-browser-tab-open.ts b/src/renderer/src/lib/workspace-browser-tab-open.ts index 144b9cd60f6..a25f9cd77ca 100644 --- a/src/renderer/src/lib/workspace-browser-tab-open.ts +++ b/src/renderer/src/lib/workspace-browser-tab-open.ts @@ -8,6 +8,7 @@ import { type ExecutionHostId } from '../../../shared/execution-host' import { SEARCH_ENGINE_LABELS, type SearchEngine } from '../../../shared/browser-url' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { BROWSER_SCREENCAST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import { getClientCreationActionPolicy, @@ -27,10 +28,12 @@ export type OpenWorkspaceBrowserTabRequest = { function isExpectedRuntimeBrowserRoute( state: AppState, + availability: ClientCreationActionAvailability, route: ReturnType, + workspaceId: string, expectedRuntimeEnvironmentId: string ): boolean { - if (!route) { + if (availability.state !== 'enabled' || workspaceId === FLOATING_TERMINAL_WORKTREE_ID || !route) { return false } const expectedEnvironmentId = expectedRuntimeEnvironmentId.trim() @@ -55,8 +58,15 @@ export function canOpenWorkspaceBrowserTabOnRuntime( workspaceId: string, expectedRuntimeEnvironmentId: string ): boolean { + const availability = getClientCreationActionPolicy(state, workspaceId)['managed-browser'] const route = resolveWorktreeOperationRoute(state, workspaceId) - return isExpectedRuntimeBrowserRoute(state, route, expectedRuntimeEnvironmentId) + return isExpectedRuntimeBrowserRoute( + state, + availability, + route, + workspaceId, + expectedRuntimeEnvironmentId + ) } // Why: concurrent URL tabs are indistinguishable under a shared "Open URL" @@ -172,7 +182,13 @@ export async function openWorkspaceBrowserTab( const expectedEnvironmentId = request.expectedRuntimeEnvironmentId?.trim() || null if ( expectedEnvironmentId && - !isExpectedRuntimeBrowserRoute(state, route, expectedEnvironmentId) + !isExpectedRuntimeBrowserRoute( + state, + availability, + route, + request.workspaceId, + expectedEnvironmentId + ) ) { throw openFailure(presentation.error, 'asserted runtime cannot provide this managed browser') } diff --git a/src/renderer/src/runtime/runtime-rpc-client.ts b/src/renderer/src/runtime/runtime-rpc-client.ts index 57ba17b75a0..7819314e5b0 100644 --- a/src/renderer/src/runtime/runtime-rpc-client.ts +++ b/src/renderer/src/runtime/runtime-rpc-client.ts @@ -325,8 +325,7 @@ export async function assertRuntimeEnvironmentCapability( message: string, timeoutMs?: number ): Promise { - const status = await getRuntimeEnvironmentStatus(environmentId, timeoutMs) - if (!status.capabilities?.includes(capability)) { + if (!(await runtimeEnvironmentSupportsCapability(environmentId, capability, timeoutMs))) { throw new Error(message) } }