/** * @file AiToolConfig.tsx * @description Settings → : the one tab every installed, enabled * catalog tool without a hand-written tab gets (Claude Code and Codex keep * theirs). Shows where the CLI lives, its homepage and an Update link into the * AI Tools modal; how many sessions it ran, their cost, and its lanes; a * default model (`PUT /api/ai-tools/:id {defaultModel}`) for a tool with a * model flag; its vendor's pricing table; and the config files its catalog * file lists, each opened over a sha and saved by the server only if it is * still that sha, valid, and backed up — a refusal because the file changed * (ESTALE) offers a reload and keeps the user's text to copy, and an edited * file asks before another file or Close drops it. Antigravity adds its hooks * card. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState, type ComponentType } from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Cpu, ExternalLink, FileCog, RefreshCw } from "lucide-react"; import { api, type AiToolConfigFile } from "../lib/api"; import { toast } from "../lib/toast"; import { confirmDiscard } from "../lib/confirm"; import { copyText } from "../lib/clipboard"; import { useUnsavedGuard } from "../hooks/useUnsavedGuard"; import type { Lane } from "../lib/types"; import { useAiTools } from "../hooks/useAiTools"; import { AiToolIcon } from "../components/AiToolIcon"; import { ModelPicker } from "../components/run/RunSetup"; import { PricingSettings } from "../components/settings/PricingSettings"; import { AntigravityHooksSettings } from "../components/settings/AntigravityHooksSettings"; /** Cards one tool adds below the generic ones. */ const EXTRAS: Record = { antigravity: AntigravityHooksSettings }; const message = (err: unknown) => (err instanceof Error ? err.message : String(err)); /** The default-model card: the picker, saved on an explicit click. */ function DefaultModel({ toolId, saved }: { toolId: string; saved: string }) { const { t } = useTranslation("settings"); const aiTools = useAiTools(); const [value, setValue] = useState(saved); const [error, setError] = useState(null); useEffect(() => setValue(saved), [saved]); const save = async () => { setError(null); try { await aiTools.update(toolId, { defaultModel: value || null }); toast.success(t("toolTab.defaultModelSaved")); } catch (err) { setError(message(err)); } }; return (

{t("toolTab.defaultModelHint")}

{error &&

{error}

}
); } /** The config-files card: pick a declared file, edit it, save over its sha. */ function ConfigFiles({ toolId }: { toolId: string }) { const { t } = useTranslation("settings"); const [files, setFiles] = useState(null); const [open, setOpen] = useState<{ index: number; path: string; sha: string } | null>(null); const [text, setText] = useState(""); // The text as opened/saved: an edit differs from it. const [base, setBase] = useState(""); const [busy, setBusy] = useState(false); const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null); // Set when a save was refused because the file changed on disk. const [stale, setStale] = useState(false); const dirty = open !== null && text !== base; useUnsavedGuard(dirty); useEffect(() => { api.aiTools .config(toolId) .then((r) => setFiles(r.files)) .catch((err) => setNote({ ok: false, text: message(err) })); }, [toolId]); const load = async (index: number) => { setNote(null); setStale(false); try { const r = await api.aiTools.readConfig(toolId, index); setOpen({ index, path: r.path, sha: r.sha }); setText(r.text); setBase(r.text); } catch (err) { setNote({ ok: false, text: message(err) }); } }; const openFile = async (f: AiToolConfigFile) => { if (dirty && !(await confirmDiscard())) return; await load(f.index); }; const close = async () => { if (dirty && !(await confirmDiscard())) return; setOpen(null); setStale(false); }; const save = async () => { if (!open) return; setBusy(true); setNote(null); setStale(false); try { const r = await api.aiTools.saveConfig(toolId, open.index, text, open.sha); setOpen({ ...open, sha: r.sha }); setBase(text); setNote({ ok: true, text: t("toolTab.saved", { backup: r.backup }) }); } catch (err) { setStale((err as { code?: string }).code === "ESTALE"); setNote({ ok: false, text: message(err) }); } finally { setBusy(false); } }; return (

{t("toolTab.configHint")}

{(files ?? []).map((f) => ( ))}
{open && (