mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 19:22:15 +08:00
feat(settings): expand About page into a tool management panel
List all managed apps with current/latest versions, with per-tool install and update buttons plus an Update All action. Installs and updates now run silently: the button shows a spinner for the full duration, versions refresh automatically when done, and failures surface the backend error detail in a toast. While loading, the button keeps its label and only swaps the icon for a spinner, so width stays constant across zh/en/ja instead of jumping when the text changes (e.g. Update -> Updating...). Also collapses and renames the install-commands area to Manual Install Commands, and removes the managed-apps badge row and the Install Missing batch button.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
Copy,
|
Copy,
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
ArrowUpCircle,
|
||||||
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +31,8 @@ import { relaunchApp } from "@/lib/updater";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import appIcon from "@/assets/icons/app-icon.png";
|
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 {
|
interface AboutSectionProps {
|
||||||
isPortable: boolean;
|
isPortable: boolean;
|
||||||
@@ -44,8 +47,16 @@ interface ToolVersion {
|
|||||||
wsl_distro: string | null;
|
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 ToolName = (typeof TOOL_NAMES)[number];
|
||||||
|
type ToolLifecycleAction = "install" | "update";
|
||||||
|
|
||||||
type WslShellPreference = {
|
type WslShellPreference = {
|
||||||
wslShell?: string | null;
|
wslShell?: string | null;
|
||||||
@@ -82,14 +93,36 @@ const ENV_BADGE_CONFIG: Record<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ONE_CLICK_INSTALL_COMMANDS = `# Claude Code (Native install - recommended)
|
const ONE_CLICK_INSTALL_COMMANDS = `# Claude Code
|
||||||
curl -fsSL https://claude.ai/install.sh | bash
|
npm i -g @anthropic-ai/claude-code@latest
|
||||||
# Codex
|
# Codex
|
||||||
npm i -g @openai/codex@latest
|
npm i -g @openai/codex@latest
|
||||||
# Gemini CLI
|
# Gemini CLI
|
||||||
npm i -g @google/gemini-cli@latest
|
npm i -g @google/gemini-cli@latest
|
||||||
# OpenCode
|
# 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<ToolName, string> = {
|
||||||
|
claude: "Claude Code",
|
||||||
|
codex: "Codex",
|
||||||
|
gemini: "Gemini CLI",
|
||||||
|
opencode: "OpenCode",
|
||||||
|
openclaw: "OpenClaw",
|
||||||
|
hermes: "Hermes",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOOL_APP_IDS: Record<ToolName, AppId> = {
|
||||||
|
claude: "claude",
|
||||||
|
codex: "codex",
|
||||||
|
gemini: "gemini",
|
||||||
|
opencode: "opencode",
|
||||||
|
openclaw: "openclaw",
|
||||||
|
hermes: "hermes",
|
||||||
|
};
|
||||||
|
|
||||||
export function AboutSection({ isPortable }: AboutSectionProps) {
|
export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||||
// ... (use hooks as before) ...
|
// ... (use hooks as before) ...
|
||||||
@@ -99,6 +132,13 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const [toolVersions, setToolVersions] = useState<ToolVersion[]>([]);
|
const [toolVersions, setToolVersions] = useState<ToolVersion[]>([]);
|
||||||
const [isLoadingTools, setIsLoadingTools] = useState(true);
|
const [isLoadingTools, setIsLoadingTools] = useState(true);
|
||||||
|
const [toolActions, setToolActions] = useState<
|
||||||
|
Partial<Record<ToolName, ToolLifecycleAction>>
|
||||||
|
>({});
|
||||||
|
const [batchAction, setBatchAction] = useState<ToolLifecycleAction | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [showInstallCommands, setShowInstallCommands] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasUpdate,
|
hasUpdate,
|
||||||
@@ -114,6 +154,23 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
>({});
|
>({});
|
||||||
const [loadingTools, setLoadingTools] = useState<Record<string, boolean>>({});
|
const [loadingTools, setLoadingTools] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
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(
|
const refreshToolVersions = useCallback(
|
||||||
async (
|
async (
|
||||||
toolNames: ToolName[],
|
toolNames: ToolName[],
|
||||||
@@ -202,7 +259,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
try {
|
try {
|
||||||
const [appVersion] = await Promise.all([
|
const [appVersion] = await Promise.all([
|
||||||
getVersion(),
|
getVersion(),
|
||||||
...(isWindows() ? [] : [loadAllToolVersions()]),
|
loadAllToolVersions(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
@@ -311,6 +368,63 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
}
|
}
|
||||||
}, [t]);
|
}, [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");
|
const displayVersion = version ?? t("common.unknown");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -450,18 +564,16 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{!isWindows() && (
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between px-1">
|
<div className="flex flex-col gap-2 px-1 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h3 className="text-sm font-medium">
|
<h3 className="text-sm font-medium">{t("settings.localEnvCheck")}</h3>
|
||||||
{t("settings.localEnvCheck")}
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
</h3>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-7 gap-1.5 text-xs"
|
className="h-7 gap-1.5 text-xs"
|
||||||
onClick={() => loadAllToolVersions()}
|
onClick={() => loadAllToolVersions()}
|
||||||
disabled={isLoadingTools}
|
disabled={isLoadingTools || Boolean(batchAction)}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={
|
className={
|
||||||
@@ -470,16 +582,48 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
/>
|
/>
|
||||||
{isLoadingTools ? t("common.refreshing") : t("common.refresh")}
|
{isLoadingTools ? t("common.refreshing") : t("common.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="h-7 gap-1.5 text-xs"
|
||||||
|
onClick={() => handleRunToolAction(updatableToolNames, "update")}
|
||||||
|
disabled={
|
||||||
|
isLoadingTools ||
|
||||||
|
Boolean(batchAction) ||
|
||||||
|
updatableToolNames.length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{batchAction === "update" ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpCircle className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
{t("settings.updateAllTools", {
|
||||||
|
count: updatableToolNames.length,
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 px-1">
|
<div className="grid gap-3 px-1 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{TOOL_NAMES.map((toolName, index) => {
|
{TOOL_NAMES.map((toolName, index) => {
|
||||||
const tool = toolVersions.find((item) => item.name === toolName);
|
const tool = toolVersionByName.get(toolName);
|
||||||
// Special case for OpenCode (capital C), others use capitalize
|
const appConfig = APP_ICON_MAP[TOOL_APP_IDS[toolName]];
|
||||||
const displayName =
|
const displayName = TOOL_DISPLAY_NAMES[toolName];
|
||||||
toolName === "opencode"
|
const isToolVersionLoading =
|
||||||
? "OpenCode"
|
isLoadingTools || Boolean(loadingTools[toolName]);
|
||||||
: toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
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");
|
const title = tool?.version || tool?.error || t("common.unknown");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -487,38 +631,86 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
key={toolName}
|
key={toolName}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3, delay: 0.15 + index * 0.05 }}
|
transition={{ duration: 0.3, delay: 0.15 + index * 0.04 }}
|
||||||
whileHover={{ scale: 1.02 }}
|
className="flex min-h-[150px] flex-col gap-3 rounded-xl border border-border bg-gradient-to-br from-card/80 to-card/40 p-4 shadow-sm transition-colors hover:border-primary/30"
|
||||||
className="flex flex-col gap-2 rounded-xl border border-border bg-gradient-to-br from-card/80 to-card/40 p-4 shadow-sm transition-colors hover:border-primary/30"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<Terminal className="h-4 w-4 text-muted-foreground" />
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-background/80 text-muted-foreground">
|
||||||
<span className="text-sm font-medium">{displayName}</span>
|
{appConfig?.icon ?? <Terminal className="h-4 w-4" />}
|
||||||
{/* Environment Badge */}
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-medium">
|
||||||
|
{displayName}
|
||||||
|
</div>
|
||||||
{tool?.env_type && ENV_BADGE_CONFIG[tool.env_type] && (
|
{tool?.env_type && ENV_BADGE_CONFIG[tool.env_type] && (
|
||||||
<span
|
<span
|
||||||
className={`text-[9px] px-1.5 py-0.5 rounded-full border ${ENV_BADGE_CONFIG[tool.env_type].className}`}
|
className={`mt-1 inline-flex w-fit text-[9px] px-1.5 py-0.5 rounded-full border ${ENV_BADGE_CONFIG[tool.env_type].className}`}
|
||||||
>
|
>
|
||||||
{t(ENV_BADGE_CONFIG[tool.env_type].labelKey)}
|
{t(ENV_BADGE_CONFIG[tool.env_type].labelKey)}
|
||||||
|
{tool.wsl_distro ? ` · ${tool.wsl_distro}` : ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* WSL Shell Selector */}
|
</div>
|
||||||
|
</div>
|
||||||
|
{isToolVersionLoading ? (
|
||||||
|
<Loader2 className="mt-1 h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
) : tool?.version ? (
|
||||||
|
isOutdated ? (
|
||||||
|
<span className="mt-1 shrink-0 rounded-full border border-yellow-500/20 bg-yellow-500/10 px-1.5 py-0.5 text-[10px] text-yellow-600 dark:text-yellow-400">
|
||||||
|
{t("settings.updateAvailableShort")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="mt-1 h-4 w-4 shrink-0 text-green-500" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="mt-1 h-4 w-4 shrink-0 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5 text-xs">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("settings.currentVersion")}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="min-w-0 truncate font-mono text-foreground"
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{isToolVersionLoading
|
||||||
|
? t("common.loading")
|
||||||
|
: tool?.version || t("common.notInstalled")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("settings.latestVersion")}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate font-mono text-foreground">
|
||||||
|
{isToolVersionLoading
|
||||||
|
? t("common.loading")
|
||||||
|
: tool?.latest_version || t("common.unknown")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!isToolVersionLoading && !tool?.version && tool?.error && (
|
||||||
|
<div className="truncate text-[11px] text-muted-foreground">
|
||||||
|
{tool.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{tool?.env_type === "wsl" && (
|
{tool?.env_type === "wsl" && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={wslShellByTool[toolName]?.wslShell || "auto"}
|
value={wslShellByTool[toolName]?.wslShell || "auto"}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) => handleToolShellChange(toolName, v)}
|
||||||
handleToolShellChange(toolName, v)
|
|
||||||
}
|
|
||||||
disabled={isLoadingTools || loadingTools[toolName]}
|
disabled={isLoadingTools || loadingTools[toolName]}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-[70px] text-xs">
|
<SelectTrigger className="h-7 w-[82px] text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="auto">
|
<SelectItem value="auto">{t("common.auto")}</SelectItem>
|
||||||
{t("common.auto")}
|
|
||||||
</SelectItem>
|
|
||||||
{WSL_SHELL_OPTIONS.map((shell) => (
|
{WSL_SHELL_OPTIONS.map((shell) => (
|
||||||
<SelectItem key={shell} value={shell}>
|
<SelectItem key={shell} value={shell}>
|
||||||
{shell}
|
{shell}
|
||||||
@@ -526,25 +718,18 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
|
||||||
{/* WSL Shell Flag Selector */}
|
|
||||||
{tool?.env_type === "wsl" && (
|
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={wslShellByTool[toolName]?.wslShellFlag || "auto"}
|
||||||
wslShellByTool[toolName]?.wslShellFlag || "auto"
|
|
||||||
}
|
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
handleToolShellFlagChange(toolName, v)
|
handleToolShellFlagChange(toolName, v)
|
||||||
}
|
}
|
||||||
disabled={isLoadingTools || loadingTools[toolName]}
|
disabled={isLoadingTools || loadingTools[toolName]}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-[70px] text-xs">
|
<SelectTrigger className="h-7 w-[82px] text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="auto">
|
<SelectItem value="auto">{t("common.auto")}</SelectItem>
|
||||||
{t("common.auto")}
|
|
||||||
</SelectItem>
|
|
||||||
{WSL_SHELL_FLAG_OPTIONS.map((flag) => (
|
{WSL_SHELL_FLAG_OPTIONS.map((flag) => (
|
||||||
<SelectItem key={flag} value={flag}>
|
<SelectItem key={flag} value={flag}>
|
||||||
{flag}
|
{flag}
|
||||||
@@ -552,50 +737,71 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{isLoadingTools || loadingTools[toolName] ? (
|
)}
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
||||||
) : tool?.version ? (
|
<div className="mt-auto flex items-center justify-end">
|
||||||
tool.latest_version &&
|
{isToolVersionLoading ? (
|
||||||
tool.version !== tool.latest_version ? (
|
<span className="text-xs text-muted-foreground">
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20">
|
{t("common.loading")}
|
||||||
{tool.latest_version}
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : action ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<Button
|
||||||
)
|
size="sm"
|
||||||
) : (
|
variant={action === "install" ? "outline" : "default"}
|
||||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
className="h-7 gap-1.5 text-xs"
|
||||||
)}
|
onClick={() => handleRunToolAction([toolName], action)}
|
||||||
</div>
|
disabled={
|
||||||
<div
|
isToolVersionLoading ||
|
||||||
className="text-xs font-mono text-muted-foreground truncate"
|
Boolean(runningAction) ||
|
||||||
title={title}
|
Boolean(batchAction)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isLoadingTools
|
{runningAction ? (
|
||||||
? t("common.loading")
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
: tool?.version
|
) : action === "install" ? (
|
||||||
? tool.version
|
<Download className="h-3.5 w-3.5" />
|
||||||
: tool?.error || t("common.notInstalled")}
|
) : (
|
||||||
|
<ArrowUpCircle className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
{/* loading 时文案保持不变、仅图标切换为 spinner,
|
||||||
|
按钮宽度恒定,避免"升级"→"升级中…"导致的抖动。 */}
|
||||||
|
{action === "install"
|
||||||
|
? t("settings.toolInstall")
|
||||||
|
: t("settings.toolUpdate")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t("settings.toolReady")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{!isWindows() && (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3, delay: 0.3 }}
|
transition={{ duration: 0.3, delay: 0.3 }}
|
||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
>
|
>
|
||||||
<h3 className="text-sm font-medium px-1">
|
<button
|
||||||
{t("settings.oneClickInstall")}
|
type="button"
|
||||||
</h3>
|
onClick={() => setShowInstallCommands((v) => !v)}
|
||||||
|
aria-expanded={showInstallCommands}
|
||||||
|
className="flex w-full items-center gap-1.5 px-1 text-sm font-medium text-foreground transition-colors hover:text-primary"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-3.5 w-3.5 transition-transform ${
|
||||||
|
showInstallCommands ? "" : "-rotate-90"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{t("settings.manualInstallCommands")}
|
||||||
|
</button>
|
||||||
|
{showInstallCommands && (
|
||||||
<div className="rounded-xl border border-border bg-gradient-to-br from-card/80 to-card/40 p-4 space-y-3 shadow-sm">
|
<div className="rounded-xl border border-border bg-gradient-to-br from-card/80 to-card/40 p-4 space-y-3 shadow-sm">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -615,8 +821,8 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
{ONE_CLICK_INSTALL_COMMANDS}
|
{ONE_CLICK_INSTALL_COMMANDS}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
|
</motion.div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -659,9 +659,18 @@
|
|||||||
"viewCurrentReleaseNotes": "View current version release notes",
|
"viewCurrentReleaseNotes": "View current version release notes",
|
||||||
"officialWebsite": "Official Website",
|
"officialWebsite": "Official Website",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"oneClickInstall": "One-click Install",
|
"manualInstallCommands": "Manual Install Commands",
|
||||||
"oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI / OpenCode",
|
"oneClickInstallHint": "Install or upgrade Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Hermes",
|
||||||
"localEnvCheck": "Local environment check",
|
"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": {
|
"envBadge": {
|
||||||
"wsl": "WSL",
|
"wsl": "WSL",
|
||||||
"windows": "Win",
|
"windows": "Win",
|
||||||
|
|||||||
@@ -659,9 +659,18 @@
|
|||||||
"viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る",
|
"viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る",
|
||||||
"officialWebsite": "公式サイト",
|
"officialWebsite": "公式サイト",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"oneClickInstall": "ワンクリックインストール",
|
"manualInstallCommands": "手動インストールコマンド",
|
||||||
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode をインストール",
|
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Hermes をインストールまたは更新",
|
||||||
"localEnvCheck": "ローカル環境チェック",
|
"localEnvCheck": "ローカル環境チェック",
|
||||||
|
"updateAllTools": "すべて更新({{count}})",
|
||||||
|
"currentVersion": "現在のバージョン",
|
||||||
|
"latestVersion": "最新バージョン",
|
||||||
|
"updateAvailableShort": "更新あり",
|
||||||
|
"toolInstall": "インストール",
|
||||||
|
"toolUpdate": "更新",
|
||||||
|
"toolReady": "準備完了",
|
||||||
|
"toolActionDone": "{{count}} 個のツールの{{action}}が完了しました",
|
||||||
|
"toolActionFailed": "インストール/更新コマンドの実行に失敗しました",
|
||||||
"envBadge": {
|
"envBadge": {
|
||||||
"wsl": "WSL",
|
"wsl": "WSL",
|
||||||
"windows": "Win",
|
"windows": "Win",
|
||||||
|
|||||||
@@ -659,9 +659,18 @@
|
|||||||
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
||||||
"officialWebsite": "官方网站",
|
"officialWebsite": "官方网站",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"oneClickInstall": "一键安装",
|
"manualInstallCommands": "手动安装命令",
|
||||||
"oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI / OpenCode",
|
"oneClickInstallHint": "安装或升级 Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Hermes",
|
||||||
"localEnvCheck": "本地环境检查",
|
"localEnvCheck": "本地环境检查",
|
||||||
|
"updateAllTools": "全部升级({{count}})",
|
||||||
|
"currentVersion": "当前版本",
|
||||||
|
"latestVersion": "最新版本",
|
||||||
|
"updateAvailableShort": "可升级",
|
||||||
|
"toolInstall": "安装",
|
||||||
|
"toolUpdate": "升级",
|
||||||
|
"toolReady": "已就绪",
|
||||||
|
"toolActionDone": "{{count}} 个工具{{action}}完成",
|
||||||
|
"toolActionFailed": "安装/升级命令执行失败",
|
||||||
"envBadge": {
|
"envBadge": {
|
||||||
"wsl": "WSL",
|
"wsl": "WSL",
|
||||||
"windows": "Win",
|
"windows": "Win",
|
||||||
|
|||||||
Reference in New Issue
Block a user