import { useCallback, useEffect, useState } from "react"; import { Download, Copy, ExternalLink, Info, Loader2, RefreshCw, Terminal, CheckCircle2, AlertCircle, } 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"; 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"] as const; type ToolName = (typeof TOOL_NAMES)[number]; 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 (Native install - recommended) curl -fsSL https://claude.ai/install.sh | bash # Codex npm i -g @openai/codex@latest # Gemini CLI npm i -g @google/gemini-cli@latest # OpenCode curl -fsSL https://opencode.ai/install | bash`; 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 { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss, isChecking, } = useUpdate(); const [wslShellByTool, setWslShellByTool] = useState< Record >({}); const [loadingTools, setLoadingTools] = useState>({}); 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 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 = 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")}
); })}

{t("settings.oneClickInstall")}

{t("settings.oneClickInstallHint")}

            {ONE_CLICK_INSTALL_COMMANDS}
          
); }