From d2a3f12baa82adfbb30389c3a2b2a300cab439ca Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:43:39 -0700 Subject: [PATCH] fix: address review findings (#5275) --- .../runtime-home-service.test.ts | 1 + src/main/codex-accounts/service.test.ts | 1 + src/main/persistence.ts | 15 + src/renderer/src/App.tsx | 5 +- .../link-routing-preference-dialog.tsx | 260 ++++++++++++++++++ .../settings/BrowserLinkRoutingSetting.tsx | 7 +- .../components/terminal-pane/TerminalPane.tsx | 35 +++ .../terminal-link-handlers.test.ts | 83 +++++- .../terminal-osc-link-routing.ts | 14 +- .../terminal-url-link-hit-testing.ts | 41 ++- .../use-terminal-pane-lifecycle.ts | 21 +- src/renderer/src/i18n/locales/en.json | 38 +++ src/renderer/src/i18n/locales/es.json | 38 +++ src/renderer/src/i18n/locales/ja.json | 38 +++ src/renderer/src/i18n/locales/ko.json | 38 +++ src/renderer/src/i18n/locales/zh.json | 38 +++ .../src/lib/http-link-routing.test.ts | 12 +- src/renderer/src/lib/http-link-routing.ts | 2 +- .../src/lib/workspace-port-actions.ts | 2 +- src/shared/constants.ts | 3 +- src/shared/types.ts | 3 + 21 files changed, 667 insertions(+), 28 deletions(-) create mode 100644 src/renderer/src/components/link-routing-preference-dialog.tsx diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 1096210f8..1630ada4d 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -82,6 +82,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings localAccountRuntime: 'host', localAccountWslDistro: null, openLinksInApp: false, + openLinksInAppPreferencePrompted: false, rightSidebarOpenByDefault: true, sourceControlViewMode: 'list', showTitlebarAppName: true, diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 21f133501..2a00e2a48 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -86,6 +86,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings localAccountRuntime: 'host', localAccountWslDistro: null, openLinksInApp: false, + openLinksInAppPreferencePrompted: false, rightSidebarOpenByDefault: true, sourceControlViewMode: 'list', showTitlebarAppName: true, diff --git a/src/main/persistence.ts b/src/main/persistence.ts index a1e7ea44a..c2a044024 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -2229,6 +2229,14 @@ export class Store { parsed.settings?.disabledTuiAgents ) const migratedAgentYoloDefaults = migrateAgentYoloDefaults(parsed.settings) + const openLinksInAppWasPersisted = Object.prototype.hasOwnProperty.call( + parsed.settings ?? {}, + 'openLinksInApp' + ) + const migratedOpenLinksInAppPreferencePrompted = + typeof parsed.settings?.openLinksInAppPreferencePrompted === 'boolean' + ? parsed.settings.openLinksInAppPreferencePrompted + : openLinksInAppWasPersisted if ( parsed.settings?.agentYoloDefaultsMigrated !== true || hasUnsupportedTuiAgentArgs('opencode', parsed.settings?.agentDefaultArgs?.opencode) || @@ -2245,6 +2253,12 @@ export class Store { if (!autoRenameBranchFromWorkDefaultedOn) { this.loadNeedsSave = true } + if ( + parsed.settings?.openLinksInAppPreferencePrompted !== + migratedOpenLinksInAppPreferencePrompted + ) { + this.loadNeedsSave = true + } const normalizedOnboarding = normalizeLoadedOnboardingState( parsed.onboarding, defaults.onboarding @@ -2322,6 +2336,7 @@ export class Store { openInApplications: normalizeOpenInApplications(parsed.settings?.openInApplications, { seedDefaults: true }), + openLinksInAppPreferencePrompted: migratedOpenLinksInAppPreferencePrompted, notifications: normalizeNotificationSettings(parsed.settings?.notifications), sourceControlAi: migratedSourceControlAi, // Why: new builds read sourceControlAi, but rollback builds still diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index d4a2d1320..f797cf4fc 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -61,6 +61,7 @@ import { WorkspacePortScanner } from './components/ports/WorkspacePortScanner' import { CrashReportDialog } from './components/crash-report/CrashReportDialog' import { RecoverableRenderErrorBoundary } from './components/error-boundaries/RecoverableRenderErrorBoundary' import { ConfirmationDialogProvider } from './components/confirmation-dialog' +import { LinkRoutingPreferenceDialogProvider } from './components/link-routing-preference-dialog' import RecentTabSwitcher from './components/tab-bar/RecentTabSwitcher' import { useGitStatusPolling } from './components/right-sidebar/useGitStatusPolling' import { useEditorExternalWatch } from './hooks/useEditorExternalWatch' @@ -1773,7 +1774,8 @@ function App(): React.JSX.Element { > - + + {/* Why: leaf-mounted retention sync keeps agent-status retention subscriptions from re-rendering the App tree. */} @@ -2287,6 +2289,7 @@ function App(): React.JSX.Element { > + diff --git a/src/renderer/src/components/link-routing-preference-dialog.tsx b/src/renderer/src/components/link-routing-preference-dialog.tsx new file mode 100644 index 000000000..cdf07d629 --- /dev/null +++ b/src/renderer/src/components/link-routing-preference-dialog.tsx @@ -0,0 +1,260 @@ +import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' +import { ExternalLink, Settings } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' +import { Badge } from '@/components/ui/badge' +import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' +import { useAppStore } from '@/store' +import { translate } from '@/i18n/i18n' + +type LinkRoutingPreferenceDialogOptions = { + url?: string + preview?: boolean + openLinksInAppDefault?: boolean +} + +type LinkRoutingPreferenceDialogRequest = { + id: number + options: LinkRoutingPreferenceDialogOptions + resolve: (openInOrca: boolean) => void +} + +type LinkRoutingPreferenceDialogContextValue = ( + options?: LinkRoutingPreferenceDialogOptions +) => Promise + +const PREVIEW_STORAGE_KEY = 'orca.previewLinkRoutingPreferenceDialog' +const PREVIEW_DEFAULT_STORAGE_KEY = `${PREVIEW_STORAGE_KEY}.default` +const LinkRoutingPreferenceDialogContext = + createContext(null) + +function displayHostForUrl(url: string | undefined): string | null { + if (!url) { + return null + } + try { + return new URL(url).host + } catch { + return null + } +} + +export function LinkRoutingPreferenceDialogProvider({ + children +}: { + children: React.ReactNode +}): React.JSX.Element { + const nextIdRef = useRef(0) + const [queue, setQueue] = useState([]) + const activeRequest = queue[0] ?? null + const activeRequestRef = useRef(activeRequest) + const setContextualToursBlockingSurfaceVisible = useAppStore( + (s) => s.setContextualToursBlockingSurfaceVisible + ) + const lastDisplayedRequestRef = useRef(activeRequest) + activeRequestRef.current = activeRequest + if (activeRequest) { + lastDisplayedRequestRef.current = activeRequest + } + // Why: Radix keeps dialog content mounted while closing; keep copy stable during exit animation. + const displayedRequest = activeRequest ?? lastDisplayedRequestRef.current + const displayHost = displayHostForUrl(displayedRequest?.options.url) + const openLinksInAppDefault = displayedRequest?.options.openLinksInAppDefault === true + const isMac = navigator.userAgent.includes('Mac') + const systemBrowserShortcutKeys = isMac ? ['⇧', '⌘'] : ['Shift', 'Ctrl'] + + useEffect(() => { + setContextualToursBlockingSurfaceVisible(activeRequest !== null) + return () => setContextualToursBlockingSurfaceVisible(false) + }, [activeRequest, setContextualToursBlockingSurfaceVisible]) + + const requestPreference = useCallback((options = {}) => { + return new Promise((resolve) => { + const request: LinkRoutingPreferenceDialogRequest = { + id: nextIdRef.current, + options, + resolve + } + nextIdRef.current += 1 + setQueue((currentQueue) => [...currentQueue, request]) + }) + }, []) + + useEffect(() => { + if (!import.meta.env.DEV || typeof window === 'undefined') { + return + } + if (window.sessionStorage.getItem(PREVIEW_STORAGE_KEY) !== '1') { + return + } + const previewDefault = window.sessionStorage.getItem(PREVIEW_DEFAULT_STORAGE_KEY) + window.sessionStorage.removeItem(PREVIEW_STORAGE_KEY) + window.sessionStorage.removeItem(PREVIEW_DEFAULT_STORAGE_KEY) + void requestPreference({ + openLinksInAppDefault: previewDefault === 'orca', + preview: true, + url: 'https://github.com/stablyai/orca/pull/1234' + }) + }, [requestPreference]) + + const settleActiveRequest = useCallback((openInOrca: boolean) => { + const request = activeRequestRef.current + if (!request) { + return + } + request.resolve(openInOrca) + setQueue((currentQueue) => { + if (currentQueue[0]?.id === request.id) { + return currentQueue.slice(1) + } + return currentQueue.filter((queuedRequest) => queuedRequest.id !== request.id) + }) + }, []) + + return ( + + {children} + !open && settleActiveRequest(false)} + > + +
+ +
+ + {translate( + 'auto.components.link.routing.preference.dialog.badge', + 'Terminal link' + )} + + {displayedRequest?.options.preview ? ( + + {translate('auto.components.link.routing.preference.dialog.preview', 'Preview')} + + ) : null} +
+
+ + {openLinksInAppDefault + ? translate( + 'auto.components.link.routing.preference.dialog.keep.title', + "Keep terminal links in Orca's browser?" + ) + : translate( + 'auto.components.link.routing.preference.dialog.title', + "Open terminal links in Orca's browser?" + )} + + + {openLinksInAppDefault + ? translate( + 'auto.components.link.routing.preference.dialog.keep.description', + 'Or use your system browser by default.' + ) + : translate( + 'auto.components.link.routing.preference.dialog.description', + "Use Orca's browser for terminal links, or keep your system browser." + )} + +
+
+
+ +
+ {displayHost ? ( +
+ + {translate('auto.components.link.routing.preference.dialog.link.label', 'Link')} + + + {displayHost} + +
+ ) : null} + +
+ +
+

+ {translate( + 'auto.components.link.routing.preference.dialog.orca.note', + 'Orca can use imported cookies for logged-in sites.' + )} +

+

+ {translate( + 'auto.components.link.routing.preference.dialog.settings.note', + 'Change this later in Settings → Browser.' + )} +

+

+ + {translate( + 'auto.components.link.routing.preference.dialog.shortcut.note.prefix', + 'When links open in Orca,' + )} + + + + {translate( + 'auto.components.link.routing.preference.dialog.shortcut.note.suffix', + 'click opens system browser once.' + )} + +

+
+
+
+ + + + + +
+
+
+ ) +} + +export function useLinkRoutingPreferenceDialog(): LinkRoutingPreferenceDialogContextValue { + const requestPreference = useContext(LinkRoutingPreferenceDialogContext) + if (!requestPreference) { + throw new Error( + 'useLinkRoutingPreferenceDialog must be used inside LinkRoutingPreferenceDialogProvider' + ) + } + return requestPreference +} diff --git a/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx b/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx index bc5fa79a7..6b0885a86 100644 --- a/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx +++ b/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx @@ -42,7 +42,12 @@ export function BrowserLinkRoutingSetting({