STA-5496 offer Orca Browser for SSH terminal links (#16503)

Reuse direct SSH browser-route eligibility and route owner-pinned terminal links through the workspace createBrowserTab path. Keep the popover and modifier hints aligned with the eligible destination.

Preserve printed 0.0.0.0 and localhost URLs; the existing SSH SOCKS boundary normalizes wildcard listeners to remote loopback immediately before connect.
This commit is contained in:
Jinwoo Hong
2026-08-30 02:37:40 -04:00
committed by GitHub
parent b261f4005c
commit 59278570db
16 changed files with 298 additions and 99 deletions
@@ -10,13 +10,12 @@ import {
BROWSER_SSH_WORKSPACE_ROUTING_SETTINGS_TARGET_ID
} from '@/lib/settings-navigation-types'
import {
isRuntimeOwnedSshTargetId,
parseExecutionHostId,
toRuntimeExecutionHostId,
toSshExecutionHostId
} from '../../../../../shared/execution-host'
import { getHostSettingOverride } from '../../../../../shared/host-setting-overrides'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import { resolveSshWorkspaceBrowserRouteEligibility } from '@/lib/ssh-workspace-browser-route-eligibility'
/**
* The address bar's leading icon, egress-aware: pages that render locally and
@@ -103,19 +102,13 @@ export function SshEgressIndicator({
worktreeId: string
}): React.JSX.Element | null {
const executionHostId = useAppStore((s) => getExecutionHostIdForWorktree(s, worktreeId))
const routingEnabled = useAppStore((s) => s.settings?.browserSshWorkspaceRoutingEnabled !== false)
const disabledTargetIds = useAppStore(
(s) => s.settings?.browserSshWorkspaceRoutingDisabledTargetIds
)
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
const settings = useAppStore((s) => s.settings)
const parsed = parseExecutionHostId(executionHostId)
const targetId =
parsed?.kind === 'ssh' && !isRuntimeOwnedSshTargetId(parsed.targetId) ? parsed.targetId : null
if (!targetId) {
const routeEligibility = resolveSshWorkspaceBrowserRouteEligibility(executionHostId, settings)
if (!routeEligibility) {
return <Globe className="size-4 shrink-0 text-muted-foreground" />
}
const routed = routingEnabled && !disabledTargetIds?.includes(targetId)
const { targetId, eligible: routed } = routeEligibility
const hostLabel =
getHostSettingOverride(settings, toSshExecutionHostId(targetId), 'displayLabel') ??
sshTargetLabels.get(targetId) ??
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useAppStore } from '@/store'
import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../../shared/execution-host'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import { resolveSshWorkspaceBrowserRouteEligibility } from '@/lib/ssh-workspace-browser-route-eligibility'
export type SshWorkspaceBrowserRouteErrorKind = 'forwarding-blocked' | 'ssh-unavailable' | 'unknown'
@@ -45,20 +45,17 @@ export function useSshWorkspaceBrowserRoute(
browseFromThisDevice: () => void
} {
const executionHostId = useAppStore((s) => getExecutionHostIdForWorktree(s, worktreeId))
const routingEnabled = useAppStore((s) => s.settings?.browserSshWorkspaceRoutingEnabled !== false)
const disabledTargetIds = useAppStore(
(s) => s.settings?.browserSshWorkspaceRoutingDisabledTargetIds
)
const browserRoutingSettings = useAppStore((s) => s.settings)
const probeSkippedTargetIds = useAppStore(
(s) => s.settings?.browserSshWorkspaceRoutingProbeSkippedTargetIds
)
const updateSettings = useAppStore((s) => s.updateSettings)
const parsed = parseExecutionHostId(executionHostId)
// Why: runtime-owned ephemeral targets belong to a paired runtime's own machinery.
const sshTargetId =
parsed?.kind === 'ssh' && !isRuntimeOwnedSshTargetId(parsed.targetId) ? parsed.targetId : null
const targetId =
routingEnabled && sshTargetId && !disabledTargetIds?.includes(sshTargetId) ? sshTargetId : null
const routeEligibility = resolveSshWorkspaceBrowserRouteEligibility(
executionHostId,
browserRoutingSettings
)
const sshTargetId = routeEligibility?.targetId ?? null
const targetId = routeEligibility?.eligible === true ? routeEligibility.targetId : null
const browserProfileId = sessionProfileId ?? 'default'
const [attempt, setAttempt] = useState<{ count: number; skipProbe: boolean }>({
count: 0,
@@ -135,7 +132,7 @@ export function useSshWorkspaceBrowserRoute(
if (!sshTargetId) {
return
}
const disabled = disabledTargetIds ?? []
const disabled = browserRoutingSettings?.browserSshWorkspaceRoutingDisabledTargetIds ?? []
if (!disabled.includes(sshTargetId)) {
updateSettings({
browserSshWorkspaceRoutingDisabledTargetIds: [...disabled, sshTargetId]
@@ -153,13 +150,14 @@ export function useSshWorkspaceBrowserRoute(
*/
export function useSshWorkspaceProbeSkipRecheck(worktreeId: string): (() => void) | null {
const executionHostId = useAppStore((s) => getExecutionHostIdForWorktree(s, worktreeId))
const browserRoutingSettings = useAppStore((s) => s.settings)
const probeSkippedTargetIds = useAppStore(
(s) => s.settings?.browserSshWorkspaceRoutingProbeSkippedTargetIds
)
const updateSettings = useAppStore((s) => s.updateSettings)
const parsed = parseExecutionHostId(executionHostId)
const targetId =
parsed?.kind === 'ssh' && !isRuntimeOwnedSshTargetId(parsed.targetId) ? parsed.targetId : null
resolveSshWorkspaceBrowserRouteEligibility(executionHostId, browserRoutingSettings)?.targetId ??
null
if (!targetId || probeSkippedTargetIds?.includes(targetId) !== true) {
return null
}
@@ -9,7 +9,7 @@ const { openHttpLinkMock } = vi.hoisted(() => ({ openHttpLinkMock: vi.fn() }))
vi.mock('@/lib/http-link-routing', () => ({
openHttpLink: openHttpLinkMock,
registerHttpLinkStoreAccessor: vi.fn(),
registerRuntimeHttpLinkBrowserOpener: vi.fn()
registerWorkspaceHttpLinkBrowserOpener: vi.fn()
}))
function makeReview(overrides: Partial<HostedReviewInfo> = {}): HostedReviewInfo {
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
registerHttpLinkStoreAccessor,
registerRuntimeHttpLinkBrowserOpener
registerWorkspaceHttpLinkBrowserOpener
} from '@/lib/http-link-routing'
import {
closeTerminalLinkActionRequest,
@@ -48,11 +48,11 @@ beforeEach(() => {
setActiveWorktree,
createBrowserTab
}))
registerRuntimeHttpLinkBrowserOpener(openRuntimeBrowserTab)
registerWorkspaceHttpLinkBrowserOpener(openRuntimeBrowserTab)
})
afterEach(() => {
registerRuntimeHttpLinkBrowserOpener(null)
registerWorkspaceHttpLinkBrowserOpener(null)
vi.clearAllMocks()
vi.unstubAllGlobals()
})
@@ -209,6 +209,28 @@ describe('terminal link action routing', () => {
expect(createBrowserTab).not.toHaveBeenCalled()
})
it('routes an explicit Orca Browser action through the owning SSH workspace', () => {
const request = vi.fn()
const url = 'http://0.0.0.0:8000/'
handleTerminalHttpLink(url, plainEvent(), {
worktreeId: 'wt-1',
sourceOwner: { kind: 'ssh', connectionId: 'ssh-1' },
linkActionContext: actionContext(request),
actionDestinations: { primary: 'system', alternate: 'orca' }
})
request.mock.calls[0][0].alternate.run()
expect(openRuntimeBrowserTab).toHaveBeenCalledWith({
workspaceId: 'wt-1',
url,
intent: { kind: 'url' },
expectedSshConnectionId: 'ssh-1'
})
expect(createBrowserTab).not.toHaveBeenCalled()
expect(openUrl).not.toHaveBeenCalled()
})
it('uses Shift+modifier for the alternate local destination', () => {
const event = { ...plainEvent(), metaKey: true, shiftKey: true }
@@ -155,25 +155,47 @@ describe('terminalUrlOpenHintOptionsFor', () => {
expect(options.modifierInverts).toBe(true)
})
it('keeps inversion for an eligible SSH pane', () => {
const options = terminalUrlOpenHintOptionsFor(
{
openLinksInApp: false,
openLinksInAppModifierInverts: true
},
{ kind: 'ssh', connectionId: 'ssh-1' },
true
)
expect(options.modifierInverts).toBe(true)
})
})
describe('terminalHttpLinkActionDestinationsFor', () => {
it('offers both destinations for a capable runtime and follows the preference', () => {
const owner = { kind: 'runtime', runtimeEnvironmentId: 'env-1' } as const
expect(terminalHttpLinkActionDestinationsFor({ openLinksInApp: true }, owner, true)).toEqual({
primary: 'orca',
alternate: 'system'
})
expect(terminalHttpLinkActionDestinationsFor({ openLinksInApp: false }, owner, true)).toEqual({
primary: 'system',
alternate: 'orca'
})
})
it.each([
['local', { kind: 'local' } as const, false],
['capable runtime', { kind: 'runtime', runtimeEnvironmentId: 'env-1' } as const, true],
['eligible SSH', { kind: 'ssh', connectionId: 'ssh-1' } as const, true]
])(
'offers both destinations for a %s owner and follows the preference',
(_label, owner, canOpen) => {
expect(
terminalHttpLinkActionDestinationsFor({ openLinksInApp: true }, owner, canOpen)
).toEqual({
primary: 'orca',
alternate: 'system'
})
expect(
terminalHttpLinkActionDestinationsFor({ openLinksInApp: false }, owner, canOpen)
).toEqual({
primary: 'system',
alternate: 'orca'
})
}
)
it.each([
['incapable runtime', { kind: 'runtime', runtimeEnvironmentId: 'env-1' } as const],
['SSH', { kind: 'ssh', connectionId: 'ssh-1' } as const],
['ineligible SSH', { kind: 'ssh', connectionId: 'ssh-1' } as const],
['unknown owner', { kind: 'unknown' } as const]
])('offers only the system browser for an %s', (_label, owner) => {
expect(terminalHttpLinkActionDestinationsFor({ openLinksInApp: true }, owner, false)).toEqual({
@@ -37,13 +37,22 @@ export type TerminalUrlOpenHintOptions = {
showActions?: boolean
}
function canSourceOwnerOpenInOrca(
sourceOwner: HttpLinkSourceOwner,
canOpenOwnedBrowser: boolean
): boolean {
return (
sourceOwner.kind === 'local' ||
((sourceOwner.kind === 'runtime' || sourceOwner.kind === 'ssh') && canOpenOwnedBrowser)
)
}
export function terminalHttpLinkActionDestinationsFor(
settings: { openLinksInApp?: boolean } | null | undefined,
sourceOwner: HttpLinkSourceOwner,
canOpenRuntimeBrowser: boolean
canOpenOwnedBrowser: boolean
): TerminalHttpLinkActionDestinations {
const canOpenInOrca =
sourceOwner.kind === 'local' || (sourceOwner.kind === 'runtime' && canOpenRuntimeBrowser)
const canOpenInOrca = canSourceOwnerOpenInOrca(sourceOwner, canOpenOwnedBrowser)
if (!canOpenInOrca) {
return { primary: 'system' }
}
@@ -52,7 +61,7 @@ export function terminalHttpLinkActionDestinationsFor(
: { primary: 'system', alternate: 'orca' }
}
// Why: only a capability-verified runtime can advertise the in-app destination.
// Why: remote owners advertise Orca only when their existing browser route is eligible.
export function terminalUrlOpenHintOptionsFor(
settings:
| {
@@ -63,10 +72,10 @@ export function terminalUrlOpenHintOptionsFor(
| null
| undefined,
sourceOwner?: HttpLinkSourceOwner,
canOpenRuntimeBrowser = false
canOpenOwnedBrowser = false
): TerminalUrlOpenHintOptions {
const sourceCanOpenInOrca = sourceOwner
? sourceOwner.kind === 'local' || (sourceOwner.kind === 'runtime' && canOpenRuntimeBrowser)
? canSourceOwnerOpenInOrca(sourceOwner, canOpenOwnedBrowser)
: !settings?.activeRuntimeEnvironmentId?.trim()
return {
openLinksInApp: settings?.openLinksInApp === true,
@@ -2,7 +2,7 @@ import type { IBufferLine, Terminal } from '@xterm/xterm'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
registerHttpLinkStoreAccessor,
registerRuntimeHttpLinkBrowserOpener
registerWorkspaceHttpLinkBrowserOpener
} from '@/lib/http-link-routing'
import { handleOscLink } from './terminal-osc-link-routing'
import { handleTerminalWebLinkClick } from './terminal-web-link-click'
@@ -108,11 +108,11 @@ beforeEach(() => {
setActiveWorktree: setActiveWorktreeMock,
createBrowserTab: createBrowserTabMock
}))
registerRuntimeHttpLinkBrowserOpener(openRuntimeBrowserTabMock)
registerWorkspaceHttpLinkBrowserOpener(openRuntimeBrowserTabMock)
})
afterEach(() => {
registerRuntimeHttpLinkBrowserOpener(null)
registerWorkspaceHttpLinkBrowserOpener(null)
vi.unstubAllGlobals()
})
@@ -190,17 +190,23 @@ describe('terminal HTTP links on a runtime-hosted pane', () => {
describe('terminal HTTP links on a direct SSH pane', () => {
const baseDeps = { worktreeId: 'wt-1', worktreePath: '/tmp', startupCwd: '/tmp' }
it('sends an OSC 8 hyperlink to the system browser', () => {
it('opens an OSC 8 hyperlink through the owning SSH workspace', () => {
expect(handleOscLink(URL, clickEvent(), { ...baseDeps, sourceOwner: sshSourceOwner })).toBe(
true
)
expect(openUrlMock).toHaveBeenCalledWith(URL)
expect(openRuntimeBrowserTabMock).toHaveBeenCalledWith({
workspaceId: 'wt-1',
url: URL,
intent: { kind: 'url' },
expectedSshConnectionId: 'ssh-1'
})
expect(openUrlMock).not.toHaveBeenCalled()
expect(createBrowserTabMock).not.toHaveBeenCalled()
expect(setActiveWorktreeMock).not.toHaveBeenCalled()
})
it('sends a WebLinksAddon click to the system browser', () => {
it('opens a WebLinksAddon click through the owning SSH workspace', () => {
const { terminal } = makeTerminal()
expect(
@@ -211,11 +217,14 @@ describe('terminal HTTP links on a direct SSH pane', () => {
})
).toBe(true)
expect(openUrlMock).toHaveBeenCalledWith(URL)
expect(openRuntimeBrowserTabMock).toHaveBeenCalledWith(
expect.objectContaining({ expectedSshConnectionId: 'ssh-1', url: URL })
)
expect(openUrlMock).not.toHaveBeenCalled()
expect(createBrowserTabMock).not.toHaveBeenCalled()
})
it('sends a click-fallback activation to the system browser', () => {
it('opens a click-fallback activation through the owning SSH workspace', () => {
const { terminal, registrations } = makeTerminal()
const disposable = installHttpLinkClickFallback(terminal, {
worktreeId: 'wt-1',
@@ -226,7 +235,10 @@ describe('terminal HTTP links on a direct SSH pane', () => {
([name, _listener, options]) => name === 'mouseup' && options === undefined
)?.[1](clickEvent())
expect(openUrlMock).toHaveBeenCalledWith(URL)
expect(openRuntimeBrowserTabMock).toHaveBeenCalledWith(
expect.objectContaining({ expectedSshConnectionId: 'ssh-1', url: URL })
)
expect(openUrlMock).not.toHaveBeenCalled()
expect(createBrowserTabMock).not.toHaveBeenCalled()
disposable.dispose()
})
@@ -277,7 +277,7 @@ export function openTerminalHttpLink(url: string, deps: UrlLinkHitTestDeps): voi
const sourceOwner = deps.sourceOwner ?? { kind: 'local' }
if (deps.forceDestination) {
openHttpLink(url, {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: deps.worktreeId,
forceInApp: deps.forceDestination === 'orca',
forceSystemBrowser: deps.forceDestination === 'system',
@@ -289,7 +289,7 @@ export function openTerminalHttpLink(url: string, deps: UrlLinkHitTestDeps): voi
// Why: the modifier states a destination outright, so it also skips the
// one-time routing prompt; openHttpLink resolves which destination it means.
openHttpLink(url, {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: deps.worktreeId,
modifierHeld: true,
sourceOwner
@@ -301,7 +301,7 @@ export function openTerminalHttpLink(url: string, deps: UrlLinkHitTestDeps): voi
const preferenceDecision =
sourceOwner.kind === 'local' ? deps.requestOpenLinksInAppPreference?.(url) : null
if (preferenceDecision === null || preferenceDecision === undefined) {
openHttpLink(url, { allowRuntimeInApp: true, worktreeId: deps.worktreeId, sourceOwner })
openHttpLink(url, { allowRemoteInApp: true, worktreeId: deps.worktreeId, sourceOwner })
return
}
@@ -311,7 +311,7 @@ export function openTerminalHttpLink(url: string, deps: UrlLinkHitTestDeps): voi
void Promise.resolve(preferenceDecision)
.then((openInOrca) => {
openHttpLink(url, {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: deps.worktreeId,
forceSystemBrowser: !openInOrca,
sourceOwner
@@ -319,7 +319,7 @@ export function openTerminalHttpLink(url: string, deps: UrlLinkHitTestDeps): voi
})
.catch(() => {
openHttpLink(url, {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: deps.worktreeId,
forceSystemBrowser: true,
sourceOwner
@@ -57,7 +57,10 @@ import {
type HttpLinkSourceOwner
} from '@/lib/http-link-routing'
import { resolveTerminalHttpLinkSourceOwner } from './terminal-http-link-source-owner'
import { canOpenWorkspaceBrowserTabOnRuntime } from '@/lib/workspace-browser-tab-open'
import {
canOpenWorkspaceBrowserTabOnRuntime,
canOpenWorkspaceBrowserTabOnSsh
} from '@/lib/workspace-browser-tab-open'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
@@ -828,15 +831,22 @@ export function useTerminalPaneLifecycle({
resolvePaneLinkCwd(paneCwdRef.current, paneId, startupCwd)
const getHttpLinkSourceOwnerForPane = (paneId: number) =>
resolveTerminalHttpLinkSourceOwner(paneTransportsRef.current.get(paneId))
const canOpenRuntimeBrowserForPane = (paneId: number): boolean => {
const canOpenOwnedBrowserForPane = (paneId: number): boolean => {
const sourceOwner = getHttpLinkSourceOwnerForPane(paneId)
return (
sourceOwner.kind === 'runtime' &&
canOpenWorkspaceBrowserTabOnRuntime(
if (sourceOwner.kind === 'runtime') {
return canOpenWorkspaceBrowserTabOnRuntime(
useAppStore.getState(),
worktreeId,
sourceOwner.runtimeEnvironmentId
)
}
return (
sourceOwner.kind === 'ssh' &&
canOpenWorkspaceBrowserTabOnSsh(
useAppStore.getState(),
worktreeId,
sourceOwner.connectionId
)
)
}
const getHttpLinkActionDestinations = (paneId: number): TerminalHttpLinkActionDestinations => {
@@ -844,7 +854,7 @@ export function useTerminalPaneLifecycle({
return terminalHttpLinkActionDestinationsFor(
settingsRef.current,
sourceOwner,
canOpenRuntimeBrowserForPane(paneId)
canOpenOwnedBrowserForPane(paneId)
)
}
const getLinkActionContext = (paneId: number): TerminalLinkActionContext | null => {
@@ -1001,7 +1011,7 @@ export function useTerminalPaneLifecycle({
...terminalUrlOpenHintOptionsFor(
settingsRef.current,
getHttpLinkSourceOwnerForPane(paneId),
canOpenRuntimeBrowserForPane(paneId)
canOpenOwnedBrowserForPane(paneId)
),
showActions: settingsRef.current?.terminalLinkActionPopoverEnabled !== false
})
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
openHttpLink,
registerHttpLinkStoreAccessor,
registerRuntimeHttpLinkBrowserOpener,
registerWorkspaceHttpLinkBrowserOpener,
resolveModifierRouting
} from './http-link-routing'
@@ -73,12 +73,12 @@ describe('modifier routing across link source owners', () => {
beforeEach(() => {
vi.clearAllMocks()
registerHttpLinkStoreAccessor(() => storeState)
registerRuntimeHttpLinkBrowserOpener(openRuntimeBrowserTabMock)
registerWorkspaceHttpLinkBrowserOpener(openRuntimeBrowserTabMock)
vi.stubGlobal('window', { api: { shell: { openUrl: openUrlMock } } })
})
afterEach(() => {
registerRuntimeHttpLinkBrowserOpener(null)
registerWorkspaceHttpLinkBrowserOpener(null)
vi.unstubAllGlobals()
})
@@ -104,7 +104,7 @@ describe('modifier routing across link source owners', () => {
}
openHttpLink('https://example.com/', {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: 'wt-1',
modifierHeld: true,
sourceOwner: { kind: 'runtime', runtimeEnvironmentId: 'env-1' }
+16 -9
View File
@@ -4,7 +4,7 @@ import type { WorkspacePortScanResult } from '../../../shared/workspace-ports'
import {
openHttpLink,
registerHttpLinkStoreAccessor,
registerRuntimeHttpLinkBrowserOpener,
registerWorkspaceHttpLinkBrowserOpener,
resolveLocalhostHttpLinkDisplayUrl
} from './http-link-routing'
@@ -48,7 +48,7 @@ beforeEach(() => {
storeState.settings = undefined
storeState.workspacePortScansByKey = {}
registerHttpLinkStoreAccessor(() => storeState)
registerRuntimeHttpLinkBrowserOpener(openRuntimeBrowserTabMock)
registerWorkspaceHttpLinkBrowserOpener(openRuntimeBrowserTabMock)
vi.stubGlobal('window', {
api: {
shell: {
@@ -62,7 +62,7 @@ beforeEach(() => {
})
afterEach(() => {
registerRuntimeHttpLinkBrowserOpener(null)
registerWorkspaceHttpLinkBrowserOpener(null)
vi.unstubAllGlobals()
})
@@ -163,26 +163,33 @@ describe('openHttpLink', () => {
expect(openUrlMock).not.toHaveBeenCalled()
})
it('routes runtime and SSH document owners to their distinct destinations', () => {
it('routes runtime and SSH document owners through their workspace browsers', () => {
storeState.settings = { openLinksInApp: true, localhostWorktreeLabelsEnabled: true }
openHttpLink('http://localhost:5180/runtime', {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: 'wt-1',
sourceOwner: { kind: 'runtime', runtimeEnvironmentId: 'env-1' }
})
openHttpLink('http://localhost:5180/ssh', {
allowRemoteInApp: true,
worktreeId: 'wt-1',
sourceOwner: { kind: 'ssh', connectionId: 'ssh-1' }
})
expect(openRuntimeBrowserTabMock).toHaveBeenCalledWith({
expect(openRuntimeBrowserTabMock).toHaveBeenNthCalledWith(1, {
workspaceId: 'wt-1',
url: 'http://localhost:5180/runtime',
intent: { kind: 'url' },
expectedRuntimeEnvironmentId: 'env-1'
})
expect(openUrlMock).toHaveBeenCalledWith('http://localhost:5180/ssh')
expect(openRuntimeBrowserTabMock).toHaveBeenNthCalledWith(2, {
workspaceId: 'wt-1',
url: 'http://localhost:5180/ssh',
intent: { kind: 'url' },
expectedSshConnectionId: 'ssh-1'
})
expect(openUrlMock).not.toHaveBeenCalled()
expect(createBrowserTabMock).not.toHaveBeenCalled()
expect(registerLocalhostLabelMock).not.toHaveBeenCalled()
})
@@ -193,7 +200,7 @@ describe('openHttpLink', () => {
storeState.settings = { openLinksInApp: true, activeRuntimeEnvironmentId: null }
openHttpLink('https://example.com/', {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: 'wt-1',
sourceOwner: { kind: 'runtime', runtimeEnvironmentId: 'env-1' }
})
@@ -211,7 +218,7 @@ describe('openHttpLink', () => {
openRuntimeBrowserTabMock.mockRejectedValueOnce(new Error('runtime unavailable'))
openHttpLink('https://example.com/', {
allowRuntimeInApp: true,
allowRemoteInApp: true,
worktreeId: 'wt-1',
sourceOwner: { kind: 'runtime', runtimeEnvironmentId: 'env-1' }
})
+22 -14
View File
@@ -10,8 +10,8 @@ import { toast } from 'sonner'
export type OpenHttpLinkOptions = {
worktreeId?: string | null
/** Terminal-only opt-in for routing a runtime-owned source through its managed browser. */
allowRuntimeInApp?: boolean
/** Terminal-only opt-in for routing a remote-owned source through its managed browser. */
allowRemoteInApp?: boolean
/** Unconditional: always use the system browser regardless of settings. */
forceSystemBrowser?: boolean
/** Unconditional for local sources: open inside Orca regardless of settings. */
@@ -47,14 +47,15 @@ type StoreAccessor = () => {
workspacePortScansByKey?: Record<string, WorkspacePortScanResult>
}
type RuntimeHttpLinkBrowserRequest = {
type WorkspaceHttpLinkBrowserRequest = {
workspaceId: string
url: string
intent: { kind: 'url' }
expectedRuntimeEnvironmentId: string
expectedRuntimeEnvironmentId?: string
expectedSshConnectionId?: string
}
type RuntimeHttpLinkBrowserOpener = (request: RuntimeHttpLinkBrowserRequest) => Promise<void>
type WorkspaceHttpLinkBrowserOpener = (request: WorkspaceHttpLinkBrowserRequest) => Promise<void>
type LocalhostLinkRepo = {
id: string
@@ -74,16 +75,16 @@ type LocalhostLinkWorktree = {
// the break, several renderer test files that load this module first see
// `createEditorSlice` as undefined at store/index.ts initialization.
let storeAccessor: StoreAccessor | null = null
let runtimeHttpLinkBrowserOpener: RuntimeHttpLinkBrowserOpener | null = null
let workspaceHttpLinkBrowserOpener: WorkspaceHttpLinkBrowserOpener | null = null
export function registerHttpLinkStoreAccessor(fn: StoreAccessor): void {
storeAccessor = fn
}
export function registerRuntimeHttpLinkBrowserOpener(
fn: RuntimeHttpLinkBrowserOpener | null
export function registerWorkspaceHttpLinkBrowserOpener(
fn: WorkspaceHttpLinkBrowserOpener | null
): void {
runtimeHttpLinkBrowserOpener = fn
workspaceHttpLinkBrowserOpener = fn
}
// Scope: http(s) URLs only. file: URIs and in-worktree markdown targets are
@@ -115,7 +116,7 @@ export function resolveModifierRouting(
export function openHttpLink(url: string, opts: OpenHttpLinkOptions = {}): void {
const {
worktreeId,
allowRuntimeInApp,
allowRemoteInApp,
forceSystemBrowser,
forceInApp,
modifierHeld,
@@ -139,13 +140,20 @@ export function openHttpLink(url: string, opts: OpenHttpLinkOptions = {}): void
Boolean(worktreeId) &&
(forceInApp || openLinksInApp || modifier.wantsOrca)
if (wantsOrca && allowRuntimeInApp && worktreeId && sourceOwner?.kind === 'runtime') {
if (runtimeHttpLinkBrowserOpener) {
void runtimeHttpLinkBrowserOpener({
if (
wantsOrca &&
allowRemoteInApp &&
worktreeId &&
(sourceOwner?.kind === 'runtime' || sourceOwner?.kind === 'ssh')
) {
if (workspaceHttpLinkBrowserOpener) {
void workspaceHttpLinkBrowserOpener({
workspaceId: worktreeId,
url,
intent: { kind: 'url' },
expectedRuntimeEnvironmentId: sourceOwner.runtimeEnvironmentId
...(sourceOwner.kind === 'runtime'
? { expectedRuntimeEnvironmentId: sourceOwner.runtimeEnvironmentId }
: { expectedSshConnectionId: sourceOwner.connectionId })
}).catch((error) => {
toast.error(
error instanceof Error
@@ -0,0 +1,29 @@
import type { GlobalSettings } from '../../../shared/global-settings-types'
import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../shared/execution-host'
type SshBrowserRoutingSettings = Pick<
GlobalSettings,
'browserSshWorkspaceRoutingEnabled' | 'browserSshWorkspaceRoutingDisabledTargetIds'
>
export type SshWorkspaceBrowserRouteEligibility = {
targetId: string
eligible: boolean
}
export function resolveSshWorkspaceBrowserRouteEligibility(
executionHostId: string | null | undefined,
settings: SshBrowserRoutingSettings | null | undefined
): SshWorkspaceBrowserRouteEligibility | null {
const parsed = parseExecutionHostId(executionHostId)
// Why: runtime-owned ephemeral SSH targets belong to the paired runtime's browser route.
if (parsed?.kind !== 'ssh' || isRuntimeOwnedSshTargetId(parsed.targetId)) {
return null
}
return {
targetId: parsed.targetId,
eligible:
settings?.browserSshWorkspaceRoutingEnabled !== false &&
!settings?.browserSshWorkspaceRoutingDisabledTargetIds?.includes(parsed.targetId)
}
}
@@ -8,6 +8,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { BROWSER_SCREENCAST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import {
canOpenWorkspaceBrowserTabOnRuntime,
canOpenWorkspaceBrowserTabOnSsh,
openWorkspaceBrowserTab
} from './workspace-browser-tab-open'
@@ -76,11 +77,16 @@ describe('openWorkspaceBrowserTab', () => {
defaultBrowserSessionProfileIdByHostId: { [sshHost]: 'ssh-profile' }
}
expect(canOpenWorkspaceBrowserTabOnSsh(mocks.state as never, WORKSPACE_ID, 'ssh-target')).toBe(
true
)
await openWorkspaceBrowserTab({
workspaceId: WORKSPACE_ID,
targetGroupId: 'group-1',
url: 'https://www.google.com/search?q=private%20query',
intent: { kind: 'search', engine: 'google' }
intent: { kind: 'search', engine: 'google' },
expectedSshConnectionId: 'ssh-target'
})
expect(createBrowserTab).toHaveBeenCalledWith(
@@ -98,6 +104,33 @@ describe('openWorkspaceBrowserTab', () => {
expect(mocks.createRemote).not.toHaveBeenCalled()
})
it('fails closed when the asserted SSH browser route is opted out or belongs to another host', async () => {
const sshHost = toSshExecutionHostId('ssh-target')
mocks.state = {
...ownerState(sshHost),
settings: { browserSshWorkspaceRoutingDisabledTargetIds: ['ssh-target'] },
createBrowserTab: vi.fn(),
defaultBrowserSessionProfileId: 'focused-profile',
defaultBrowserSessionProfileIdByHostId: { [sshHost]: 'ssh-profile' }
}
expect(canOpenWorkspaceBrowserTabOnSsh(mocks.state as never, WORKSPACE_ID, 'ssh-target')).toBe(
false
)
expect(canOpenWorkspaceBrowserTabOnSsh(mocks.state as never, WORKSPACE_ID, 'ssh-other')).toBe(
false
)
await expect(
openWorkspaceBrowserTab({
workspaceId: WORKSPACE_ID,
url: 'http://0.0.0.0:8000/',
intent: { kind: 'url' },
expectedSshConnectionId: 'ssh-target'
})
).rejects.toThrow('Unable to open URL.')
expect(mocks.state.createBrowserTab).not.toHaveBeenCalled()
})
it('surfaces the opening workspace and titles runtime-owned URL tabs by target', async () => {
const createBrowserTab = vi.fn()
mocks.state = {
@@ -15,6 +15,8 @@ import {
type ClientCreationActionAvailability
} from './client-creation-action-policy'
import { resolveWorktreeOperationRoute } from './worktree-operation-route'
import { getExecutionHostIdForWorktree } from './worktree-runtime-owner'
import { resolveSshWorkspaceBrowserRouteEligibility } from './ssh-workspace-browser-route-eligibility'
export type WorkspaceBrowserTabIntent = { kind: 'url' } | { kind: 'search'; engine: SearchEngine }
@@ -24,6 +26,7 @@ export type OpenWorkspaceBrowserTabRequest = {
url: string
intent: WorkspaceBrowserTabIntent
expectedRuntimeEnvironmentId?: string
expectedSshConnectionId?: string
}
function isExpectedRuntimeBrowserRoute(
@@ -69,6 +72,42 @@ export function canOpenWorkspaceBrowserTabOnRuntime(
)
}
function isExpectedSshBrowserRoute(
state: AppState,
availability: ClientCreationActionAvailability,
route: ReturnType<typeof resolveWorktreeOperationRoute>,
workspaceId: string,
expectedSshConnectionId: string
): boolean {
if (availability.state !== 'enabled' || workspaceId === FLOATING_TERMINAL_WORKTREE_ID || !route) {
return false
}
const expectedTargetId = expectedSshConnectionId.trim()
const eligibility = resolveSshWorkspaceBrowserRouteEligibility(
getExecutionHostIdForWorktree(state, workspaceId),
state.settings
)
const host = parseExecutionHostId(route.executionHostId)
return (
Boolean(expectedTargetId) &&
route.runtimeEnvironmentId === null &&
eligibility?.eligible === true &&
eligibility.targetId === expectedTargetId &&
host?.kind === 'ssh' &&
host.targetId === expectedTargetId
)
}
export function canOpenWorkspaceBrowserTabOnSsh(
state: AppState,
workspaceId: string,
expectedSshConnectionId: string
): boolean {
const availability = getClientCreationActionPolicy(state, workspaceId)['managed-browser']
const route = resolveWorktreeOperationRoute(state, workspaceId)
return isExpectedSshBrowserRoute(state, availability, route, workspaceId, expectedSshConnectionId)
}
// Why: concurrent URL tabs are indistinguishable under a shared "Open URL"
// label until the page title loads; the query string stays out so a typed URL
// does not park credentials or tokens in persisted tab state.
@@ -183,6 +222,11 @@ export async function openWorkspaceBrowserTab(
request.expectedRuntimeEnvironmentId === undefined
? null
: request.expectedRuntimeEnvironmentId.trim()
const expectedSshConnectionId =
request.expectedSshConnectionId === undefined ? null : request.expectedSshConnectionId.trim()
if (expectedEnvironmentId !== null && expectedSshConnectionId !== null) {
throw openFailure(presentation.error, 'browser owner assertion is ambiguous')
}
if (
expectedEnvironmentId !== null &&
!isExpectedRuntimeBrowserRoute(
@@ -195,6 +239,18 @@ export async function openWorkspaceBrowserTab(
) {
throw openFailure(presentation.error, 'asserted runtime cannot provide this managed browser')
}
if (
expectedSshConnectionId !== null &&
!isExpectedSshBrowserRoute(
state,
availability,
route,
request.workspaceId,
expectedSshConnectionId
)
) {
throw openFailure(presentation.error, 'asserted SSH connection cannot provide this browser')
}
const host = parseExecutionHostId(route.executionHostId)
if (!environmentId) {
if (!host || host.kind === 'runtime') {
+2 -2
View File
@@ -49,7 +49,7 @@ import { e2eConfig } from '@/lib/e2e-config'
import type { createWebRuntimeSessionTerminal } from '@/runtime/web-runtime-session'
import {
registerHttpLinkStoreAccessor,
registerRuntimeHttpLinkBrowserOpener
registerWorkspaceHttpLinkBrowserOpener
} from '@/lib/http-link-routing'
import { installStoreListenerCensus } from './store-listener-census'
import { withReactCommitCascadeWriteProbe } from './react-commit-cascade-write-probe'
@@ -112,7 +112,7 @@ export const useAppStore = create<AppState>()(
)
registerHttpLinkStoreAccessor(() => useAppStore.getState())
registerRuntimeHttpLinkBrowserOpener(async (request) => {
registerWorkspaceHttpLinkBrowserOpener(async (request) => {
const { openWorkspaceBrowserTab } = await import('@/lib/workspace-browser-tab-open')
await openWorkspaceBrowserTab(request)
})