Honor system-browser shortcut for port opens (#5756)

This commit is contained in:
Brennan Benson 2026-06-18 22:23:56 -07:00 committed by GitHub
parent f1384597f7
commit 4a2dc069d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 413 additions and 53 deletions

View File

@ -23,10 +23,13 @@ vi.mock('@/lib/worktree-activation', () => ({
import { getLocalWorkspacePortSections } from './PortsPanel'
import {
getPortOpenBrowserTooltipLabel,
getPortSystemBrowserHint,
killWorkspacePortForTarget,
mergeWorkspacePortScans,
openWorkspacePortInBrowser,
refreshWorkspacePortScanAfterStop,
resolvePortOpenInOrcaBrowser,
scanWorkspacePortsForTarget
} from '@/lib/workspace-port-actions'
@ -67,6 +70,17 @@ const runtimeCall = vi.fn()
const runtimeEnvironmentCall = vi.fn()
const openUrl = vi.fn()
function portOpenClick(
overrides: Partial<Pick<MouseEvent, 'metaKey' | 'ctrlKey' | 'shiftKey'>> = {}
): Pick<MouseEvent, 'metaKey' | 'ctrlKey' | 'shiftKey'> {
return {
metaKey: false,
ctrlKey: false,
shiftKey: false,
...overrides
}
}
beforeEach(() => {
localScan.mockReset()
localKill.mockReset()
@ -96,6 +110,61 @@ beforeEach(() => {
})
describe('PortsPanel runtime routing', () => {
it('formats platform-specific system-browser hints for port open tooltips', () => {
expect(getPortSystemBrowserHint(true)).toBe('⇧⌘+click for system browser')
expect(getPortSystemBrowserHint(false)).toBe('Shift+Ctrl+click for system browser')
expect(getPortOpenBrowserTooltipLabel('Open in Browser', false)).toBe(
'Open in Browser. Shift+Ctrl+click for system browser'
)
})
it('maps macOS Shift+Cmd-click to system-browser port routing', () => {
expect(
resolvePortOpenInOrcaBrowser({
settings: { openLinksInApp: true },
event: portOpenClick({ metaKey: true, shiftKey: true }),
isMac: true
})
).toBe(false)
})
it('maps non-macOS Shift+Ctrl-click to system-browser port routing', () => {
expect(
resolvePortOpenInOrcaBrowser({
settings: { openLinksInApp: true },
event: portOpenClick({ ctrlKey: true, shiftKey: true }),
isMac: false
})
).toBe(false)
})
it('does not treat macOS Shift+Ctrl-click as a system-browser port override', () => {
expect(
resolvePortOpenInOrcaBrowser({
settings: { openLinksInApp: true },
event: portOpenClick({ ctrlKey: true, shiftKey: true }),
isMac: true
})
).toBe(true)
})
it('keeps plain and no-event port opens on the saved link-routing setting', () => {
expect(
resolvePortOpenInOrcaBrowser({
settings: { openLinksInApp: true },
event: portOpenClick(),
isMac: false
})
).toBe(true)
expect(
resolvePortOpenInOrcaBrowser({
settings: { openLinksInApp: false },
event: null,
isMac: false
})
).toBe(false)
})
it('uses local IPC for local workspace port scans and kills', async () => {
localScan.mockResolvedValueOnce(emptyScan)
localKill.mockResolvedValueOnce({ ok: true })
@ -313,6 +382,31 @@ describe('PortsPanel runtime routing', () => {
expect(activateAndRevealWorktreeMock).not.toHaveBeenCalled()
})
it('opens forced local workspace port clicks in the system browser', async () => {
const createBrowserTab = vi.fn()
const setRemoteBrowserPageHandle = vi.fn()
openUrl.mockResolvedValueOnce(undefined)
const openInOrcaBrowser = resolvePortOpenInOrcaBrowser({
settings: { openLinksInApp: true },
event: portOpenClick({ ctrlKey: true, shiftKey: true }),
isMac: false
})
await expect(
openWorkspacePortInBrowser({
port: workspacePort,
runtimeTarget: { kind: 'local' },
createBrowserTab: createBrowserTab as never,
setRemoteBrowserPageHandle: setRemoteBrowserPageHandle as never,
openInOrcaBrowser
})
).resolves.toEqual({ ok: true })
expect(openUrl).toHaveBeenCalledWith('http://127.0.0.1:63468')
expect(createBrowserTab).not.toHaveBeenCalled()
expect(activateAndRevealWorktreeMock).not.toHaveBeenCalled()
})
it('returns post-stop refresh failures without throwing', async () => {
const setWorkspacePortScan = vi.fn()
const setWorkspacePortScanRefreshing = vi.fn()

View File

@ -22,10 +22,11 @@ import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import {
killWorkspacePortForTarget,
getPortOpenBrowserTooltipLabel,
openWorkspacePortInBrowser,
refreshWorkspacePortScanAfterStop,
resolvePortOpenInOrcaBrowser,
scanWorkspacePortsForTarget,
shouldOpenWorkspacePortInOrcaBrowser,
workspacePortRuntimeTargetKey
} from '@/lib/workspace-port-actions'
import {
@ -281,14 +282,18 @@ function LocalWorkspacePortsPanel({ isVisible }: { isVisible: boolean }): React.
)
const handleOpenPortInBrowser = useCallback(
async (port: WorkspacePort) => {
async (port: WorkspacePort, event?: React.MouseEvent<HTMLButtonElement>) => {
const result = await openWorkspacePortInBrowser({
port,
activeWorktreeId: activeWorktree?.id,
runtimeTarget,
createBrowserTab,
setRemoteBrowserPageHandle,
openInOrcaBrowser: shouldOpenWorkspacePortInOrcaBrowser(settings)
openInOrcaBrowser: resolvePortOpenInOrcaBrowser({
settings,
event,
isMac: navigator.userAgent.includes('Mac')
})
})
if (!result.ok) {
toast.error(
@ -453,7 +458,7 @@ function LocalPortSection({
onToggle: () => void
onStopPort: (port: WorkspacePort) => void
onShowDetails: (port: WorkspacePort) => void
onOpenInBrowser: (port: WorkspacePort) => void
onOpenInBrowser: (port: WorkspacePort, event?: React.MouseEvent<HTMLButtonElement>) => void
}): React.JSX.Element | null {
if (ports.length === 0 && !emptyText) {
return null
@ -507,15 +512,18 @@ function LocalPortRow({
port: WorkspacePort
onStop: (port: WorkspacePort) => void
onShowDetails: (port: WorkspacePort) => void
onOpenInBrowser: (port: WorkspacePort) => void
onOpenInBrowser: (port: WorkspacePort, event?: React.MouseEvent<HTMLButtonElement>) => void
}): React.JSX.Element {
const handleCopy = useCallback(() => {
void window.api.ui.writeClipboardText(addressForPort(port))
}, [port])
const handleOpenBrowser = useCallback(() => {
void onOpenInBrowser(port)
}, [onOpenInBrowser, port])
const handleOpenBrowser = useCallback(
(event?: React.MouseEvent<HTMLButtonElement>) => {
void onOpenInBrowser(port, event)
},
[onOpenInBrowser, port]
)
const handleCopyButtonClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
@ -529,7 +537,9 @@ function LocalPortRow({
const handleOpenBrowserButtonClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
handleOpenBrowser()
// Why: keyboard activations have detail=0; only pointer clicks carry
// the modifier intent for the system-browser escape hatch.
handleOpenBrowser(event.detail > 0 ? event : undefined)
if (event.detail > 0) {
event.currentTarget.blur()
}
@ -555,6 +565,10 @@ function LocalPortRow({
: port.kind === 'container'
? 'Container or forwarded service'
: 'Unassigned'
const openBrowserLabel = translate(
'auto.components.right.sidebar.PortsPanel.b22b128b2a',
'Open in Browser'
)
const confidenceLabel =
port.kind === 'workspace' ? (port.owner.confidence === 'cwd' ? 'cwd' : 'command') : null
const canStopProcess =
@ -603,19 +617,13 @@ function LocalPortRow({
size="icon-xs"
className="text-muted-foreground hover:text-foreground"
onClick={handleOpenBrowserButtonClick}
aria-label={translate(
'auto.components.right.sidebar.PortsPanel.b22b128b2a',
'Open in Browser'
)}
aria-label={openBrowserLabel}
>
<ExternalLink size={13} />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate(
'auto.components.right.sidebar.PortsPanel.b22b128b2a',
'Open in Browser'
)}
{getPortOpenBrowserTooltipLabel(openBrowserLabel)}
</TooltipContent>
</Tooltip>
<Tooltip>
@ -672,9 +680,12 @@ function LocalPortRow({
<ContextMenuLabel
className={LOCAL_PORT_MENU_LABEL_CLASS}
>{`:${port.port}`}</ContextMenuLabel>
<ContextMenuItem className={LOCAL_PORT_MENU_ITEM_CLASS} onSelect={handleOpenBrowser}>
<ContextMenuItem
className={LOCAL_PORT_MENU_ITEM_CLASS}
onSelect={() => handleOpenBrowser()}
>
<ExternalLink size={13} />
{translate('auto.components.right.sidebar.PortsPanel.b22b128b2a', 'Open in Browser')}
{openBrowserLabel}
</ContextMenuItem>
<ContextMenuItem className={LOCAL_PORT_MENU_ITEM_CLASS} onSelect={handleCopy}>
<Copy size={13} />
@ -850,9 +861,15 @@ function SshPortsPanel(): React.JSX.Element {
}, [])
const handleOpenForwardInBrowser = useCallback(
(entry: PortForwardEntry) => {
(entry: PortForwardEntry, event?: React.MouseEvent<HTMLButtonElement>) => {
const url = browserUrlForPortForwardEntry(entry)
if (!shouldOpenWorkspacePortInOrcaBrowser(settings)) {
if (
!resolvePortOpenInOrcaBrowser({
settings,
event,
isMac: navigator.userAgent.includes('Mac')
})
) {
void window.api.shell.openUrl(url)
return
}
@ -935,7 +952,7 @@ function SshPortsPanel(): React.JSX.Element {
key={entry.id}
entry={entry}
onEdit={() => handleEdit(entry)}
onOpenInBrowser={() => handleOpenForwardInBrowser(entry)}
onOpenInBrowser={(event) => handleOpenForwardInBrowser(entry, event)}
/>
))}
</div>
@ -1015,7 +1032,7 @@ function ForwardedPortRow({
}: {
entry: PortForwardEntry
onEdit: () => void
onOpenInBrowser: () => void
onOpenInBrowser: (event?: React.MouseEvent<HTMLButtonElement>) => void
}): React.JSX.Element {
const [removing, setRemoving] = useState(false)
const mountedRef = useMountedRef()
@ -1037,9 +1054,12 @@ function ForwardedPortRow({
void window.api.ui.writeClipboardText(forwardedAddress)
}, [forwardedAddress])
const handleOpenBrowser = useCallback(() => {
onOpenInBrowser()
}, [onOpenInBrowser])
const handleOpenBrowser = useCallback(
(event?: React.MouseEvent<HTMLButtonElement>) => {
onOpenInBrowser(event)
},
[onOpenInBrowser]
)
const handleCopyButtonClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
@ -1053,7 +1073,9 @@ function ForwardedPortRow({
const handleOpenBrowserButtonClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
handleOpenBrowser()
// Why: keyboard activations have detail=0; only pointer clicks carry
// the modifier intent for the system-browser escape hatch.
handleOpenBrowser(event.detail > 0 ? event : undefined)
if (event.detail > 0) {
event.currentTarget.blur()
}
@ -1082,6 +1104,21 @@ function ForwardedPortRow({
)
const advertisedBrowserUrl = advertisedBrowserUrlForForwardedRow(entry)
const openBrowserLabel = translate(
'auto.components.right.sidebar.PortsPanel.b22b128b2a',
'Open in Browser'
)
const openBrowserTitle = getPortOpenBrowserTooltipLabel(
advertisedBrowserUrl
? translate(
'auto.components.right.sidebar.PortsPanel.75aeea592f',
'Open {{value0}} in Browser',
{
value0: advertisedBrowserUrl
}
)
: openBrowserLabel
)
return (
<div className="group flex items-center gap-2 py-1 px-1 -mx-1 rounded hover:bg-accent/50 transition-colors">
@ -1112,15 +1149,7 @@ function ForwardedPortRow({
type="button"
className="p-1 rounded hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
onClick={handleOpenBrowserButtonClick}
title={
advertisedBrowserUrl
? translate(
'auto.components.right.sidebar.PortsPanel.75aeea592f',
'Open {{value0}} in Browser',
{ value0: advertisedBrowserUrl }
)
: translate('auto.components.right.sidebar.PortsPanel.b22b128b2a', 'Open in Browser')
}
title={openBrowserTitle}
>
<ExternalLink size={13} />
</button>

View File

@ -47,6 +47,7 @@ describe('WorktreeCardPortsDetails', () => {
expect(markup).toContain('dev.preview.localhost:58941')
expect(markup).toContain('aria-label="Copy dev.preview.localhost:58941"')
expect(markup).toContain('Open in Browser. Shift+Ctrl+click for system browser')
expect(markup).toContain(
'<section class="space-y-1.5"><div class="flex items-center gap-1.5 px-1'
)

View File

@ -10,11 +10,12 @@ import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import {
canStopWorkspacePort,
getPortOpenBrowserTooltipLabel,
goToWorkspacePortOwner,
killWorkspacePortForTarget,
openWorkspacePortInBrowser,
refreshWorkspacePortScanAfterStop,
shouldOpenWorkspacePortInOrcaBrowser
resolvePortOpenInOrcaBrowser
} from '@/lib/workspace-port-actions'
import { addressForPort } from '@/lib/workspace-port-urls'
import type { WorkspacePort } from '../../../../shared/workspace-ports'
@ -59,11 +60,13 @@ export function WorktreeCardPortsTrigger({
function PortAction({
label,
tooltipLabel = label,
disabled = false,
onClick,
children
}: {
label: string
tooltipLabel?: string
disabled?: boolean
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
children: React.ReactNode
@ -91,7 +94,7 @@ function PortAction({
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{label}
{tooltipLabel}
</TooltipContent>
</Tooltip>
)
@ -115,17 +118,28 @@ function WorktreePortRow({ port }: { port: WorkspacePort }): React.JSX.Element {
const processLabel = port.processName ?? (port.pid ? `PID ${port.pid}` : 'Unknown process')
const address = addressForPort(port)
const canStop = canStopWorkspacePort(port)
const openBrowserLabel = translate(
'auto.components.sidebar.WorktreeCardPorts.33bc7d7495',
'Open in Browser'
)
const handleOpen = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
recordFeatureInteraction('ports')
const openInOrcaBrowser = resolvePortOpenInOrcaBrowser({
settings,
// Why: keyboard activations have detail=0; only pointer clicks carry
// the modifier intent for the system-browser escape hatch.
event: event.detail > 0 ? event : null,
isMac: navigator.userAgent.includes('Mac')
})
void openWorkspacePortInBrowser({
port,
runtimeTarget,
createBrowserTab,
setRemoteBrowserPageHandle,
openInOrcaBrowser: shouldOpenWorkspacePortInOrcaBrowser(settings)
openInOrcaBrowser
}).then((result) => {
if (!result.ok) {
toast.error(
@ -244,10 +258,8 @@ function WorktreePortRow({ port }: { port: WorkspacePort }): React.JSX.Element {
</Tooltip>
<div className="absolute inset-y-0 right-0 flex items-center gap-0.5 rounded-md border border-border/40 bg-popover/95 px-0.5 can-hover:opacity-0 shadow-xs transition-opacity group-hover/port:opacity-100 group-focus-within/port:opacity-100">
<PortAction
label={translate(
'auto.components.sidebar.WorktreeCardPorts.33bc7d7495',
'Open in Browser'
)}
label={openBrowserLabel}
tooltipLabel={getPortOpenBrowserTooltipLabel(openBrowserLabel)}
onClick={handleOpen}
>
<ExternalLink className="size-3" />

View File

@ -0,0 +1,184 @@
// @vitest-environment happy-dom
import React, { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorkspacePort } from '../../../../shared/workspace-ports'
const {
activateAndRevealWorktreeMock,
createBrowserTabMock,
openUrlMock,
recordFeatureInteractionMock,
setRemoteBrowserPageHandleMock,
storeState
} = vi.hoisted(() => {
const state = {
settings: { openLinksInApp: true },
createBrowserTab: vi.fn(),
setRemoteBrowserPageHandle: vi.fn(),
setWorkspacePortScan: vi.fn(),
setWorkspacePortScanForKey: vi.fn(),
setWorkspacePortScanRefreshing: vi.fn(),
recordFeatureInteraction: vi.fn(),
workspacePortScansByKey: {}
}
return {
activateAndRevealWorktreeMock: vi.fn(),
createBrowserTabMock: state.createBrowserTab,
openUrlMock: vi.fn(),
recordFeatureInteractionMock: state.recordFeatureInteraction,
setRemoteBrowserPageHandleMock: state.setRemoteBrowserPageHandle,
storeState: state
}
})
vi.mock('@/store', () => {
const useAppStore = Object.assign(
(selector: (state: typeof storeState) => unknown) => selector(storeState),
{ getState: () => storeState }
)
return { useAppStore }
})
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: activateAndRevealWorktreeMock
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => null
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({
getActiveRuntimeTarget: () => ({ kind: 'local' }),
callRuntimeRpc: vi.fn(),
RuntimeRpcCallError: class RuntimeRpcCallError extends Error {
code?: string
}
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>
}))
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string) => fallback
}))
vi.mock('sonner', () => ({
toast: {
error: vi.fn(),
success: vi.fn()
}
}))
import { PortRow } from './ports-status-popover-rows'
const externalPort: WorkspacePort = {
id: '127.0.0.1:63468:1234',
bindHost: '127.0.0.1',
connectHost: '127.0.0.1',
port: 63468,
pid: 1234,
processName: 'node',
protocol: 'http',
kind: 'external'
}
describe('status bar port row open routing', () => {
let container: HTMLDivElement
let root: Root
beforeEach(() => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mozilla/5.0 (X11; Linux x86_64)',
configurable: true
})
;(window as unknown as { api: unknown }).api = {
shell: {
openUrl: openUrlMock
},
ui: {
writeClipboardText: vi.fn()
}
}
openUrlMock.mockResolvedValue(undefined)
createBrowserTabMock.mockReset()
openUrlMock.mockClear()
recordFeatureInteractionMock.mockClear()
setRemoteBrowserPageHandleMock.mockClear()
activateAndRevealWorktreeMock.mockClear()
})
afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
})
function renderPortRow(): HTMLButtonElement {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
act(() => {
root.render(<PortRow port={externalPort} activeWorktreeId={null} external />)
})
const openButton = container.querySelector<HTMLButtonElement>(
'button[aria-label="Open in Browser"]'
)
if (!openButton) {
throw new Error('expected Open in Browser button')
}
expect(container.textContent).toContain('Open in Browser. Shift+Ctrl+click for system browser')
return openButton
}
it('keeps the open button enabled and forwards Shift+Ctrl-click to system-browser routing', async () => {
const openButton = renderPortRow()
expect(openButton.disabled).toBe(false)
await act(async () => {
openButton.dispatchEvent(
new window.MouseEvent('click', {
bubbles: true,
cancelable: true,
ctrlKey: true,
detail: 1,
shiftKey: true
})
)
await Promise.resolve()
})
expect(recordFeatureInteractionMock).toHaveBeenCalledWith('ports')
expect(openUrlMock).toHaveBeenCalledWith('http://127.0.0.1:63468')
expect(createBrowserTabMock).not.toHaveBeenCalled()
expect(activateAndRevealWorktreeMock).not.toHaveBeenCalled()
})
it('keeps no-pointer activations on the saved link-routing setting', async () => {
const openButton = renderPortRow()
expect(openButton.disabled).toBe(false)
await act(async () => {
openButton.dispatchEvent(
new window.MouseEvent('click', {
bubbles: true,
cancelable: true,
ctrlKey: true,
shiftKey: true
})
)
await Promise.resolve()
})
expect(recordFeatureInteractionMock).toHaveBeenCalledWith('ports')
expect(openUrlMock).not.toHaveBeenCalled()
expect(createBrowserTabMock).not.toHaveBeenCalled()
})
})

View File

@ -6,11 +6,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import {
addressForPort,
canStopWorkspacePort,
getPortOpenBrowserTooltipLabel,
goToWorkspacePortOwner,
killWorkspacePortForTarget,
openWorkspacePortInBrowser,
refreshWorkspacePortScanAfterStop,
shouldOpenWorkspacePortInOrcaBrowser
resolvePortOpenInOrcaBrowser
} from '@/lib/workspace-port-actions'
import type { WorkspacePortGroup } from '@/lib/workspace-port-groups'
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
@ -21,11 +22,13 @@ import { translate } from '@/i18n/i18n'
function PortAction({
label,
tooltipLabel = label,
onClick,
disabled,
children
}: {
label: string
tooltipLabel?: string
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
disabled?: boolean
children: React.ReactNode
@ -57,7 +60,7 @@ function PortAction({
{disabled ? <span className="inline-flex">{button}</span> : button}
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4} className="z-[70]">
{label}
{tooltipLabel}
</TooltipContent>
</Tooltip>
)
@ -90,14 +93,23 @@ export function PortRow({
[runtimeEnvironmentId, settings]
)
const processLabel = port.processName ?? (port.pid ? `PID ${port.pid}` : 'Unknown process')
const openInOrcaBrowser = shouldOpenWorkspacePortInOrcaBrowser(settings)
const canOpen = !openInOrcaBrowser || port.kind === 'workspace' || Boolean(activeWorktreeId)
const canStop = canStopWorkspacePort(port)
const openBrowserLabel = translate(
'auto.components.status.bar.ports.status.popover.rows.085f4f0334',
'Open in Browser'
)
const handleOpen = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
recordFeatureInteraction('ports')
const openInOrcaBrowser = resolvePortOpenInOrcaBrowser({
settings,
// Why: keyboard activations have detail=0; only pointer clicks carry
// the modifier intent for the system-browser escape hatch.
event: event.detail > 0 ? event : null,
isMac: navigator.userAgent.includes('Mac')
})
void openWorkspacePortInBrowser({
port,
activeWorktreeId,
@ -120,10 +132,10 @@ export function PortRow({
[
activeWorktreeId,
createBrowserTab,
openInOrcaBrowser,
port,
recordFeatureInteraction,
runtimeTarget,
settings,
setRemoteBrowserPageHandle
]
)
@ -219,12 +231,9 @@ export function PortRow({
</Tooltip>
<div className="absolute inset-y-0 right-0 flex items-center gap-0.5 rounded-md border border-border/40 bg-popover/95 px-0.5 can-hover:opacity-0 shadow-xs transition-opacity group-hover/port:opacity-100 group-focus-within/port:opacity-100">
<PortAction
label={translate(
'auto.components.status.bar.ports.status.popover.rows.085f4f0334',
'Open in Browser'
)}
label={openBrowserLabel}
tooltipLabel={getPortOpenBrowserTooltipLabel(openBrowserLabel)}
onClick={handleOpen}
disabled={!canOpen}
>
<ExternalLink className="size-3" />
</PortAction>

View File

@ -46,6 +46,37 @@ export function shouldOpenWorkspacePortInOrcaBrowser(
return settings?.openLinksInApp === true
}
function isMacShortcutPlatform(): boolean {
return typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac')
}
export function getPortSystemBrowserHint(isMac: boolean = isMacShortcutPlatform()): string {
return isMac ? '⇧⌘+click for system browser' : 'Shift+Ctrl+click for system browser'
}
export function getPortOpenBrowserTooltipLabel(openLabel: string, isMac?: boolean): string {
return `${openLabel}. ${getPortSystemBrowserHint(isMac)}`
}
type PortOpenClickEvent = Pick<MouseEvent, 'metaKey' | 'ctrlKey' | 'shiftKey'>
export function resolvePortOpenInOrcaBrowser({
settings,
event,
isMac
}: {
settings: { openLinksInApp?: boolean } | null | undefined
event?: PortOpenClickEvent | null
isMac: boolean
}): boolean {
// Why: Shift+Cmd/Ctrl is the external-browser escape hatch; no pointer
// event means context-menu and keyboard opens should keep the saved setting.
if (event?.shiftKey && (isMac ? event.metaKey : event.ctrlKey)) {
return false
}
return shouldOpenWorkspacePortInOrcaBrowser(settings)
}
export function workspacePortOwnerWorktreeId(port: WorkspacePort): string | null {
return port.kind === 'workspace' ? port.owner.worktreeId : null
}