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; 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 (

{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}
            
)}
); }