Support shift+cmd/ctrl-click for external terminal links (#495)

This commit is contained in:
Neil 2026-04-11 15:33:09 -07:00 committed by GitHub
parent 49c7b788c3
commit aeb8002df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 70 additions and 14 deletions

View File

@ -6,15 +6,20 @@ const openFileUriMock = vi.fn()
const openFileMock = vi.fn()
const authorizeExternalPathMock = vi.fn()
const statMock = vi.fn().mockResolvedValue({ isDirectory: false })
const setActiveWorktreeMock = vi.fn()
const createBrowserTabMock = vi.fn()
const deps = { worktreeId: 'wt-1', worktreePath: '/tmp' }
const storeState = {
settings: undefined as { openLinksInApp?: boolean } | undefined,
setActiveWorktree: setActiveWorktreeMock,
createBrowserTab: createBrowserTabMock,
openFile: openFileMock
}
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({
setActiveWorktree: vi.fn(),
openFile: openFileMock
})
getState: () => storeState
}
}))
@ -28,6 +33,7 @@ function setPlatform(userAgent: string): void {
beforeEach(() => {
vi.clearAllMocks()
storeState.settings = undefined
vi.stubGlobal('window', {
api: {
shell: {
@ -75,6 +81,27 @@ describe('handleOscLink', () => {
expect(openUrlMock).toHaveBeenCalledWith('https://example.com/')
})
it('keeps cmd/ctrl+click in Orca when the in-app browser setting is enabled', () => {
setPlatform('Macintosh')
storeState.settings = { openLinksInApp: true }
handleOscLink('https://example.com', { metaKey: true, ctrlKey: false, shiftKey: false }, deps)
expect(createBrowserTabMock).toHaveBeenCalledWith('wt-1', 'https://example.com/')
expect(setActiveWorktreeMock).toHaveBeenCalledWith('wt-1')
expect(openUrlMock).not.toHaveBeenCalled()
})
it('uses the system browser for shift+cmd/ctrl+click even when Orca browser tabs are enabled', () => {
setPlatform('Windows')
storeState.settings = { openLinksInApp: true }
handleOscLink('https://example.com', { metaKey: false, ctrlKey: true, shiftKey: true }, deps)
expect(openUrlMock).toHaveBeenCalledWith('https://example.com/')
expect(createBrowserTabMock).not.toHaveBeenCalled()
})
it('opens file links in Orca instead of via shell when the platform modifier is pressed', async () => {
setPlatform('Windows')

View File

@ -18,6 +18,23 @@ export type LinkHandlerDeps = {
pathExistsCache: Map<string, boolean>
}
type TerminalLinkEvent = Pick<MouseEvent, 'metaKey' | 'ctrlKey'> &
Partial<Pick<MouseEvent, 'shiftKey'>>
function isMacPlatform(): boolean {
return navigator.userAgent.includes('Mac')
}
export function getTerminalFileOpenHint(): string {
return isMacPlatform() ? '⌘+click to open' : 'Ctrl+click to open'
}
export function getTerminalUrlOpenHint(): string {
return isMacPlatform()
? '⌘+click to open or ⇧⌘+click for system browser'
: 'Ctrl+click to open or Shift+Ctrl+click for system browser'
}
export function openDetectedFilePath(
filePath: string,
line: number | null,
@ -152,13 +169,13 @@ export function createFilePathLinkProvider(
export function isTerminalLinkActivation(
event: Pick<MouseEvent, 'metaKey' | 'ctrlKey'> | undefined
): boolean {
const isMac = navigator.userAgent.includes('Mac')
const isMac = isMacPlatform()
return isMac ? Boolean(event?.metaKey) : Boolean(event?.ctrlKey)
}
export function handleOscLink(
rawText: string,
event: Pick<MouseEvent, 'metaKey' | 'ctrlKey'> | undefined,
event: TerminalLinkEvent | undefined,
deps: Pick<LinkHandlerDeps, 'worktreeId' | 'worktreePath'>
): void {
if (!isTerminalLinkActivation(event)) {
@ -177,7 +194,9 @@ export function handleOscLink(
// Why: when the user opts into Orca's browser tabs, terminal links should
// stay worktree-scoped instead of escaping to the system browser. We still
// fall back externally when the setting is off or no worktree owns the pane.
if (store.settings?.openLinksInApp && deps.worktreeId) {
// Shift is the explicit escape hatch for "open this one in my system browser"
// without forcing the user to toggle the global in-app browser preference.
if (store.settings?.openLinksInApp && deps.worktreeId && !event?.shiftKey) {
store.setActiveWorktree(deps.worktreeId)
store.createBrowserTab(deps.worktreeId, parsed.toString())
return

View File

@ -3,7 +3,12 @@ import { useEffect, useRef } from 'react'
import type { IDisposable } from '@xterm/xterm'
import { PaneManager } from '@/lib/pane-manager/pane-manager'
import { useAppStore } from '@/store'
import { createFilePathLinkProvider, handleOscLink } from './terminal-link-handlers'
import {
createFilePathLinkProvider,
getTerminalFileOpenHint,
getTerminalUrlOpenHint,
handleOscLink
} from './terminal-link-handlers'
import type { LinkHandlerDeps } from './terminal-link-handlers'
import type { GlobalSettings, TerminalLayoutSnapshot } from '../../../../shared/types'
import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts'
@ -209,13 +214,13 @@ export function useTerminalPaneLifecycle({
getPtyIdForPane: (paneId) => paneTransportsRef.current.get(paneId)?.getPtyId() ?? null
})
const isMac = navigator.userAgent.includes('Mac')
const openLinkHint = isMac ? '⌘+click to open' : 'Ctrl+click to open'
const fileOpenLinkHint = getTerminalFileOpenHint()
const urlOpenLinkHint = getTerminalUrlOpenHint()
const manager = new PaneManager(container, {
onPaneCreated: (pane) => {
const linkProviderDisposable = pane.terminal.registerLinkProvider(
createFilePathLinkProvider(pane.id, linkDeps, pane.linkTooltip, openLinkHint)
createFilePathLinkProvider(pane.id, linkDeps, pane.linkTooltip, fileOpenLinkHint)
)
linkProviderDisposablesRef.current.set(pane.id, linkProviderDisposable)
pane.terminal.options.linkHandler = {
@ -225,7 +230,7 @@ export function useTerminalPaneLifecycle({
// GitHub owner/repo#issue references emitted by CLI tools) — same
// behaviour as the WebLinksAddon provides for plain-text URLs.
hover: (_event, text) => {
pane.linkTooltip.textContent = `${text} (${openLinkHint})`
pane.linkTooltip.textContent = `${text} (${urlOpenLinkHint})`
pane.linkTooltip.style.display = ''
},
leave: () => {

View File

@ -19,6 +19,12 @@ import { safeFit } from './pane-tree-ops'
const ENABLE_WEBGL_RENDERER = true
function getTerminalUrlOpenHint(): string {
return navigator.userAgent.includes('Mac')
? '⌘+click to open or ⇧⌘+click for system browser'
: 'Ctrl+click to open or Shift+Ctrl+click for system browser'
}
export function createPaneDOM(
id: number,
options: PaneManagerOptions,
@ -65,8 +71,7 @@ export function createPaneDOM(
const fitAddon = new FitAddon()
const searchAddon = new SearchAddon()
const unicode11Addon = new Unicode11Addon()
const isMac = navigator.userAgent.includes('Mac')
const openLinkHint = isMac ? '⌘+click to open' : 'Ctrl+click to open'
const openLinkHint = getTerminalUrlOpenHint()
// URL tooltip element — Ghostty-style bottom-left hint on hover
const linkTooltip = document.createElement('div')