fix(links): open local html file links in the browser link routing picks (#32706)

This commit is contained in:
Kelvin Amoaba
2026-10-23 21:18:09 +00:00
committed by GitHub
parent 6ed9382c05
commit 2d06b6d6f9
13 changed files with 158 additions and 49 deletions
@@ -7,6 +7,7 @@ import {
import type { TerminalLinkClickBehavior } from '@/components/terminal-pane/terminal-link-click-behavior'
import {
buildHttpLinkActions,
httpLinkDirectClickDestination,
openRoutedHttpLink,
type HttpLinkActionDestinations,
type HttpLinkDestination
@@ -59,11 +60,7 @@ export function handleNativeChatWebLink(
if (isTerminalLinkDirectActivation(event)) {
event.preventDefault()
open(
event.shiftKey
? (deps.destinations.alternate ?? deps.destinations.primary)
: deps.destinations.primary
)
open(httpLinkDirectClickDestination(deps.destinations, event.shiftKey))
return true
}
if (!isTerminalLinkActionActivation(event)) {
@@ -0,0 +1,14 @@
import {
httpLinkActionDestinationsFor,
type HttpLinkActionDestinations
} from '@/lib/http-link-destinations'
import { useAppStore } from '@/store'
export function isHtmlFilePath(filePath: string): boolean {
return /\.html?$/i.test(filePath)
}
/** A local HTML file is a page, so it opens where http links do. */
export function localHtmlFileDestinations(): HttpLinkActionDestinations {
return httpLinkActionDestinationsFor(useAppStore.getState().settings, { kind: 'local' }, false)
}
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { TerminalLinkActionContext } from './terminal-link-action-request'
const mocks = vi.hoisted(() => {
const settings: { activeRuntimeEnvironmentId: string | null } = {
const settings: { activeRuntimeEnvironmentId: string | null; openLinksInApp?: boolean } = {
activeRuntimeEnvironmentId: null
}
const tabGroups: Record<string, unknown> = {}
@@ -105,6 +105,33 @@ describe('terminal file link actions', () => {
})
})
it('offers a local HTML file the link-routing browsers, primary first', () => {
const request = vi.fn()
handleTerminalFileLink('/repo/report.html', null, null, plainEvent(), deps, context(request))
mocks.settings = { activeRuntimeEnvironmentId: null, openLinksInApp: true }
handleTerminalFileLink('/repo/report.html', null, null, plainEvent(), deps, context(request))
const [systemFirst, orcaFirst] = request.mock.calls.map(([call]) => call)
expect([systemFirst.primary.label, systemFirst.alternate.label]).toEqual([
'System Browser',
'Orca Browser'
])
expect([orcaFirst.primary.label, orcaFirst.alternate.label]).toEqual([
'Orca Browser',
'System Browser'
])
systemFirst.primary.run()
systemFirst.alternate.run()
expect(mocks.openDetectedFilePath).toHaveBeenNthCalledWith(1, '/repo/report.html', null, null, {
...deps,
browserDestination: 'system'
})
expect(mocks.openDetectedFilePath).toHaveBeenNthCalledWith(2, '/repo/report.html', null, null, {
...deps,
browserDestination: 'orca'
})
})
it('labels workspace switching and omits an impossible remote alternate', () => {
mocks.worktreeRoot = true
mocks.canOpenWithSystemDefault = false
@@ -1,5 +1,6 @@
import type { LinkActionRequest } from '@/components/link-actions/link-action-request'
import type { HttpLinkSourceOwner } from '@/lib/http-link-routing'
import { buildHttpLinkActions } from '@/lib/http-link-destinations'
import {
getTerminalFileContext,
mapTerminalFilePath,
@@ -8,6 +9,7 @@ import {
terminalLinkWslDistro,
type FileOpenFailure
} from './terminal-file-open-routing'
import { isHtmlFilePath, localHtmlFileDestinations } from './local-html-file-destinations'
import { isTerminalLinkDirectActivation } from './terminal-link-activation'
import {
requestTerminalLinkAction,
@@ -166,22 +168,30 @@ export function buildFileLinkActions(
}
: null
const secondaryActions = [openInGroupRow, revealRow].filter((row) => row !== null)
const rows =
!worktreeRoot && canOpenWithSystemDefault && isHtmlFilePath(mappedPath)
? buildHttpLinkActions(localHtmlFileDestinations(), (browserDestination) =>
openDetectedFilePath(filePath, line, column, { ...deps, browserDestination })
)
: {
primary: {
label: worktreeRoot
? translate(
'auto.components.terminal.pane.TerminalLinkActionPopover.switchWorkspace',
'Switch workspace'
)
: translate(
'auto.components.terminal.pane.TerminalLinkActionPopover.openFile',
'Open file'
),
run: () => openDetectedFilePath(filePath, line, column, deps)
},
...(systemDefaultRow ? { alternate: systemDefaultRow } : {})
}
return {
destination: mappedPath,
kind: worktreeRoot ? 'workspace' : 'file',
primary: {
label: worktreeRoot
? translate(
'auto.components.terminal.pane.TerminalLinkActionPopover.switchWorkspace',
'Switch workspace'
)
: translate(
'auto.components.terminal.pane.TerminalLinkActionPopover.openFile',
'Open file'
),
run: () => openDetectedFilePath(filePath, line, column, deps)
},
...(systemDefaultRow ? { alternate: systemDefaultRow } : {}),
...rows,
...(secondaryActions.length > 0 ? { secondaryActions } : {})
}
}
@@ -39,6 +39,11 @@ export {
type TerminalFileContext
} from './terminal-file-path-mapping'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
import {
httpLinkDirectClickDestination,
type HttpLinkDestination
} from '@/lib/http-link-destinations'
import { isHtmlFilePath, localHtmlFileDestinations } from './local-html-file-destinations'
export type FileOpenFailure = {
/** `missing` is a verified absence; `unverifiable` means the host could not answer (dropped SSH, timeout, denied path). */
@@ -51,7 +56,10 @@ type TerminalFileOpenDeps = {
worktreePath: string
runtimeEnvironmentId?: string | null
wslDistro?: string | null
/** Shift+modifier: the OS default app, or the other link-routing browser for a local HTML file. */
openWithSystemDefault?: boolean
/** Where a local HTML file opens, chosen from its popover; otherwise link routing decides. */
browserDestination?: HttpLinkDestination
/** The paired-server terminal that printed the link; lets its host grant a file outside workspaces. */
terminalHandle?: string | null
/** Reports a path that could not be verified before opening; skipped once a later open supersedes it. */
@@ -62,10 +70,6 @@ type TerminalFileOpenDeps = {
openInSourceGroup?: boolean
}
export function isHtmlFilePath(filePath: string): boolean {
return /\.html?$/i.test(filePath)
}
function openHtmlFileInBrowser(filePath: string, worktreeId: string, clientLocal: boolean): void {
const store = useAppStore.getState()
if (worktreeId) {
@@ -213,7 +217,21 @@ export function openDetectedFilePath(
}
const canOpenWithSystemDefault = shouldOpenTerminalFileWithSystemDefault(fileContext)
if (openWithSystemDefault && canOpenWithSystemDefault) {
// Why: a remote workspace's hint and rows promise Orca before the path resolves, and an
// explicit group request falls through so the HTML branch can refuse it.
const pageDestination =
canOpenWithSystemDefault &&
!clientLocal &&
hostGrantId === null &&
!explicitGroupRequest &&
isHtmlFilePath(mappedFilePath)
? (deps.browserDestination ??
httpLinkDirectClickDestination(localHtmlFileDestinations(), openWithSystemDefault))
: null
const opensWithSystemDefault = pageDestination
? pageDestination === 'system'
: openWithSystemDefault
if (opensWithSystemDefault && canOpenWithSystemDefault) {
// Why: Shift+Cmd/Ctrl mirrors URL links by escaping Orca and honoring the
// user's OS file associations without adding editor-specific settings.
const openedWithSystemDefault = await window.api.shell.openFilePath(
@@ -243,8 +261,8 @@ export function openDetectedFilePath(
return
}
// Why: local HTML files render in Orca's browser for ordinary Cmd/Ctrl-click,
// and remain the fallback if Shift+Cmd/Ctrl cannot launch the OS default.
// Why: a local HTML file routed to Orca renders in its browser, which also catches a
// system-browser open the OS could not launch.
if (isHtmlFilePath(mappedFilePath) && hostGrantId === null) {
if (canOpenWithSystemDefault) {
// Why: Orca's browser has no group placement in v1.
@@ -201,6 +201,7 @@ describe('createFilePathLinkProvider range bounds', () => {
it('opens a wrapped continuation-row html path from a direct modifier-click fallback', async () => {
setPlatform('Macintosh')
storeState.settings = { openLinksInApp: true }
const rows = [
makeBufferLine('open mobile/mock-'),
makeBufferLine('homepage.html', { isWrapped: true })
@@ -49,8 +49,9 @@ vi.mock('@/lib/runtime-workspace-file-route', () => ({
installTerminalLinkTestEnvironment(doubles)
describe('handleOscLink', () => {
it('opens local .html file paths in Orca browser tabs with the platform modifier', async () => {
it('opens local .html file paths in Orca browser tabs when links route to Orca', async () => {
setPlatform('Macintosh')
storeState.settings = { openLinksInApp: true }
openDetectedFilePath('/tmp/report.html', null, null, deps)
@@ -72,8 +73,9 @@ describe('handleOscLink', () => {
})
})
it('also opens local .htm paths in Orca browser tabs with the platform modifier', async () => {
it('also opens local .htm paths in Orca browser tabs when links route to Orca', async () => {
setPlatform('Macintosh')
storeState.settings = { openLinksInApp: true }
openDetectedFilePath('/tmp/legacy.HTM', null, null, deps)
await new Promise((resolve) => setTimeout(resolve, 0))
@@ -88,6 +90,28 @@ describe('handleOscLink', () => {
expect(openFilePathMock).not.toHaveBeenCalled()
})
it('opens local .html file paths in the system browser when links route there', async () => {
setPlatform('Macintosh')
storeState.settings = { openLinksInApp: false }
openDetectedFilePath('/tmp/report.html', null, null, deps)
await flushAsyncWork()
expect(openFilePathMock).toHaveBeenCalledWith('/tmp/report.html', 'local')
expect(createBrowserTabMock).not.toHaveBeenCalled()
// Shift takes the other destination, as it does for http links.
openDetectedFilePath('/tmp/report.html', null, null, { ...deps, openWithSystemDefault: true })
await flushAsyncWork()
expect(openFilePathMock).toHaveBeenCalledTimes(1)
expect(createBrowserTabMock).toHaveBeenCalledWith(
'wt-1',
'file:///tmp/report.html',
expect.objectContaining({ title: 'report.html', activate: true })
)
})
it('opens local file paths in Orca and reveals default column 1 with the platform modifier', async () => {
setPlatform('Macintosh')
@@ -8,10 +8,8 @@ import {
buildCandidateLogicalLinesForBufferPosition,
openFilePathLinkAtBufferPosition
} from './terminal-file-link-hit-testing'
import {
isHtmlFilePath,
shouldOpenTerminalFileWithSystemDefault
} from './terminal-file-open-routing'
import { shouldOpenTerminalFileWithSystemDefault } from './terminal-file-open-routing'
import { isHtmlFilePath, localHtmlFileDestinations } from './local-html-file-destinations'
import { fileLinkTargetExists, resolveFileLinkTarget } from './terminal-file-link-target'
import {
buildHardWrappedPathLogicalLineCandidates,
@@ -180,7 +178,7 @@ export function createFilePathLinkProvider(
? getTerminalWorktreePathOpenHint(canOpenWithSystemDefault, showActions)
: canOpenWithSystemDefault
? isHtmlFilePath(mappedPath)
? getTerminalHtmlFileOpenHint(showActions)
? getTerminalHtmlFileOpenHint(localHtmlFileDestinations(), showActions)
: showActions
? openLinkHint
: getTerminalFileOpenHint(false)
@@ -184,6 +184,7 @@ describe('terminal file links carry their source tab', () => {
it('keeps automatic HTML opens on their existing browser route', async () => {
setPlatform('Macintosh')
storeState.settings = { openLinksInApp: true }
seedSourceTab()
openDetectedFilePath('/tmp/report.html', null, null, sourceDeps)
@@ -1,4 +1,7 @@
import { canSourceOwnerOpenInOrca } from '@/lib/http-link-destinations'
import {
canSourceOwnerOpenInOrca,
type HttpLinkActionDestinations
} from '@/lib/http-link-destinations'
import type { HttpLinkSourceOwner } from '@/lib/http-link-routing'
export function isMacPlatform(): boolean {
@@ -23,12 +26,16 @@ export function getTerminalOrcaFileOpenHint(showActions = true): string {
: `${prefix}Ctrl+click to open in Orca`
}
// Why: local HTML paths keep Shift+modifier as the system-browser shortcut.
export function getTerminalHtmlFileOpenHint(showActions = true): string {
const prefix = terminalLinkActionHintPrefix(showActions)
return isMacPlatform()
? `${prefix}⌘+click to open, or ⇧⌘+click for default browser`
: `${prefix}Ctrl+click to open, or Shift+Ctrl+click for default browser`
// Why: Shift+modifier on a local HTML file always takes the other link-routing destination.
export function getTerminalHtmlFileOpenHint(
destinations: HttpLinkActionDestinations,
showActions = true
): string {
return getTerminalUrlOpenHint({
openLinksInApp: destinations.primary === 'orca',
modifierInverts: true,
showActions
})
}
export type TerminalUrlOpenHintOptions = {
@@ -226,8 +226,11 @@ describe('handleOscLink', () => {
expect(getTerminalFileOpenHint()).toBe(
'Click for actions, ⌘+click to open, or ⇧⌘+click for default app'
)
expect(getTerminalHtmlFileOpenHint()).toBe(
'Click for actions, ⌘+click to open, or ⇧⌘+click for default browser'
expect(getTerminalHtmlFileOpenHint({ primary: 'system', alternate: 'orca' })).toBe(
'Click for actions, ⌘+click to open, or ⇧⌘+click to open in Orca'
)
expect(getTerminalHtmlFileOpenHint({ primary: 'orca', alternate: 'system' })).toBe(
'Click for actions, ⌘+click to open, or ⇧⌘+click for system browser'
)
expect(getTerminalUrlOpenHint()).toBe(
'Click for actions, ⌘+click to open, or ⇧⌘+click for system browser'
@@ -237,8 +240,8 @@ describe('handleOscLink', () => {
expect(getTerminalFileOpenHint()).toBe(
'Click for actions, Ctrl+click to open, or Shift+Ctrl+click for default app'
)
expect(getTerminalHtmlFileOpenHint()).toBe(
'Click for actions, Ctrl+click to open, or Shift+Ctrl+click for default browser'
expect(getTerminalHtmlFileOpenHint({ primary: 'system', alternate: 'orca' })).toBe(
'Click for actions, Ctrl+click to open, or Shift+Ctrl+click to open in Orca'
)
expect(getTerminalUrlOpenHint()).toBe(
'Click for actions, Ctrl+click to open, or Shift+Ctrl+click for system browser'
@@ -248,8 +251,8 @@ describe('handleOscLink', () => {
it('omits plain-click actions from hover hints when the popover is disabled', () => {
setPlatform('Macintosh')
expect(getTerminalFileOpenHint(false)).toBe('⌘+click to open, or ⇧⌘+click for default app')
expect(getTerminalHtmlFileOpenHint(false)).toBe(
'⌘+click to open, or ⇧⌘+click for default browser'
expect(getTerminalHtmlFileOpenHint({ primary: 'system', alternate: 'orca' }, false)).toBe(
'⌘+click to open, or ⇧⌘+click to open in Orca'
)
expect(getTerminalUrlOpenHint({ showActions: false })).toBe(
'⌘+click to open, or ⇧⌘+click for system browser'
@@ -14,6 +14,7 @@ import { terminalLogicalLinkHasOscLink } from './terminal-osc-link-cells'
import { isTerminalLinkifierHoverActive } from '@/lib/pane-manager/terminal-linkifier-hover-reset'
import {
buildHttpLinkActions,
httpLinkDirectClickDestination,
openRoutedHttpLink,
type HttpLinkActionDestinations,
type HttpLinkDestination,
@@ -72,9 +73,9 @@ export function handleTerminalHttpLink(
deps: UrlLinkHitTestDeps
): boolean {
if (isTerminalHttpLinkActivation(event)) {
const forceDestination = event?.shiftKey
? (deps.actionDestinations?.alternate ?? deps.actionDestinations?.primary)
: deps.actionDestinations?.primary
const forceDestination =
deps.actionDestinations &&
httpLinkDirectClickDestination(deps.actionDestinations, Boolean(event?.shiftKey))
openRoutedHttpLink(url, {
...deps,
modifierHeld: forceDestination ? false : Boolean(event?.shiftKey),
@@ -42,6 +42,14 @@ export function httpLinkActionDestinationsFor(
: { primary: 'system', alternate: 'orca' }
}
/** A modifier click opens the primary destination; Shift takes the other one. */
export function httpLinkDirectClickDestination(
destinations: HttpLinkActionDestinations,
shiftKey: boolean
): HttpLinkDestination {
return shiftKey ? (destinations.alternate ?? destinations.primary) : destinations.primary
}
export function httpLinkDestinationLabel(destination: HttpLinkDestination): string {
return destination === 'orca'
? translate(