/** * @file ClaudeAgentHubSettings.tsx * @description The dashboard's own settings that belong to Claude Code, shown * under Settings → Claude Code → Overview: the CLAUDE_HOME the server reads * transcripts and config from, the AgentHub hooks installed into Claude Code * with a Reinstall button (named as the plugin's when the enabled `agenthub` * plugin provides them), and a link to the Claude table on the Pricing page. * Changing CLAUDE_HOME asks first. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { RefreshCw, Plug, AlertTriangle, RotateCcw, CheckCircle, XCircle } from "lucide-react"; import { api } from "../../lib/api"; import { Skeleton } from "../Skeleton"; import { HomeDirSetting } from "./HomeDirSetting"; import { PricingLink } from "./PricingSettings"; type HooksInfo = { installed: boolean; path: string; hooks: Record; source?: "plugin"; }; /** * Claude Code's AgentHub settings: CLAUDE_HOME, hook status and the pricing link. * @param onHomeChanged - called after CLAUDE_HOME was saved (the page re-reads * the config it lists, which lives under that home). * @returns The three sections, each with its own id. */ export function ClaudeAgentHubSettings({ onHomeChanged }: { onHomeChanged?: () => void } = {}) { const { t } = useTranslation("settings"); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [hooks, setHooks] = useState(null); const [actionLoading, setActionLoading] = useState(null); const [actionResult, setActionResult] = useState<{ key: string; message: string; isError: boolean; } | null>(null); const load = useCallback(async () => { try { const infoRes = await api.settings.info(); setHooks(infoRes.hooks); setError(null); } catch (err) { setError(err instanceof Error ? err.message : t("messages.failedLoad")); } finally { setLoading(false); } }, [t]); useEffect(() => { load(); }, [load]); // Hooks can be (re)installed from a terminal while this tab is open; keep the // badge current the way the old Settings page did, every 10 s. useEffect(() => { const id = setInterval(() => { if (!(document.visibilityState === "visible")) return; api.settings .info() .then((r) => setHooks(r.hooks)) .catch(() => {}); }, 10000); return () => clearInterval(id); }, []); useEffect(() => { if (!actionResult) return; const timeout = setTimeout(() => setActionResult(null), 5000); return () => clearTimeout(timeout); }, [actionResult]); const runAction = async (key: string, fn: () => Promise) => { setActionLoading(key); setActionResult(null); try { const message = await fn(); setActionResult({ key, message, isError: false }); await load(); } catch (err) { setActionResult({ key, message: t("messages.actionFailed", { message: err instanceof Error ? err.message : t("messages.unknownError"), }), isError: true, }); } finally { setActionLoading(null); } }; const handleReinstallHooks = () => runAction("hooks", async () => { const res = await api.settings.reinstallHooks(); return res.ok ? t("hooks.success") : t("hooks.failed"); }); const actionBanner = (keys: string[]) => { const match = actionResult && keys.includes(actionResult.key) ? actionResult : null; if (!match) return null; return (
{match.message}
); }; if (loading) { return (
); } return (
api.settings.claudeHome .get() .then((r) => ({ value: r.claude_home, envOwned: !!r.env_owned })) } save={(p) => api.settings.claudeHome.set(p).then((r) => { void load(); onHomeChanged?.(); return { value: r.claude_home, envOwned: !!r.env_owned }; }) } /> {error && (
{error}
)} {/* ─── HOOK CONFIGURATION ─── */}

{t("hooks.title")}

{t("hooks.description")}

{hooks?.installed ? ( {t("hooks.allInstalled")} ) : ( {t("hooks.incomplete")} )}
{actionBanner(["hooks"])} {hooks && ( <>
{Object.entries(hooks.hooks).map(([hook, active]) => (
{active ? ( ) : ( )} {hook}
))}
{hooks.source === "plugin" ? (

{t("hooks.plugin")}

) : (

{hooks.path}

)} )}
); }