diff --git a/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts b/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts index 4c0f0ca967c..87289fbdbbd 100644 --- a/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts +++ b/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts @@ -129,6 +129,7 @@ export function handleRichMarkdownEditorClick({ runtimeEnvironmentId, sourceOwner, settings, + worktreeId, worktreeRoot }) return true @@ -184,6 +185,7 @@ function getClickedLinkHref(view: EditorView, pos: number): string { function openMarkdownLinkInClientOs({ href, filePath, + worktreeId, worktreeRoot, runtimeEnvironmentId, sourceOwner, @@ -191,6 +193,7 @@ function openMarkdownLinkInClientOs({ }: { href: string filePath: string + worktreeId: string worktreeRoot: string | null runtimeEnvironmentId?: string | null sourceOwner: HttpLinkSourceOwner @@ -204,7 +207,9 @@ function openMarkdownLinkInClientOs({ return } if (classified.kind === 'external') { - openHttpLink(classified.url, { forceSystemBrowser: true, sourceOwner }) + // Why: same escape hatch as the markdown preview of this very file — routing must + // not depend on which of the two views the link was clicked in. + openHttpLink(classified.url, { worktreeId, modifierHeld: true, sourceOwner }) return } if (classified.kind === 'anchor') { diff --git a/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx b/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx index 106054a9d6a..aeb8f4151d8 100644 --- a/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx +++ b/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx @@ -1,5 +1,4 @@ import type { GlobalSettings } from '../../../../shared/types' -import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' import { SettingsSwitchRow } from './SettingsFormControls' import { @@ -48,10 +47,6 @@ export function BrowserLinkRoutingModifierSetting({ openLinksInAppModifierInverts: settings.openLinksInAppModifierInverts !== true }) } - ariaLabel={translate( - 'auto.components.settings.BrowserLinkRoutingModifierSetting.toggleLabel', - 'Toggle inverted link routing modifier' - )} /> diff --git a/src/renderer/src/components/settings/BrowserPane.tsx b/src/renderer/src/components/settings/BrowserPane.tsx index c030cffd912..716393a4953 100644 --- a/src/renderer/src/components/settings/BrowserPane.tsx +++ b/src/renderer/src/components/settings/BrowserPane.tsx @@ -107,7 +107,7 @@ export function BrowserPane({ const showCookies = matchesSettingsSearch(searchQuery, [getBrowserPaneSearchEntries()[6]]) const showBrowserUse = matchesSettingsSearch(searchQuery, getBrowserUsePaneSearchEntries()) const isMac = isMacUserAgent() - const linkRoutingDescription = getBrowserLinkRoutingDescription({ isMac }) + const linkRoutingDescription = getBrowserLinkRoutingDescription() const hostLabelOverrides = useMemo(() => getHostDisplayLabelOverrides(settings), [settings]) const browserSessionHostOptions = useMemo( () => diff --git a/src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts b/src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts index b8f6e188c48..1151d3e70e1 100644 --- a/src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts +++ b/src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts @@ -3,7 +3,8 @@ import { translate } from '@/i18n/i18n' /** * Title and description both name the destination the modifier reaches, which is * the opposite of wherever Link Routing points. Kept out of the component so the - * settings-search index and the component render identical strings. + * settings-search index can reuse the same strings; the index pins openLinksInApp + * to false, so it also indexes the other title as a keyword. */ export function getLinkRoutingModifierTitle(openLinksInApp: boolean): string { return openLinksInApp @@ -17,6 +18,8 @@ export function getLinkRoutingModifierTitle(openLinksInApp: boolean): string { ) } +// Why: the Orca branch is enabled-state copy — with the toggle off the chord +// still lands on the system browser, so it must not promise Orca in present tense. export function getLinkRoutingModifierDescription({ openLinksInApp, isMac @@ -33,7 +36,7 @@ export function getLinkRoutingModifierDescription({ ) : translate( 'auto.components.settings.BrowserLinkRoutingModifierSetting.descriptionOrca', - "Links open in your system browser, so {{chord}}+click opens one in Orca's built-in browser instead.", + "Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead.", { chord } ) } diff --git a/src/renderer/src/components/settings/browser-search.test.ts b/src/renderer/src/components/settings/browser-search.test.ts index 6eaa408e93b..cd53de69030 100644 --- a/src/renderer/src/components/settings/browser-search.test.ts +++ b/src/renderer/src/components/settings/browser-search.test.ts @@ -1,26 +1,16 @@ import { describe, expect, it } from 'vitest' -import { - getBrowserLinkRoutingDescription, - getBrowserLinkRoutingShortcutLabel, - getBrowserPaneSearchEntries -} from './browser-search' +import { getBrowserLinkRoutingDescription, getBrowserPaneSearchEntries } from './browser-search' import { getLinkRoutingModifierDescription, getLinkRoutingModifierTitle } from './browser-link-routing-modifier-copy' describe('browser settings search copy', () => { - it('uses macOS shortcut symbols for Link Routing copy and search metadata', () => { - expect(getBrowserLinkRoutingShortcutLabel({ isMac: true })).toBe('⇧⌘-click') - - const description = getBrowserLinkRoutingDescription({ isMac: true }) - expect(description).toContain('⇧⌘-click') - expect(description).not.toContain('Cmd/Ctrl') - + it('uses macOS shortcut keywords for Link Routing search metadata', () => { const linkRoutingEntry = getBrowserPaneSearchEntries({ isMac: true }).find( (entry) => entry.title === 'Link Routing' ) - expect(linkRoutingEntry?.description).toBe(description) + expect(linkRoutingEntry?.description).toBe(getBrowserLinkRoutingDescription()) expect(linkRoutingEntry?.keywords).toContain('cmd') expect(linkRoutingEntry?.keywords).not.toContain('ctrl') @@ -30,20 +20,22 @@ describe('browser settings search copy', () => { expect(defaultZoomEntry?.keywords).toContain('zoom') }) - it('uses Ctrl shortcut text for Link Routing copy and search metadata off macOS', () => { - expect(getBrowserLinkRoutingShortcutLabel({ isMac: false })).toBe('Shift+Ctrl+click') - - const description = getBrowserLinkRoutingDescription({ isMac: false }) - expect(description).toContain('Shift+Ctrl+click') - expect(description).not.toContain('Cmd/Ctrl') - + it('uses Ctrl shortcut keywords for Link Routing search metadata off macOS', () => { const linkRoutingEntry = getBrowserPaneSearchEntries({ isMac: false }).find( (entry) => entry.title === 'Link Routing' ) - expect(linkRoutingEntry?.description).toBe(description) + expect(linkRoutingEntry?.description).toBe(getBrowserLinkRoutingDescription()) expect(linkRoutingEntry?.keywords).toContain('ctrl') expect(linkRoutingEntry?.keywords).not.toContain('cmd') }) + + // Why: the modifier can now invert, so no fixed sentence here is true in every + // state — the nested row owns that claim and states the live destination. + it('leaves the modifier claim to the nested row', () => { + const description = getBrowserLinkRoutingDescription() + expect(description).not.toContain('click') + expect(description).not.toContain('system browser') + }) }) describe('browser link routing modifier copy', () => { @@ -83,4 +75,19 @@ describe('browser link routing modifier copy', () => { 'system browser' ) }) + + // Why: the toggle is off by default, so present-tense "opens one in Orca" would + // describe behavior the user does not have yet. + it('phrases the Orca branch as enabled-state copy', () => { + expect(getLinkRoutingModifierDescription({ openLinksInApp: false, isMac: true })).toContain( + 'When enabled' + ) + }) + + // Why: the entry is built with openLinksInApp false, so without this the row is + // unfindable by the title it actually renders when Link Routing is on. + it('indexes both titles so the row is findable in either routing state', () => { + const entry = getBrowserPaneSearchEntries({ isMac: true })[4] + expect(entry?.keywords).toContain(getLinkRoutingModifierTitle(true)) + }) }) diff --git a/src/renderer/src/components/settings/browser-search.ts b/src/renderer/src/components/settings/browser-search.ts index f34fdb8a6de..8e656ca3cbb 100644 --- a/src/renderer/src/components/settings/browser-search.ts +++ b/src/renderer/src/components/settings/browser-search.ts @@ -16,15 +16,13 @@ function getDefaultBrowserShortcutPlatform(): BrowserShortcutPlatform { } } -export function getBrowserLinkRoutingShortcutLabel(platform: BrowserShortcutPlatform): string { - return platform.isMac ? '⇧⌘-click' : 'Shift+Ctrl+click' -} - -// Why: the modifier sentence moved to the nested "Hold Shift to open in …" row — -// once that row can invert the routing, "always uses your system browser" is no -// longer true, and the child row states the live destination instead. -export function getBrowserLinkRoutingDescription(platform: BrowserShortcutPlatform): string { - return `Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor. ${getBrowserLinkRoutingShortcutLabel(platform)} opens a link the other way.` +// Why: the modifier sentence moved to the nested "Hold Shift to open in …" row. +// Once that row can invert the routing, no single sentence here is true in every +// state — "always uses your system browser" breaks when inverting is on, and +// "opens a link the other way" breaks in the default state where it is off. The +// chord is now stated only by the child row, which knows the live destination. +export function getBrowserLinkRoutingDescription(): string { + return "Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor." } export function getBrowserPaneSearchEntries( @@ -99,7 +97,7 @@ export function getBrowserPaneSearchEntries( }, { title: translate('auto.components.settings.browser.search.5cb082b3e3', 'Link Routing'), - description: getBrowserLinkRoutingDescription(platform), + description: getBrowserLinkRoutingDescription(), keywords: [ ...translateSearchKeyword('auto.components.settings.browser.search.2d2d995c58', 'browser'), ...translateSearchKeyword('auto.components.settings.browser.search.44d14df30d', 'preview'), @@ -142,6 +140,9 @@ export function getBrowserPaneSearchEntries( 'auto.components.settings.browser.search.linkRoutingModifier.opposite', 'opposite' ), + // Why: the row renders live copy but this entry is built with openLinksInApp + // false, so index the other title too or the row is unfindable by its own text. + getLinkRoutingModifierTitle(true), platform.isMac ? 'cmd' : 'ctrl' ] }, diff --git a/src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts b/src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts index 4d802efed52..656db8d4bfd 100644 --- a/src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { getTerminalUrlOpenHint } from './terminal-link-open-hints' +import { getTerminalUrlOpenHint, terminalUrlOpenHintOptionsFor } from './terminal-link-open-hints' function stubPlatform(isMac: boolean): void { vi.stubGlobal('navigator', { userAgent: isMac ? 'Mac OS X' : 'Windows NT 10.0' }) @@ -45,3 +45,45 @@ describe('getTerminalUrlOpenHint', () => { ) }) }) + +describe('terminalUrlOpenHintOptionsFor', () => { + it('reports inversion when links open externally on a local runtime', () => { + expect( + terminalUrlOpenHintOptionsFor({ + openLinksInApp: false, + openLinksInAppModifierInverts: true + }) + ).toEqual({ openLinksInApp: false, modifierInverts: true }) + }) + + // Why: openHttpLink refuses to route a remote-owned URL into Orca, so promising + // "open in Orca" there would advertise a click that lands somewhere else. + it('drops inversion while a remote runtime is active', () => { + stubPlatform(true) + const options = terminalUrlOpenHintOptionsFor({ + openLinksInApp: false, + openLinksInAppModifierInverts: true, + activeRuntimeEnvironmentId: 'remote-1' + }) + + expect(options.modifierInverts).toBe(false) + expect(getTerminalUrlOpenHint(options)).toContain('for system browser') + }) + + it('ignores a blank runtime id', () => { + expect( + terminalUrlOpenHintOptionsFor({ + openLinksInApp: false, + openLinksInAppModifierInverts: true, + activeRuntimeEnvironmentId: ' ' + }).modifierInverts + ).toBe(true) + }) + + it('tolerates missing settings', () => { + expect(terminalUrlOpenHintOptionsFor(null)).toEqual({ + openLinksInApp: false, + modifierInverts: false + }) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/terminal-link-open-hints.ts b/src/renderer/src/components/terminal-pane/terminal-link-open-hints.ts index 3ecb4f9dbb4..6316b4b7c26 100644 --- a/src/renderer/src/components/terminal-pane/terminal-link-open-hints.ts +++ b/src/renderer/src/components/terminal-pane/terminal-link-open-hints.ts @@ -25,6 +25,26 @@ export type TerminalUrlOpenHintOptions = { modifierInverts?: boolean } +// Why: openHttpLink only routes to Orca when the source is local, so a remote runtime +// pins every link to the system browser and inverting cannot reach Orca there. +export function terminalUrlOpenHintOptionsFor( + settings: + | { + openLinksInApp?: boolean + openLinksInAppModifierInverts?: boolean + activeRuntimeEnvironmentId?: string | null + } + | null + | undefined +): TerminalUrlOpenHintOptions { + return { + openLinksInApp: settings?.openLinksInApp === true, + modifierInverts: + settings?.openLinksInAppModifierInverts === true && + !settings?.activeRuntimeEnvironmentId?.trim() + } +} + // Why: with modifierInverts on, Shift no longer always means "system browser" — // it means "the other one" — so the hint has to name the actual destination. export function getTerminalUrlOpenHint(options: TerminalUrlOpenHintOptions = {}): string { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts index dce481daec7..810a69dfe56 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts @@ -31,6 +31,7 @@ import { getTerminalUrlOpenHint, installFilePathLinkClickFallback } from './terminal-link-handlers' +import { terminalUrlOpenHintOptionsFor } from './terminal-link-open-hints' import { createTerminalHandleLinkProvider } from './terminal-handle-links' import type { LinkHandlerDeps } from './terminal-link-handlers' import { handleOscLink } from './terminal-osc-link-routing' @@ -780,10 +781,7 @@ export function useTerminalPaneLifecycle({ const fileOpenLinkHint = getTerminalFileOpenHint() // Why: read settingsRef at fire time so toggling link routing applies without recreating panes. const getUrlOpenLinkHint = (): string => - getTerminalUrlOpenHint({ - openLinksInApp: settingsRef.current?.openLinksInApp === true, - modifierInverts: settingsRef.current?.openLinksInAppModifierInverts === true - }) + getTerminalUrlOpenHint(terminalUrlOpenHintOptionsFor(settingsRef.current)) const osc7UncHost = extractUncHost(startupCwd) let releaseWebviewDragPassthrough: (() => void) | null = null diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 54d17a4b09e..c5a8248af14 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -7603,6 +7603,12 @@ "8dd4805991": "file", "68d1db8929": "markdown", "90425d313c": "shift", + "linkRoutingModifier": { + "routing": "routing", + "modifier": "modifier", + "invert": "invert", + "opposite": "opposite" + }, "72c58f7792": "webview", "82ba1c80ea": "localhost", "bea27bac4b": "links", @@ -9747,11 +9753,10 @@ "legacyHelp": "Update this server manually once to enable remote updates." }, "BrowserLinkRoutingModifierSetting": { - "toggleLabel": "Toggle inverted link routing modifier", "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", "descriptionSystem": "Links open in Orca, so {{chord}}+click sends one to your system browser instead.", - "descriptionOrca": "Links open in your system browser, so {{chord}}+click opens one in Orca's built-in browser instead." + "descriptionOrca": "Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead." } }, "right": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 7b050ab634b..90da0f05c3b 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -7543,6 +7543,12 @@ "8dd4805991": "archivo", "68d1db8929": "markdown", "90425d313c": "cambio", + "linkRoutingModifier": { + "routing": "enrutamiento", + "modifier": "modificador", + "invert": "invertir", + "opposite": "opuesto" + }, "72c58f7792": "vista web", "82ba1c80ea": "localhost", "bea27bac4b": "enlaces", @@ -9724,11 +9730,10 @@ "legacyHelp": "Update this server manually once to enable remote updates." }, "BrowserLinkRoutingModifierSetting": { - "toggleLabel": "Toggle inverted link routing modifier", "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", "descriptionSystem": "Links open in Orca, so {{chord}}+click sends one to your system browser instead.", - "descriptionOrca": "Links open in your system browser, so {{chord}}+click opens one in Orca's built-in browser instead." + "descriptionOrca": "Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead." } }, "right": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 0ddc7a69638..1a8bab0a15a 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -7565,6 +7565,12 @@ "8dd4805991": "ファイル", "68d1db8929": "markdown", "90425d313c": "シフト", + "linkRoutingModifier": { + "routing": "ルーティング", + "modifier": "修飾キー", + "invert": "反転", + "opposite": "逆" + }, "72c58f7792": "ウェブビュー", "82ba1c80ea": "ローカルホスト", "bea27bac4b": "リンク", @@ -9724,11 +9730,10 @@ "legacyHelp": "Update this server manually once to enable remote updates." }, "BrowserLinkRoutingModifierSetting": { - "toggleLabel": "Toggle inverted link routing modifier", "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", "descriptionSystem": "Links open in Orca, so {{chord}}+click sends one to your system browser instead.", - "descriptionOrca": "Links open in your system browser, so {{chord}}+click opens one in Orca's built-in browser instead." + "descriptionOrca": "Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead." } }, "right": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 60583855af3..4039c4fd403 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -7528,6 +7528,12 @@ "8dd4805991": "파일", "68d1db8929": "markdown", "90425d313c": "Shift", + "linkRoutingModifier": { + "routing": "라우팅", + "modifier": "수정자", + "invert": "반전", + "opposite": "반대" + }, "72c58f7792": "웹뷰", "82ba1c80ea": "로컬호스트", "bea27bac4b": "링크", @@ -9724,11 +9730,10 @@ "legacyHelp": "Update this server manually once to enable remote updates." }, "BrowserLinkRoutingModifierSetting": { - "toggleLabel": "Toggle inverted link routing modifier", "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", "descriptionSystem": "Links open in Orca, so {{chord}}+click sends one to your system browser instead.", - "descriptionOrca": "Links open in your system browser, so {{chord}}+click opens one in Orca's built-in browser instead." + "descriptionOrca": "Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead." } }, "right": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 706ae0f3483..192b53fccb8 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -7528,6 +7528,12 @@ "8dd4805991": "文件", "68d1db8929": "markdown", "90425d313c": "转移", + "linkRoutingModifier": { + "routing": "路由", + "modifier": "修饰键", + "invert": "反转", + "opposite": "相反" + }, "72c58f7792": "网页视图", "82ba1c80ea": "本地主机", "bea27bac4b": "链接", @@ -9724,11 +9730,10 @@ "legacyHelp": "Update this server manually once to enable remote updates." }, "BrowserLinkRoutingModifierSetting": { - "toggleLabel": "Toggle inverted link routing modifier", "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", "descriptionSystem": "Links open in Orca, so {{chord}}+click sends one to your system browser instead.", - "descriptionOrca": "Links open in your system browser, so {{chord}}+click opens one in Orca's built-in browser instead." + "descriptionOrca": "Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead." } }, "right": { diff --git a/src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts b/src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts index d4aa34759bd..4c57b61d1bb 100644 --- a/src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts @@ -111,6 +111,29 @@ describe('createPaneDOM link tooltips', () => { ) }) + // Why: the only PaneManager consumer passes linkOpenHint, so the hardcoded fallback + // is unreachable in production — without this, dropping the option stays green. + it('re-resolves the caller hint on every hover so setting changes apply live', () => { + const leafId = '11111111-1111-4111-8111-111111111111' as TerminalLeafId + let hint = 'first hint' + const pane = createPaneDOM( + 1, + leafId, + { linkOpenHint: () => hint }, + { active: null } as never, + {} as never, + vi.fn(), + vi.fn() + ) + + webLinksAddonMock.options?.hover?.({} as MouseEvent, 'http://localhost:5180/') + expect(pane.linkTooltip.textContent).toBe('http://localhost:5180/ (first hint)') + + hint = 'second hint' + webLinksAddonMock.options?.hover?.({} as MouseEvent, 'http://localhost:5180/') + expect(pane.linkTooltip.textContent).toBe('http://localhost:5180/ (second hint)') + }) + it('lets callers replace WebLinks hover text for display-only labels', async () => { const labeledText = 'http://main.orca.localhost:60016/ (localhost:5180; click to open)' const leafId = '11111111-1111-4111-8111-111111111111' as TerminalLeafId