From 4a2dc069d5334467f2efc2e745bd014c41fc581c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:23:56 -0700 Subject: [PATCH] Honor system-browser shortcut for port opens (#5756) --- .../right-sidebar/PortsPanel.test.tsx | 94 +++++++++ .../components/right-sidebar/PortsPanel.tsx | 101 ++++++---- .../sidebar/WorktreeCardPorts.test.tsx | 1 + .../components/sidebar/WorktreeCardPorts.tsx | 26 ++- .../ports-status-popover-rows.test.tsx | 184 ++++++++++++++++++ .../status-bar/ports-status-popover-rows.tsx | 29 ++- .../src/lib/workspace-port-actions.ts | 31 +++ 7 files changed, 413 insertions(+), 53 deletions(-) create mode 100644 src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx diff --git a/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx b/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx index 312264794..1dc5b616e 100644 --- a/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx @@ -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 { + 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() diff --git a/src/renderer/src/components/right-sidebar/PortsPanel.tsx b/src/renderer/src/components/right-sidebar/PortsPanel.tsx index 13881765c..a27195550 100644 --- a/src/renderer/src/components/right-sidebar/PortsPanel.tsx +++ b/src/renderer/src/components/right-sidebar/PortsPanel.tsx @@ -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) => { 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) => 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) => 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) => { + void onOpenInBrowser(port, event) + }, + [onOpenInBrowser, port] + ) const handleCopyButtonClick = useCallback( (event: React.MouseEvent) => { @@ -529,7 +537,9 @@ function LocalPortRow({ const handleOpenBrowserButtonClick = useCallback( (event: React.MouseEvent) => { - 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} > - {translate( - 'auto.components.right.sidebar.PortsPanel.b22b128b2a', - 'Open in Browser' - )} + {getPortOpenBrowserTooltipLabel(openBrowserLabel)} @@ -672,9 +680,12 @@ function LocalPortRow({ {`:${port.port}`} - + handleOpenBrowser()} + > - {translate('auto.components.right.sidebar.PortsPanel.b22b128b2a', 'Open in Browser')} + {openBrowserLabel} @@ -850,9 +861,15 @@ function SshPortsPanel(): React.JSX.Element { }, []) const handleOpenForwardInBrowser = useCallback( - (entry: PortForwardEntry) => { + (entry: PortForwardEntry, event?: React.MouseEvent) => { 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)} /> ))} @@ -1015,7 +1032,7 @@ function ForwardedPortRow({ }: { entry: PortForwardEntry onEdit: () => void - onOpenInBrowser: () => void + onOpenInBrowser: (event?: React.MouseEvent) => 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) => { + onOpenInBrowser(event) + }, + [onOpenInBrowser] + ) const handleCopyButtonClick = useCallback( (event: React.MouseEvent) => { @@ -1053,7 +1073,9 @@ function ForwardedPortRow({ const handleOpenBrowserButtonClick = useCallback( (event: React.MouseEvent) => { - 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 (
@@ -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} > diff --git a/src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx index 774b0dc30..0a8bb4d24 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx @@ -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( '
- {label} + {tooltipLabel} ) @@ -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) => { 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 {
diff --git a/src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx b/src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx new file mode 100644 index 000000000..47c86d89c --- /dev/null +++ b/src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx @@ -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() + }) + const openButton = container.querySelector( + '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() + }) +}) diff --git a/src/renderer/src/components/status-bar/ports-status-popover-rows.tsx b/src/renderer/src/components/status-bar/ports-status-popover-rows.tsx index b0a410337..b88c41eaf 100644 --- a/src/renderer/src/components/status-bar/ports-status-popover-rows.tsx +++ b/src/renderer/src/components/status-bar/ports-status-popover-rows.tsx @@ -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) => void disabled?: boolean children: React.ReactNode @@ -57,7 +60,7 @@ function PortAction({ {disabled ? {button} : button} - {label} + {tooltipLabel} ) @@ -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) => { 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({
diff --git a/src/renderer/src/lib/workspace-port-actions.ts b/src/renderer/src/lib/workspace-port-actions.ts index 8e40333e7..2a3a93667 100644 --- a/src/renderer/src/lib/workspace-port-actions.ts +++ b/src/renderer/src/lib/workspace-port-actions.ts @@ -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 + +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 }