import { useCallback, useEffect, useMemo, useState } from "react"; import { Download, Copy, ExternalLink, Github, Globe, Info, Loader2, RefreshCw, Terminal, CheckCircle2, AlertCircle, ArrowUpCircle, ChevronDown, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { getVersion } from "@tauri-apps/api/app"; import { settingsApi } from "@/lib/api"; import { useUpdate } from "@/contexts/UpdateContext"; 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 { APP_ICON_MAP } from "@/config/appConfig"; import type { AppId } from "@/lib/api/types"; interface AboutSectionProps { isPortable: boolean; } interface ToolVersion { name: string; version: string | null; latest_version: string | null; error: string | null; env_type: "windows" | "wsl" | "macos" | "linux" | "unknown"; wsl_distro: string | null; } 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; wslShellFlag?: string | null; }; const WSL_SHELL_OPTIONS = ["sh", "bash", "zsh", "fish", "dash"] as const; // UI-friendly order: login shell first. const WSL_SHELL_FLAG_OPTIONS = ["-lic", "-lc", "-c"] as const; const ENV_BADGE_CONFIG: Record< string, { labelKey: string; className: string } > = { wsl: { labelKey: "settings.envBadge.wsl", className: "bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20", }, windows: { labelKey: "settings.envBadge.windows", className: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", }, macos: { labelKey: "settings.envBadge.macos", className: "bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/20", }, linux: { labelKey: "settings.envBadge.linux", className: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20", }, }; 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 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) ... const { t } = useTranslation(); const [version, setVersion] = useState(null); const [isLoadingVersion, setIsLoadingVersion] = useState(true); 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, updateInfo, updateHandle, checkUpdate, resetDismiss, isChecking, } = useUpdate(); const [wslShellByTool, setWslShellByTool] = useState< Record >({}); 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[], wslOverrides?: Record, ) => { if (toolNames.length === 0) return; // 单工具刷新使用统一后端入口(get_tool_versions)并带工具过滤。 setLoadingTools((prev) => { const next = { ...prev }; for (const name of toolNames) next[name] = true; return next; }); try { const updated = await settingsApi.getToolVersions( toolNames, wslOverrides, ); setToolVersions((prev) => { if (prev.length === 0) return updated; const byName = new Map(updated.map((t) => [t.name, t])); const merged = prev.map((t) => byName.get(t.name) ?? t); const existing = new Set(prev.map((t) => t.name)); for (const u of updated) { if (!existing.has(u.name)) merged.push(u); } return merged; }); } catch (error) { console.error("[AboutSection] Failed to refresh tools", error); } finally { setLoadingTools((prev) => { const next = { ...prev }; for (const name of toolNames) next[name] = false; return next; }); } }, [], ); const loadAllToolVersions = useCallback(async () => { setIsLoadingTools(true); try { // Respect current UI overrides (shell / flag) when doing a full refresh. const versions = await settingsApi.getToolVersions( [...TOOL_NAMES], wslShellByTool, ); setToolVersions(versions); } catch (error) { console.error("[AboutSection] Failed to load tool versions", error); } finally { setIsLoadingTools(false); } }, [wslShellByTool]); const handleToolShellChange = async (toolName: ToolName, value: string) => { const wslShell = value === "auto" ? null : value; const nextPref: WslShellPreference = { ...(wslShellByTool[toolName] ?? {}), wslShell, }; setWslShellByTool((prev) => ({ ...prev, [toolName]: nextPref })); await refreshToolVersions([toolName], { [toolName]: nextPref }); }; const handleToolShellFlagChange = async ( toolName: ToolName, value: string, ) => { const wslShellFlag = value === "auto" ? null : value; const nextPref: WslShellPreference = { ...(wslShellByTool[toolName] ?? {}), wslShellFlag, }; setWslShellByTool((prev) => ({ ...prev, [toolName]: nextPref })); await refreshToolVersions([toolName], { [toolName]: nextPref }); }; useEffect(() => { let active = true; const load = async () => { try { const [appVersion] = await Promise.all([ getVersion(), loadAllToolVersions(), ]); if (active) { setVersion(appVersion); } } catch (error) { console.error("[AboutSection] Failed to load info", error); if (active) { setVersion(null); } } finally { if (active) { setIsLoadingVersion(false); } } }; void load(); return () => { active = false; }; // Mount-only: loadAllToolVersions is intentionally excluded to avoid // re-fetching all tools whenever wslShellByTool changes. Single-tool // refreshes are handled by refreshToolVersions in the shell/flag handlers. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ... (handlers like handleOpenReleaseNotes, handleCheckUpdate) ... const handleOpenReleaseNotes = useCallback(async () => { try { const targetVersion = updateInfo?.availableVersion ?? version ?? ""; const displayVersion = targetVersion.startsWith("v") ? targetVersion : targetVersion ? `v${targetVersion}` : ""; if (!displayVersion) { await settingsApi.openExternal( "https://github.com/farion1231/cc-switch/releases", ); return; } await settingsApi.openExternal( `https://github.com/farion1231/cc-switch/releases/tag/${displayVersion}`, ); } catch (error) { console.error("[AboutSection] Failed to open release notes", error); toast.error(t("settings.openReleaseNotesFailed")); } }, [t, updateInfo?.availableVersion, version]); const handleCheckUpdate = useCallback(async () => { if (hasUpdate && updateHandle) { if (isPortable) { try { await settingsApi.checkUpdates(); } catch (error) { console.error("[AboutSection] Portable update failed", error); } return; } setIsDownloading(true); try { resetDismiss(); await updateHandle.downloadAndInstall(); await relaunchApp(); } catch (error) { console.error("[AboutSection] Update failed", error); toast.error(t("settings.updateFailed")); try { await settingsApi.checkUpdates(); } catch (fallbackError) { console.error( "[AboutSection] Failed to open fallback updater", fallbackError, ); } } finally { setIsDownloading(false); } return; } try { const available = await checkUpdate(); if (!available) { toast.success(t("settings.upToDate"), { closeButton: true }); } } catch (error) { console.error("[AboutSection] Check update failed", error); toast.error(t("settings.checkUpdateFailed")); } }, [checkUpdate, hasUpdate, isPortable, resetDismiss, t, updateHandle]); const handleCopyInstallCommands = useCallback(async () => { try { await navigator.clipboard.writeText(ONE_CLICK_INSTALL_COMMANDS); toast.success(t("settings.installCommandsCopied"), { closeButton: true }); } catch (error) { console.error("[AboutSection] Failed to copy install commands", error); toast.error(t("settings.installCommandsCopyFailed")); } }, [t]); const handleRunToolAction = useCallback( async (toolNames: ToolName[], action: ToolLifecycleAction) => { if (toolNames.length === 0) return; const isBatch = toolNames.length > 1; if (isBatch) { setBatchAction(action); } // 逐工具串行执行:每个工具独立成败、独立刷新版本,一个失败不会连坐 // 后续工具(后端把整批拼成单脚本 + set -e,会在首个失败处中止整批)。 const failures: { toolName: ToolName; detail: string }[] = []; let succeeded = 0; for (const toolName of toolNames) { setToolActions((prev) => ({ ...prev, [toolName]: action })); try { await settingsApi.runToolLifecycleAction( [toolName], action, wslShellByTool, ); // 静默执行真正结束后刷新该工具版本,卡片立即反映结果。 await refreshToolVersions([toolName], wslShellByTool); succeeded += 1; } catch (error) { console.error( `[AboutSection] Failed to run tool action for ${toolName}`, error, ); const detail = error instanceof Error ? error.message : String(error); failures.push({ toolName, detail }); } finally { setToolActions((prev) => { const next = { ...prev }; delete next[toolName]; return next; }); } } if (isBatch) { setBatchAction(null); } const actionLabel = action === "install" ? t("settings.toolInstall") : t("settings.toolUpdate"); if (failures.length === 0) { toast.success( t("settings.toolActionDone", { count: succeeded, action: actionLabel, }), { closeButton: true }, ); return; } // 批量场景每个失败只摘取错误末行(最相关),单工具场景给出完整详情。 const lastLine = (text: string) => { const lines = text.trim().split("\n").filter(Boolean); return lines[lines.length - 1] ?? text; }; const failureDescription = isBatch ? failures .map( (f) => `${TOOL_DISPLAY_NAMES[f.toolName]}: ${lastLine(f.detail)}`, ) .join("\n") : failures[0]?.detail; if (succeeded === 0) { toast.error(t("settings.toolActionFailed"), { description: failureDescription || undefined, closeButton: true, }); } else { // 部分成功:用 warning 汇总成败数量,详情列出失败的工具。 toast.warning( t("settings.toolActionPartial", { succeeded, failed: failures.length, action: actionLabel, }), { description: failureDescription || undefined, closeButton: true }, ); } }, [t, wslShellByTool, refreshToolVersions], ); const displayVersion = version ?? t("common.unknown"); // 任一安装/升级进行中(批量或单工具)即视为忙碌:用于禁用所有操作按钮, // 避免并发触发多个 npm/pip 全局写入造成冲突。 const isAnyBusy = Boolean(batchAction) || Object.keys(toolActions).length > 0; return (

{t("common.about")}

{t("settings.aboutHint")}

CC Switch

CC Switch

{t("common.version")} {isLoadingVersion ? ( ) : ( {`v${displayVersion}`} )} {isPortable && ( {t("settings.portableMode")} )}
{hasUpdate && updateInfo && (

{t("settings.updateAvailable", { version: updateInfo.availableVersion, })}

{updateInfo.notes && (

{updateInfo.notes}

)}
)}

{t("settings.localEnvCheck")}

{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 && (

{t("settings.oneClickInstallHint")}

              {ONE_CLICK_INSTALL_COMMANDS}
            
)}
); }