From d5825cd912ae52da2fb41d4590e1379f25656e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=99=E5=AD=90?= <113168461+706412584@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:06:46 +0800 Subject: [PATCH] feat(plugins): one-click 'Install all' button for missing prerequisites (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update zh-TW translations * chore: update jp and kr translations * feat(plugins): one-click 'Install all' button for missing prerequisites Builds on PR #8. The prerequisites modal now has a primary 'Install all' button that opens a fresh terminal tab and injects each missing prerequisite's first install command into the PTY one at a time. Implementation: - New helper desktop/src/lib/terminalCommandInjection.ts: injectInstallScriptIntoNewTerminal(commands) opens a terminal tab via useTabStore.openTerminalTab(), waits via subscribeTerminalRuntime for the spawn to bind nativeSessionId, then writes each command + carriage return through terminalApi.write with a 150ms inter-command delay so output stays grouped. Times out at 15s if the spawn never completes (host without terminal capability raises a clear error). - Modal: new 'Install all (N)' button in the footer (only shown when there is at least one row whose install map covers the current platform). Clicking shows a loading spinner; on success, fires an info toast 'N install commands injected — watch them run, then click Recheck'. Errors fall back to a toast pointing at the per-command Copy / Open-in-terminal buttons. - Selects the FIRST install step per platform per row. Plugin authors put the most ergonomic option first (winget on Windows, brew on macOS), so this is the right default. Users can still copy alternate install methods manually. - i18n: 4 new keys x 5 locales (en/zh added here; jp/kr/zh-TW already in by chore commits cf7ab076 + 819eaca4). Why not pipe everything through one shell '&&' chain? Two reasons: (1) a failure mid-chain blocks remaining commands silently — sending each line independently lets the user see every result and fix mid-stream; (2) chained command output collapses into a hard-to-read wall. Tested: bun run lint clean. Confidence: high Scope-risk: narrow --------- Co-authored-by: 你的姓名 --- .../plugins/PluginPrerequisitesModal.tsx | 67 ++++++++- desktop/src/i18n/locales/en.ts | 4 + desktop/src/i18n/locales/jp.ts | 4 + desktop/src/i18n/locales/kr.ts | 4 + desktop/src/i18n/locales/zh-TW.ts | 4 + desktop/src/i18n/locales/zh.ts | 4 + desktop/src/lib/terminalCommandInjection.ts | 134 ++++++++++++++++++ 7 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 desktop/src/lib/terminalCommandInjection.ts diff --git a/desktop/src/components/plugins/PluginPrerequisitesModal.tsx b/desktop/src/components/plugins/PluginPrerequisitesModal.tsx index 5df36fbd..3ebb5f4e 100644 --- a/desktop/src/components/plugins/PluginPrerequisitesModal.tsx +++ b/desktop/src/components/plugins/PluginPrerequisitesModal.tsx @@ -5,6 +5,7 @@ import { useTranslation } from '../../i18n' import { useUIStore } from '../../stores/uiStore' import { useTabStore } from '../../stores/tabStore' import { copyTextToClipboard } from '../chat/clipboard' +import { injectInstallScriptIntoNewTerminal } from '../../lib/terminalCommandInjection' import type { PluginPrerequisiteInstallStep, PluginPrerequisiteRow, @@ -92,12 +93,35 @@ export function PluginPrerequisitesModal({ // Optimistic copy-feedback state, keyed by `${rowIdx}:${stepIdx}`. // Reset on modal close so reopening the modal starts fresh. const [copiedKey, setCopiedKey] = useState(null) + const [installAllRunning, setInstallAllRunning] = useState(false) useEffect(() => { - if (!open) setCopiedKey(null) + if (!open) { + setCopiedKey(null) + setInstallAllRunning(false) + } }, [open]) const missingRows = rows.filter((r) => !r.installed) + // Build the "install all" command list for the current platform. + // Pick the FIRST install step per row — plugin authors put the most + // ergonomic option first (e.g. winget on Windows, brew on macOS). + // A row with no install step for this platform is silently dropped + // — the user still sees the row in the modal with the "no automated + // install for {platform}" hint. + const installAllCommands = useMemo(() => { + const cmds: string[] = [] + for (const row of missingRows) { + const steps = row.install?.[platform] ?? [] + if (steps.length > 0) { + cmds.push(steps[0]!.cmd) + } + } + return cmds + }, [missingRows, platform]) + + const canInstallAll = installAllCommands.length > 0 + const handleCopy = async (cmd: string, key: string) => { const ok = await copyTextToClipboard(cmd) if (!ok) { @@ -131,6 +155,30 @@ export function PluginPrerequisitesModal({ }) } + const handleInstallAll = async () => { + if (installAllRunning || !canInstallAll) return + setInstallAllRunning(true) + try { + const result = await injectInstallScriptIntoNewTerminal(installAllCommands) + addToast({ + type: 'info', + message: t('pluginPrereq.installAllRunningToast', { + count: String(result.commands.length), + }), + duration: 8000, + }) + } catch (err) { + addToast({ + type: 'error', + message: t('pluginPrereq.installAllFailedToast', { + detail: err instanceof Error ? err.message : String(err), + }), + }) + } finally { + setInstallAllRunning(false) + } + } + return ( {t('pluginPrereq.dismiss')} + {canInstallAll && ( + + )}