diff --git a/src/components/settings/AboutSection.tsx b/src/components/settings/AboutSection.tsx index bdd72505b..bd67fce3a 100644 --- a/src/components/settings/AboutSection.tsx +++ b/src/components/settings/AboutSection.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Download, Copy, @@ -11,6 +11,8 @@ import { Terminal, CheckCircle2, AlertCircle, + ArrowUpCircle, + ChevronDown, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -29,7 +31,8 @@ import { relaunchApp } from "@/lib/updater"; import { Badge } from "@/components/ui/badge"; import { motion } from "framer-motion"; import appIcon from "@/assets/icons/app-icon.png"; -import { isWindows } from "@/lib/platform"; +import { APP_ICON_MAP } from "@/config/appConfig"; +import type { AppId } from "@/lib/api/types"; interface AboutSectionProps { isPortable: boolean; @@ -44,8 +47,16 @@ interface ToolVersion { wsl_distro: string | null; } -const TOOL_NAMES = ["claude", "codex", "gemini", "opencode"] as const; +const TOOL_NAMES = [ + "claude", + "codex", + "gemini", + "opencode", + "openclaw", + "hermes", +] as const; type ToolName = (typeof TOOL_NAMES)[number]; +type ToolLifecycleAction = "install" | "update"; type WslShellPreference = { wslShell?: string | null; @@ -82,14 +93,36 @@ const ENV_BADGE_CONFIG: Record< }, }; -const ONE_CLICK_INSTALL_COMMANDS = `# Claude Code (Native install - recommended) -curl -fsSL https://claude.ai/install.sh | bash +const ONE_CLICK_INSTALL_COMMANDS = `# Claude Code +npm i -g @anthropic-ai/claude-code@latest # Codex npm i -g @openai/codex@latest # Gemini CLI npm i -g @google/gemini-cli@latest # OpenCode -curl -fsSL https://opencode.ai/install | bash`; +npm i -g opencode-ai@latest +# OpenClaw +npm i -g openclaw@latest +# Hermes +python3 -m pip install --upgrade "hermes-agent[web]"`; + +const TOOL_DISPLAY_NAMES: Record = { + claude: "Claude Code", + codex: "Codex", + gemini: "Gemini CLI", + opencode: "OpenCode", + openclaw: "OpenClaw", + hermes: "Hermes", +}; + +const TOOL_APP_IDS: Record = { + claude: "claude", + codex: "codex", + gemini: "gemini", + opencode: "opencode", + openclaw: "openclaw", + hermes: "hermes", +}; export function AboutSection({ isPortable }: AboutSectionProps) { // ... (use hooks as before) ... @@ -99,6 +132,13 @@ export function AboutSection({ isPortable }: AboutSectionProps) { const [isDownloading, setIsDownloading] = useState(false); const [toolVersions, setToolVersions] = useState([]); const [isLoadingTools, setIsLoadingTools] = useState(true); + const [toolActions, setToolActions] = useState< + Partial> + >({}); + const [batchAction, setBatchAction] = useState( + null, + ); + const [showInstallCommands, setShowInstallCommands] = useState(false); const { hasUpdate, @@ -114,6 +154,23 @@ export function AboutSection({ isPortable }: AboutSectionProps) { >({}); const [loadingTools, setLoadingTools] = useState>({}); + const toolVersionByName = useMemo(() => { + return new Map(toolVersions.map((tool) => [tool.name, tool])); + }, [toolVersions]); + + const updatableToolNames = useMemo( + () => + TOOL_NAMES.filter((toolName) => { + const tool = toolVersionByName.get(toolName); + return Boolean( + tool?.version && + tool.latest_version && + tool.version !== tool.latest_version, + ); + }), + [toolVersionByName], + ); + const refreshToolVersions = useCallback( async ( toolNames: ToolName[], @@ -202,7 +259,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) { try { const [appVersion] = await Promise.all([ getVersion(), - ...(isWindows() ? [] : [loadAllToolVersions()]), + loadAllToolVersions(), ]); if (active) { @@ -311,6 +368,63 @@ export function AboutSection({ isPortable }: AboutSectionProps) { } }, [t]); + const handleRunToolAction = useCallback( + async (toolNames: ToolName[], action: ToolLifecycleAction) => { + if (toolNames.length === 0) return; + + setToolActions((prev) => { + const next = { ...prev }; + for (const toolName of toolNames) { + next[toolName] = action; + } + return next; + }); + if (toolNames.length > 1) { + setBatchAction(action); + } + + try { + await settingsApi.runToolLifecycleAction( + [...toolNames], + action, + wslShellByTool, + ); + // 静默执行已真正结束:刷新对应工具的版本号,让卡片立即反映安装结果。 + await refreshToolVersions([...toolNames], wslShellByTool); + toast.success( + t("settings.toolActionDone", { + count: toolNames.length, + action: + action === "install" + ? t("settings.toolInstall") + : t("settings.toolUpdate"), + }), + { closeButton: true }, + ); + } catch (error) { + console.error("[AboutSection] Failed to run tool action", error); + // 后端在命令失败时回传 stderr 末尾若干行,作为 toast 详情提示用户。 + const detail = error instanceof Error ? error.message : String(error); + toast.error(t("settings.toolActionFailed"), { + description: detail || undefined, + closeButton: true, + }); + } finally { + setToolActions((prev) => { + const next = { ...prev }; + for (const toolName of toolNames) { + delete next[toolName]; + } + return next; + }); + if (toolNames.length > 1) { + setBatchAction(null); + } + } + }, + [t, wslShellByTool, refreshToolVersions], + ); + const displayVersion = version ?? t("common.unknown"); return ( @@ -450,18 +564,16 @@ export function AboutSection({ isPortable }: AboutSectionProps) { )} - {!isWindows() && ( -
-
-

- {t("settings.localEnvCheck")} -

+
+
+

{t("settings.localEnvCheck")}

+
-
- -
- {TOOL_NAMES.map((toolName, index) => { - const tool = toolVersions.find((item) => item.name === toolName); - // Special case for OpenCode (capital C), others use capitalize - const displayName = - toolName === "opencode" - ? "OpenCode" - : toolName.charAt(0).toUpperCase() + toolName.slice(1); - const title = tool?.version || tool?.error || t("common.unknown"); - - return ( - -
-
- - {displayName} - {/* Environment Badge */} - {tool?.env_type && ENV_BADGE_CONFIG[tool.env_type] && ( - - {t(ENV_BADGE_CONFIG[tool.env_type].labelKey)} - - )} - {/* WSL Shell Selector */} - {tool?.env_type === "wsl" && ( - - )} - {/* WSL Shell Flag Selector */} - {tool?.env_type === "wsl" && ( - - )} -
- {isLoadingTools || loadingTools[toolName] ? ( - - ) : tool?.version ? ( - tool.latest_version && - tool.version !== tool.latest_version ? ( - - {tool.latest_version} - - ) : ( - - ) - ) : ( - - )} -
-
- {isLoadingTools - ? t("common.loading") - : tool?.version - ? tool.version - : tool?.error || t("common.notInstalled")} -
-
- ); - })} +
- )} - {!isWindows() && ( - + {TOOL_NAMES.map((toolName, index) => { + const tool = toolVersionByName.get(toolName); + const appConfig = APP_ICON_MAP[TOOL_APP_IDS[toolName]]; + const displayName = TOOL_DISPLAY_NAMES[toolName]; + const isToolVersionLoading = + isLoadingTools || Boolean(loadingTools[toolName]); + const isOutdated = Boolean( + tool?.version && + tool.latest_version && + tool.version !== tool.latest_version, + ); + const action = isToolVersionLoading + ? null + : !tool?.version + ? "install" + : isOutdated + ? "update" + : null; + const runningAction = toolActions[toolName]; + const title = tool?.version || tool?.error || t("common.unknown"); + + return ( + +
+
+ + {appConfig?.icon ?? } + +
+
+ {displayName} +
+ {tool?.env_type && ENV_BADGE_CONFIG[tool.env_type] && ( + + {t(ENV_BADGE_CONFIG[tool.env_type].labelKey)} + {tool.wsl_distro ? ` · ${tool.wsl_distro}` : ""} + + )} +
+
+ {isToolVersionLoading ? ( + + ) : tool?.version ? ( + isOutdated ? ( + + {t("settings.updateAvailableShort")} + + ) : ( + + ) + ) : ( + + )} +
+ +
+
+ + {t("settings.currentVersion")} + + + {isToolVersionLoading + ? t("common.loading") + : tool?.version || t("common.notInstalled")} + +
+
+ + {t("settings.latestVersion")} + + + {isToolVersionLoading + ? t("common.loading") + : tool?.latest_version || t("common.unknown")} + +
+ {!isToolVersionLoading && !tool?.version && tool?.error && ( +
+ {tool.error} +
+ )} +
+ + {tool?.env_type === "wsl" && ( +
+ + +
+ )} + +
+ {isToolVersionLoading ? ( + + {t("common.loading")} + + ) : action ? ( + + ) : ( + + {t("settings.toolReady")} + + )} +
+
+ ); + })} +
+
+ + + + {showInstallCommands && (

@@ -615,8 +821,8 @@ export function AboutSection({ isPortable }: AboutSectionProps) { {ONE_CLICK_INSTALL_COMMANDS}

- - )} + )} + ); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9c3ef2579..79e36f7ba 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -659,9 +659,18 @@ "viewCurrentReleaseNotes": "View current version release notes", "officialWebsite": "Official Website", "github": "GitHub", - "oneClickInstall": "One-click Install", - "oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI / OpenCode", + "manualInstallCommands": "Manual Install Commands", + "oneClickInstallHint": "Install or upgrade Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Hermes", "localEnvCheck": "Local environment check", + "updateAllTools": "Update All ({{count}})", + "currentVersion": "Current Version", + "latestVersion": "Latest Version", + "updateAvailableShort": "Update", + "toolInstall": "Install", + "toolUpdate": "Update", + "toolReady": "Ready", + "toolActionDone": "{{action}} completed for {{count}} tool(s)", + "toolActionFailed": "Install/update command failed", "envBadge": { "wsl": "WSL", "windows": "Win", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index b180722b8..85cc6c620 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -659,9 +659,18 @@ "viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る", "officialWebsite": "公式サイト", "github": "GitHub", - "oneClickInstall": "ワンクリックインストール", - "oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode をインストール", + "manualInstallCommands": "手動インストールコマンド", + "oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Hermes をインストールまたは更新", "localEnvCheck": "ローカル環境チェック", + "updateAllTools": "すべて更新({{count}})", + "currentVersion": "現在のバージョン", + "latestVersion": "最新バージョン", + "updateAvailableShort": "更新あり", + "toolInstall": "インストール", + "toolUpdate": "更新", + "toolReady": "準備完了", + "toolActionDone": "{{count}} 個のツールの{{action}}が完了しました", + "toolActionFailed": "インストール/更新コマンドの実行に失敗しました", "envBadge": { "wsl": "WSL", "windows": "Win", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index a5e1e5520..f8492661b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -659,9 +659,18 @@ "viewCurrentReleaseNotes": "查看当前版本更新日志", "officialWebsite": "官方网站", "github": "GitHub", - "oneClickInstall": "一键安装", - "oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI / OpenCode", + "manualInstallCommands": "手动安装命令", + "oneClickInstallHint": "安装或升级 Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Hermes", "localEnvCheck": "本地环境检查", + "updateAllTools": "全部升级({{count}})", + "currentVersion": "当前版本", + "latestVersion": "最新版本", + "updateAvailableShort": "可升级", + "toolInstall": "安装", + "toolUpdate": "升级", + "toolReady": "已就绪", + "toolActionDone": "{{count}} 个工具{{action}}完成", + "toolActionFailed": "安装/升级命令执行失败", "envBadge": { "wsl": "WSL", "windows": "Win",