diff --git a/src/renderer/src/components/QuickOpen.tsx b/src/renderer/src/components/QuickOpen.tsx index d197094d5..395628c0b 100644 --- a/src/renderer/src/components/QuickOpen.tsx +++ b/src/renderer/src/components/QuickOpen.tsx @@ -1,6 +1,4 @@ -/* oxlint-disable max-lines */ -import React, { useCallback, useDeferredValue, useMemo, useRef, useState } from 'react' -import { AlertTriangle, Check, Copy } from 'lucide-react' +import React, { useCallback, useDeferredValue, useMemo, useState } from 'react' import { useAppStore } from '@/store' import { useActiveWorktree } from '@/store/selectors' import { detectLanguage } from '@/lib/language-detect' @@ -17,38 +15,10 @@ import { prepareQuickOpenFiles, rankQuickOpenFiles } from '@/components/quick-op import { useRuntimeFileListForWorktree } from '@/components/quick-open-file-list' import { useModalReturnFocus } from '@/hooks/useModalReturnFocus' import { translate } from '@/i18n/i18n' - -/** - * Parses the install-ripgrep guidance message produced by the relay's - * buildInstallRgMessage(). Returns the parts needed to render as formatted - * guidance (reason + install command) when matched, or null otherwise so - * callers can fall back to plain-text display. - * - * Why: the message is plain text on the wire (thrown as an Error), but the - * renderer is the only place with enough UI vocabulary to present ripgrep - * as an inline code span and the install command as a copyable code block. - */ -function parseInstallRgGuidance( - message: string -): { reason: string; command: string | null; guidance: string | null } | null { - const match = message.match( - /^Quick Open scan too large \(([^)]+)\)\. Install ripgrep on the remote to enable fast, gitignore-aware listing: (.+)$/ - ) - if (!match) { - return null - } - const reason = match[1] - const tail = match[2].trim() - // Why: on unknown distros the relay emits prose like "install ripgrep via - // your package manager (e.g. apt/dnf/pacman)" — there's no single command - // to copy, so surface it as plain guidance without the code block. - const looksLikeCommand = /^(sudo\s+)?(brew|apt|dnf|pacman|apk)\s/.test(tail) - return { - reason, - command: looksLikeCommand ? tail : null, - guidance: looksLikeCommand ? null : tail - } -} +import { + parseQuickOpenInstallRgGuidance, + QuickOpenInstallRgGuidance +} from '@/components/quick-open-install-rg-guidance' function FooterKey({ children }: { children: React.ReactNode }): React.JSX.Element { return ( @@ -58,109 +28,6 @@ function FooterKey({ children }: { children: React.ReactNode }): React.JSX.Eleme ) } -function InstallRgGuidance({ - reason, - command, - guidance -}: { - reason: string - command: string | null - guidance?: string | null -}): React.JSX.Element { - const [copied, setCopied] = useState(false) - const copiedResetTimerRef = useRef(null) - // Why: clipboard IPC can resolve after this guidance unmounts; avoid - // starting a reset timer that will outlive the component. - const isMountedRef = useRef(false) - - const clearCopiedResetTimer = useCallback((): void => { - if (copiedResetTimerRef.current !== null) { - window.clearTimeout(copiedResetTimerRef.current) - copiedResetTimerRef.current = null - } - }, []) - - const setCopyButtonRef = useCallback( - (node: HTMLButtonElement | null) => { - isMountedRef.current = node !== null - if (node === null) { - clearCopiedResetTimer() - } - }, - [clearCopiedResetTimer] - ) - - const handleCopy = useCallback(() => { - if (!command) { - return - } - // Why: use Electron's clipboard IPC instead of navigator.clipboard — the - // latter often fails silently in the renderer due to focus/permission - // quirks inside Radix dialogs. All other copy buttons in the app go - // through window.api.ui.writeClipboardText for consistency. - void window.api.ui - .writeClipboardText(command) - .then(() => { - if (!isMountedRef.current) { - return - } - clearCopiedResetTimer() - setCopied(true) - copiedResetTimerRef.current = window.setTimeout(() => { - copiedResetTimerRef.current = null - setCopied(false) - }, 1500) - }) - .catch(() => { - /* best-effort */ - }) - }, [clearCopiedResetTimer, command]) - - return ( -
-
-
-

- {translate('auto.components.QuickOpen.2ca749c15d', 'Install')}{' '} - - {translate('auto.components.QuickOpen.5d80dc39bb', 'ripgrep')} - {' '} - {translate( - 'auto.components.QuickOpen.1cf8561ab4', - 'on the remote to enable fast, gitignore-aware listing:' - )} -

- {command ? ( -
- {command} - -
- ) : guidance ? ( -

{guidance}

- ) : null} -
- ) -} - export default function QuickOpen(): React.JSX.Element | null { const visible = useAppStore((s) => s.activeModal === 'quick-open') const closeModal = useAppStore((s) => s.closeModal) @@ -259,9 +126,9 @@ export default function QuickOpen(): React.JSX.Element | null { ) : loadError ? ( (() => { - const guidance = parseInstallRgGuidance(loadError) + const guidance = parseQuickOpenInstallRgGuidance(loadError) return guidance ? ( - (null) + // Why: clipboard IPC can resolve after this guidance unmounts; avoid + // starting a reset timer that will outlive the component. + const isMountedRef = useRef(false) + + const clearCopiedResetTimer = useCallback((): void => { + if (copiedResetTimerRef.current !== null) { + window.clearTimeout(copiedResetTimerRef.current) + copiedResetTimerRef.current = null + } + }, []) + + const setCopyButtonRef = useCallback( + (node: HTMLButtonElement | null) => { + isMountedRef.current = node !== null + if (node === null) { + clearCopiedResetTimer() + } + }, + [clearCopiedResetTimer] + ) + + const handleCopy = useCallback(() => { + if (!command) { + return + } + // Why: use Electron's clipboard IPC instead of navigator.clipboard; the + // latter often fails silently in the renderer due to focus/permission + // quirks inside Radix dialogs. All other copy buttons in the app go + // through window.api.ui.writeClipboardText for consistency. + void window.api.ui + .writeClipboardText(command) + .then(() => { + if (!isMountedRef.current) { + return + } + clearCopiedResetTimer() + setCopied(true) + copiedResetTimerRef.current = window.setTimeout(() => { + copiedResetTimerRef.current = null + setCopied(false) + }, 1500) + }) + .catch(() => { + /* best-effort */ + }) + }, [clearCopiedResetTimer, command]) + + return ( +
+
+
+

+ {translate('auto.components.QuickOpen.2ca749c15d', 'Install')}{' '} + + {translate('auto.components.QuickOpen.5d80dc39bb', 'ripgrep')} + {' '} + {translate( + 'auto.components.QuickOpen.1cf8561ab4', + 'on the remote to enable fast, gitignore-aware listing:' + )} +

+ {command ? ( +
+ {command} + +
+ ) : guidance ? ( +

{guidance}

+ ) : null} +
+ ) +}