mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(links): open local html file links in the browser link routing picks (#32706)
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user