"use client"; import { useEffect, useMemo, useState } from "react"; import { LOCALES, LOCALE_LABEL, useStore, type AgentInfo, type Locale, } from "@/lib/store"; import { useT, type DictKey } from "@/lib/i18n"; import { refreshTemplates } from "@/lib/templates"; type Props = { onClose: () => void; initialSection?: SectionId }; export type SectionId = "agent" | "deploy" | "marketplace" | "language"; const SECTIONS: Array<{ id: SectionId; labelKey: DictKey; hintKey: DictKey }> = [ { id: "agent", labelKey: "settings.section.agent.label", hintKey: "settings.section.agent.hint" }, { id: "deploy", labelKey: "settings.section.deploy.label", hintKey: "settings.section.deploy.hint" }, { id: "marketplace", labelKey: "settings.section.marketplace.label", hintKey: "settings.section.marketplace.hint", }, { id: "language", labelKey: "settings.section.language.label", hintKey: "settings.section.language.hint" }, ]; const PROTOCOL_KEY: Record = { stdin: { key: "protocol.stdin", tone: "ok" }, argv: { key: "protocol.argv", tone: "ok" }, "argv-message": { key: "protocol.argvMessage", tone: "ok" }, acp: { key: "protocol.acp", tone: "warn" }, "pi-rpc": { key: "protocol.piRpc", tone: "warn" }, }; const VENDOR_GRADIENT: Record = { Anthropic: "from-[#c96442] to-[#e9b94a]", OpenAI: "from-[#10a37f] to-[#1f7a3a]", Cursor: "from-[#5b6cf2] to-[#a1a8f5]", Google: "from-[#4285f4] to-[#34a853]", GitHub: "from-[#24292e] to-[#444c56]", Open: "from-[#6e7448] to-[#b26200]", Alibaba: "from-[#ff7a00] to-[#ed6f5c]", Aider: "from-[#6c3aa6] to-[#9c2a25]", DeepSeek: "from-[#2563eb] to-[#7c3aed]", CodeWhale: "from-[#2563eb] to-[#7c3aed]", Cognition: "from-[#0f172a] to-[#475569]", Mature: "from-[#7c2d12] to-[#b45309]", Moonshot: "from-[#0ea5e9] to-[#1e3a8a]", Inflection: "from-[#a855f7] to-[#ec4899]", AWS: "from-[#ff9900] to-[#232f3e]", Kilo: "from-[#16a34a] to-[#0d9488]", Mistral: "from-[#fb923c] to-[#ef4444]", Qoder: "from-[#0891b2] to-[#7c3aed]", }; export function SettingsModal({ onClose, initialSection = "agent" }: Props) { const [section, setSection] = useState(initialSection); const t = useT(); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} >
{t("settings.eyebrow")}

{t("settings.titlePart1")} {t("settings.titleAccent")}

{section === "agent" && } {section === "deploy" && } {section === "marketplace" && } {section === "language" && }
); } function AgentSection() { const setSelectedAgent = useStore((s) => s.setSelectedAgent); const setAgents = useStore((s) => s.setAgents); const setAgentModel = useStore((s) => s.setAgentModel); const setAgentBinOverride = useStore((s) => s.setAgentBinOverride); const agents = useStore((s) => s.agents); const selected = useStore((s) => s.selectedAgent); const agentModels = useStore((s) => s.agentModels); const agentBinOverrides = useStore((s) => s.agentBinOverrides); const t = useT(); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); const load = async () => { setLoading(true); setErr(null); try { const res = await fetch("/api/agents", { cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()) as { agents: AgentInfo[] }; setAgents(data.agents); const installed = data.agents.filter((a) => a.available); if (!installed.find((a) => a.id === selected) && installed.length) { setSelectedAgent(installed[0].id); } } catch (e) { setErr(e instanceof Error ? e.message : "detection failed"); } finally { setLoading(false); } }; useEffect(() => { if (!agents.length) load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const installed = useMemo(() => agents.filter((a) => a.available), [agents]); const missing = useMemo(() => agents.filter((a) => !a.available), [agents]); const selectedAgent = installed.find((a) => a.id === selected); const selectedModelId = selected ? agentModels[selected] ?? "default" : "default"; return (

{t("settings.agent.title")}

{t("settings.agent.subtitle")}

{err && (
{t("welcome.detectionFailed")}: {err}
)} {installed.length > 0 && ( <>
{t("welcome.installed", { n: installed.length })}
{installed.map((a) => ( setSelectedAgent(a.id)} /> ))}
)} {selectedAgent && ( setAgentModel(selectedAgent.id, id)} /> )} {selectedAgent && ( setAgentBinOverride(selectedAgent.id, p)} /> )} {missing.length > 0 && ( <>
{t("welcome.notInstalled", { n: missing.length })}
{missing.map((a) => ( ))}
)} {!loading && agents.length === 0 && (
๐Ÿชž

{t("welcome.noAgentsTitle")}

{t("welcome.noAgentsBody")}

)}
); } function LanguageSection() { const locale = useStore((s) => s.locale); const setLocale = useStore((s) => s.setLocale); const t = useT(); return (

{t("settings.language.title")}

{t("settings.language.subtitle")}

{LOCALES.map((code) => { const active = code === locale; return ( ); })}
{t("settings.language.note")}
); } function AgentCard({ agent, selected, onClick, }: { agent: AgentInfo; selected: boolean; onClick: () => void; }) { const t = useT(); const proto = PROTOCOL_KEY[agent.protocol]; const gradient = VENDOR_GRADIENT[agent.vendor] ?? "from-[var(--ink)] to-[var(--ink-soft)]"; return ( ); } function ModelPicker({ agent, modelId, onPick, }: { agent: AgentInfo; modelId: string; onPick: (id: string) => void; }) { const t = useT(); const models = agent.models.length ? agent.models : [{ id: "default", label: t("model.defaultLabel") }]; const MARK = "โ€‹"; const [before, after = ""] = t("model.label", { agent: MARK }).split(MARK); return (
{t("model.eyebrow")}
{before} {agent.label} {after}
{t("model.defaultHint.prefix")} --model {t("model.defaultHint.suffix")}
{models.map((m) => { const active = m.id === modelId; return ( ); })}
); } function CustomBinPath({ agent, value, onChange, }: { agent: AgentInfo; value: string; onChange: (path: string) => void; }) { const t = useT(); const [draft, setDraft] = useState(value); // Keep the local input in sync if the persisted value changes from elsewhere // (e.g. agent switch). Avoid clobbering an in-progress edit. useEffect(() => { setDraft(value); }, [value, agent.id]); // Best-effort platform sniff for the placeholder hint. The actual path // resolution happens server-side in `resolveBinForAgent`. const isWindows = typeof navigator !== "undefined" && /Win/i.test(navigator.platform); const placeholder = isWindows ? "C:\\Users\\you\\scoop\\apps\\nodejs\\current\\claude.cmd" : "/usr/local/bin/claude"; const detected = agent.path; const dirty = draft.trim() !== value.trim(); return (
{t("agent.customBin.eyebrow")}
{t("agent.customBin.subtitle", { agent: agent.label })}
{detected && (
{t("agent.customBin.detected")} {detected}
)}
setDraft(e.target.value)} onBlur={() => onChange(draft)} onKeyDown={(e) => { if (e.key === "Enter") onChange(draft); if (e.key === "Escape") setDraft(value); }} placeholder={placeholder} className="min-w-0 flex-1 rounded-lg px-3 py-1.5 font-mono text-[12px] outline-none" style={{ background: "var(--surface)", border: "1px solid var(--line)", color: "var(--ink)", }} /> {value && ( )} {dirty && ( )}
{t("agent.customBin.hint")}
); } function MissingCard({ agent }: { agent: AgentInfo }) { const t = useT(); const gradient = VENDOR_GRADIENT[agent.vendor] ?? "from-[var(--ink-faint)] to-[var(--ink-mute)]"; return (
{agent.label.charAt(0)}
{agent.label}
{agent.vendor}
{t("agent.notInstalled")}
); } function DeploySection() { const t = useT(); return (

{t("settings.deploy.title")}

{t("settings.deploy.subtitle")}

); } function VercelDeployConfig() { const t = useT(); const [token, setToken] = useState(""); const [teamSlug, setTeamSlug] = useState(""); const [configured, setConfigured] = useState(false); const [tokenMask, setTokenMask] = useState(""); const [loading, setLoading] = useState(false); const [savedAt, setSavedAt] = useState(null); const [err, setErr] = useState(null); const load = async () => { try { const res = await fetch("/api/deploy/config?provider=vercel"); if (!res.ok) return; const data = await res.json(); setConfigured(!!data.configured); setTokenMask(data.tokenMask || ""); // Show the mask as the input value when configured so the user sees // something other than an empty box. They can replace it to update. setToken(data.tokenMask || ""); setTeamSlug(data.teamSlug || ""); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } }; useEffect(() => { load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onSave = async () => { setLoading(true); setErr(null); try { const res = await fetch("/api/deploy/config?provider=vercel", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: token.trim(), teamSlug: teamSlug.trim() }), }); const data = await res.json(); if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); setConfigured(!!data.configured); setTokenMask(data.tokenMask || ""); setToken(data.tokenMask || token); setTeamSlug(data.teamSlug || ""); setSavedAt(Date.now()); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }; const onClear = async () => { if (!confirm("Clear Vercel token from ~/.html-anything?")) return; setLoading(true); setErr(null); try { const res = await fetch("/api/deploy/config?provider=vercel", { method: "DELETE", }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); setToken(""); setTeamSlug(""); setConfigured(false); setTokenMask(""); setSavedAt(null); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }; return (
{t("settings.deploy.vercel.title")}
{configured && (
โ— {t("settings.deploy.configured")}
)}
vercel.com/account/tokens โ†—
setToken(e.target.value)} placeholder={t("settings.deploy.vercel.tokenPlaceholder")} className="w-full rounded-lg px-3 py-1.5 font-mono text-[12px] outline-none mb-2" style={{ background: "var(--surface)", border: "1px solid var(--line)", color: "var(--ink)", }} /> setTeamSlug(e.target.value)} placeholder={t("settings.deploy.vercel.teamSlugPlaceholder")} className="w-full rounded-lg px-3 py-1.5 font-mono text-[12px] outline-none mb-3" style={{ background: "var(--surface)", border: "1px solid var(--line)", color: "var(--ink)", }} />
{t("settings.deploy.vercel.tokenHint")}
{configured && ( )} {savedAt && ( {t("settings.deploy.configured")} )}
{err && (
{err}
)}
); } function ComingSoonProvider() { const t = useT(); return (
{t("deploy.provider.cloudflarePages")}
{t("deploy.provider.cloudflarePages.comingSoon")}
); } type InstalledPackage = { id: string; source: { type: "github"; owner: string; repo: string; ref: string }; installedAt: string; skills: string[]; }; function MarketplaceSection() { const t = useT(); const [packages, setPackages] = useState([]); const [loading, setLoading] = useState(false); const [source, setSource] = useState(""); const [installing, setInstalling] = useState(false); const [err, setErr] = useState(null); const [info, setInfo] = useState(null); const load = async () => { setLoading(true); setErr(null); try { const res = await fetch("/api/marketplace", { cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()) as { packages: InstalledPackage[] }; setPackages(data.packages); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const onInstall = async () => { const spec = source.trim(); if (!spec) return; setInstalling(true); setErr(null); setInfo(null); try { const res = await fetch("/api/marketplace/install", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ source: spec }), }); const data = (await res.json()) as | { package: InstalledPackage } | { error: string; message?: string }; if (!res.ok || !("package" in data)) { const errMsg = "error" in data ? data.message ?? data.error : "install failed"; throw new Error(errMsg); } const pkg = data.package; setSource(""); setInfo( t("marketplace.installSucceeded", { n: pkg.skills.length, repo: `${pkg.source.owner}/${pkg.source.repo}`, }), ); // Drop the in-memory template registry cache AND push the fresh list // to every mounted `useTemplates` consumer โ€” the picker switches over // immediately, no page reload. Failing here just means the picker // keeps the previous list; the install itself already succeeded. await refreshTemplates().catch(() => undefined); await load(); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setInstalling(false); } }; const onUninstall = async (id: string) => { setErr(null); setInfo(null); try { const res = await fetch(`/api/marketplace/packages/${encodeURIComponent(id)}`, { method: "DELETE", }); if (!res.ok) { const data = (await res.json().catch(() => ({}))) as { message?: string; error?: string }; throw new Error(data.message ?? data.error ?? `HTTP ${res.status}`); } setInfo(t("marketplace.uninstalled")); // Push the post-uninstall list to mounted picker consumers so the // removed skill disappears without waiting for a page reload. await refreshTemplates().catch(() => undefined); await load(); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } }; return (

{t("settings.marketplace.title")}

{t("settings.marketplace.subtitle")}

{t("marketplace.installFromGithub")}
setSource(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !installing) onInstall(); }} placeholder={t("marketplace.placeholder")} disabled={installing} className="min-w-0 flex-1 rounded-lg px-3 py-2 font-mono text-[12px] outline-none" style={{ background: "var(--surface)", border: "1px solid var(--line)", color: "var(--ink)", }} />
{t("marketplace.hint")}
{err && (
{err}
)} {info && (
{info}
)}
{t("marketplace.installed", { n: packages.length })}
{packages.length === 0 && !loading && (
๐Ÿ“ฆ

{t("marketplace.empty.title")}

{t("marketplace.empty.body")}

)}
{packages.map((pkg) => ( onUninstall(pkg.id)} /> ))}
); } function PackageCard({ pkg, onUninstall }: { pkg: InstalledPackage; onUninstall: () => void }) { const t = useT(); const repoUrl = `https://github.com/${pkg.source.owner}/${pkg.source.repo}`; return (
{pkg.source.owner}/{pkg.source.repo} {pkg.source.ref}
{repoUrl.replace(/^https?:\/\//, "")} ยท {t("marketplace.skillCount", { n: pkg.skills.length })} ยท {new Date(pkg.installedAt).toLocaleDateString()}
{pkg.skills.length > 0 && (
{pkg.skills.map((s) => ( {s} ))}
)}
); }