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.
This commit is contained in:
Jinjing
2026-08-12 22:06:40 -07:00
parent 9c7bcffe2a
commit 3aed34a60a
4 changed files with 102 additions and 6 deletions
@@ -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({
@@ -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<string, unknown>
}))
@@ -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<string, unknown> {
@@ -55,6 +61,7 @@ function browserCapableRuntime(environmentId: string): Record<string, unknown> {
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()
})
@@ -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<typeof resolveWorktreeOperationRoute>,
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')
}
@@ -325,8 +325,7 @@ export async function assertRuntimeEnvironmentCapability(
message: string,
timeoutMs?: number
): Promise<void> {
const status = await getRuntimeEnvironmentStatus(environmentId, timeoutMs)
if (!status.capabilities?.includes(capability)) {
if (!(await runtimeEnvironmentSupportsCapability(environmentId, capability, timeoutMs))) {
throw new Error(message)
}
}