import { Icon } from "./Icon"; import { useEffect, useState, useSyncExternalStore } from "react"; import { get, patch as apiPatch, post, put } from "../api"; import { useFetch, useModels } from "../hooks"; import { getLang, languages, setLang, useT, type Key } from "../i18n"; import { navigate } from "../router"; import { useStore } from "../store"; import { canPrompt, isInstalled, onInstallChange, platformHint, promptInstall } from "../install"; import { currentSubscription, disablePush, enablePush, pushSupported, syncPush, testPush } from "../push"; import { applyTheme, getTheme, getThemeMode, onThemeChange, THEME_MODES, THEMES, type Theme, type ThemeMode } from "../theme"; import { setReasoningMode, useReasoningMode, type ReasoningMode } from "../prefs"; import { ModelSelect } from "./ModelSelect"; import { Account } from "./settings/Account"; import { Templates } from "./settings/Templates"; import { AgentsEditor } from "./settings/AgentsEditor"; import { Folders } from "./settings/Folders"; import { Users } from "./settings/Users"; import { Audit } from "./settings/Audit"; import { Tools } from "./settings/Tools"; import { confirmDialog, promptDialog, toast } from "../dialogs"; type Json = any; interface ConfigResponse { config: Json; workspace: string; currency: string; } type Patch = { path: string[]; value: unknown }; const MASK = "••••••••"; async function savePatches(patches: Patch[]) { await apiPatch("/config", { patches }); } function useSaver(reload: () => void) { const [notice, setNotice] = useState<{ ok: boolean; text: string }>(); const t = useT(); const run = async (fn: () => Promise) => { try { await fn(); setNotice({ ok: true, text: t("settings.saved") }); reload(); } catch (e: any) { setNotice({ ok: false, text: e.message }); } }; const view = notice &&
{notice.text}
; return { run, view }; } // --- general ---------------------------------------------------------------- /** Per-device settings (language, theme, notifications) – available to everyone. */ function InterfaceSettings() { const t = useT(); const reasoning = useReasoningMode(); const theme = useSyncExternalStore(onThemeChange, getTheme); const mode = useSyncExternalStore(onThemeChange, getThemeMode); const themeInfo = THEMES.find((th) => th.id === theme)!; return (

{t("settings.interface")}

{t("settings.themeSource", { source: themeInfo.source, license: themeInfo.license })}
{THEME_MODES.map((m) => ( ))}
{t("settings.reasoningHint")}
{t("settings.notificationsHint")}
); } function InstallControl() { const t = useT(); const prompt = useSyncExternalStore(onInstallChange, canPrompt); if (isInstalled()) return {t("install.installed")}; if (prompt) return (
); return
{t(`install.hint.${platformHint()}` as Key)}
; } function VersionInfo() { const info = useFetch<{ version: string; commit?: string }>("/info").data; return
{info ? `CodeCraft ${info.version}${info.commit ? ` (${info.commit})` : ""}` : "…"}
; } /** Web Push on this device: enable/disable and a test notification. */ function PushControl() { const t = useT(); const [state, setState] = useState<"loading" | "on" | "off" | "unsupported" | "denied">("loading"); const [busy, setBusy] = useState(false); useEffect(() => { if (!pushSupported()) return setState("unsupported"); if (Notification.permission === "denied") return setState("denied"); currentSubscription() .then(async (s) => { setState(s ? "on" : "off"); // A subscription the server lost (or never got) is registered again. if (s) await syncPush().catch(() => undefined); }) .catch(() => setState("off")); }, []); const run = (f: () => Promise) => async () => { setBusy(true); try { await f(); } catch (e: any) { toast(e.message ?? String(e)); } finally { setBusy(false); } }; if (state === "loading") return
; if (state === "unsupported") return
{t("settings.pushUnsupported")}
; if (state === "denied") return
{t("settings.pushDenied")}
; return (
{state === "on" ? ( <> {t("settings.notificationsOn")} ) : ( )}
); } function General({ cfg, reload }: { cfg: ConfigResponse; reload: () => void }) { const t = useT(); const s = cfg.config.server; const c = cfg.config.craft; const [form, setForm] = useState({ default_model: s.default_model ?? "", small_model: s.small_model ?? "", currency: s.currency ?? "USD", auto_compact_threshold: s.auto_compact_threshold ?? 0.85, max_parallel_subagents: s.max_parallel_subagents ?? 4, monthly_budget: s.monthly_budget ?? 0, budget_warning: s.budget_warning ?? 0.8, images_budget: cfg.config.images?.monthly_budget ?? "", budget_action: s.budget_action ?? "warn", sandbox_mode: cfg.config.sandbox?.mode ?? "auto", clean_room: c.clean_room ?? true, chromium: c.chromium ?? "chromium", widths: (c.widths ?? []).join(", "), images_enabled: cfg.config.images?.enabled ?? false, images_key: cfg.config.images?.api_key === MASK ? "" : (cfg.config.images?.api_key ?? "${FAL_KEY}"), images_model: cfg.config.images?.default_model ?? "fal-ai/flux/schnell", images_dir: cfg.config.images?.output_dir ?? "generated", }); const { run, view } = useSaver(reload); const save = () => run(() => savePatches([ { path: ["server", "default_model"], value: form.default_model }, { path: ["server", "small_model"], value: form.small_model }, { path: ["server", "currency"], value: form.currency }, { path: ["server", "auto_compact_threshold"], value: Number(form.auto_compact_threshold) }, { path: ["server", "max_parallel_subagents"], value: Number(form.max_parallel_subagents) }, { path: ["server", "monthly_budget"], value: Number(form.monthly_budget) || 0 }, { path: ["server", "budget_warning"], value: Number(form.budget_warning) || 0.8 }, { path: ["server", "budget_action"], value: form.budget_action }, { path: ["sandbox", "mode"], value: form.sandbox_mode }, { path: ["images", "monthly_budget"], value: form.images_budget === "" ? null : Number(form.images_budget) }, { path: ["craft", "clean_room"], value: form.clean_room }, { path: ["craft", "chromium"], value: form.chromium }, { path: ["craft", "widths"], value: String(form.widths).split(/[\s,]+/).filter(Boolean).map(Number) }, { path: ["images", "enabled"], value: form.images_enabled }, { path: ["images", "default_model"], value: form.images_model }, { path: ["images", "output_dir"], value: form.images_dir }, ...(form.images_key ? [{ path: ["images", "api_key"], value: form.images_key }] : []), ]), ); return ( <>

{t("settings.server")}

{cfg.workspace} setForm({ ...form, default_model: v })} /> setForm({ ...form, small_model: v })} />
{t("settings.smallModelHint")}
setForm({ ...form, currency: e.target.value.toUpperCase() })} maxLength={3} /> setForm({ ...form, auto_compact_threshold: e.target.value })} />
{t("settings.autoCompactHint")}
setForm({ ...form, monthly_budget: e.target.value })} />
{t("settings.monthlyBudgetHint")}
setForm({ ...form, budget_warning: e.target.value })} />
{t("settings.sandboxHint")}
setForm({ ...form, max_parallel_subagents: e.target.value })} />
{t("settings.maxParallelSubagentsHint")}

{t("settings.craft")}

setForm({ ...form, clean_room: e.target.checked })} style={{ justifySelf: "start" }} />
{t("settings.cleanRoomHint")}
setForm({ ...form, chromium: e.target.value })} /> setForm({ ...form, widths: e.target.value })} />

{t("settings.images")}

setForm({ ...form, images_enabled: e.target.checked })} style={{ justifySelf: "start" }} /> setForm({ ...form, images_key: e.target.value })} /> setForm({ ...form, images_budget: e.target.value })} /> setForm({ ...form, images_dir: e.target.value })} />
{t("settings.imagesHint")}
{view} ); } // --- providers -------------------------------------------------------------- interface ModelRow { id: string; name?: string; context?: number; max_output?: number; reasoning?: boolean; vision?: boolean; effort?: string; input_price?: number; output_price?: number; cache_read_price?: number; cache_write_price?: number; max_concurrency?: number; fallback?: string; options?: Record; } const PRESETS: Record = { anthropic: { kind: "anthropic", base_url: "https://api.anthropic.com/v1", api_key: "${ANTHROPIC_API_KEY}" }, gemini: { kind: "openai", base_url: "https://generativelanguage.googleapis.com/v1beta/openai", api_key: "${GEMINI_API_KEY}" }, "opencode-go": { kind: "openai", base_url: "https://opencode.ai/inference/go/openai/v1", api_key: "${OPENCODE_API_KEY}" }, fal: { kind: "openai", base_url: "https://fal.run/openrouter/router/openai/v1", api_key: "${FAL_KEY}", auth_prefix: "Key " }, openrouter: { kind: "openai", base_url: "https://openrouter.ai/api/v1", api_key: "${OPENROUTER_API_KEY}" }, ollama: { kind: "openai", base_url: "http://host.docker.internal:11434/v1", api_key: "" }, custom: { kind: "openai", base_url: "http://", api_key: "" }, }; function normalizeModels(models: Json[]): ModelRow[] { return (models ?? []).map((m) => (typeof m === "string" ? { id: m } : { ...m })); } function cleanModel(m: ModelRow) { const out: Record = {}; for (const [k, v] of Object.entries(m)) { if (v === undefined || v === "" || (typeof v === "number" && Number.isNaN(v))) continue; if (v === false && (k === "reasoning" || k === "vision")) continue; out[k] = v; } return out; } type ProviderStatus = { up: boolean; state?: "ok" | "auth" | "down"; error?: string; latency_ms: number; checked_at: number }; function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; reload: () => void; status?: ProviderStatus }) { const t = useT(); const [form, setForm] = useState({ ...p, api_key: p.api_key === MASK ? "" : p.api_key, admin_key: "" }); const [priceNote, setPriceNote] = useState(); const fetchPrices = async () => { try { const r = await get<{ models: ModelRow[] }>(`/providers/${name}/prices`); const byId = new Map(r.models.map((m) => [m.id, m])); let n = 0; setModels((ms) => ms.map((m) => { const f = byId.get(m.id); if (!f) return m; n++; return { ...m, input_price: f.input_price ?? m.input_price, output_price: f.output_price ?? m.output_price, cache_read_price: f.cache_read_price ?? m.cache_read_price, cache_write_price: f.cache_write_price ?? m.cache_write_price, context: m.context ?? f.context }; }), ); setPriceNote(t("settings.pricesFetched", { n })); } catch (e: any) { setPriceNote(e.message); } }; const [models, setModels] = useState(normalizeModels(p.models)); const [open, setOpen] = useState(false); const discovered = useModels().find((x) => x.provider === name)?.models ?? []; const { run, view } = useSaver(reload); const num = (v: string) => (v === "" ? undefined : Number(v)); const setModel = (i: number, patch: Partial) => setModels((ms) => ms.map((m, j) => (j === i ? { ...m, ...patch } : m))); const save = () => run(() => { const patches: Patch[] = [ { path: ["providers", name, "kind"], value: form.kind }, { path: ["providers", name, "base_url"], value: form.base_url }, { path: ["providers", name, "enabled"], value: !!form.enabled }, { path: ["providers", name, "models"], value: models.filter((m) => m.id.trim()).map(cleanModel) }, { path: ["providers", name, "auth_prefix"], value: form.auth_prefix ? form.auth_prefix : null }, { path: ["providers", name, "max_concurrency"], value: form.max_concurrency ? Number(form.max_concurrency) : null }, { path: ["providers", name, "monthly_budget"], value: form.monthly_budget ? Number(form.monthly_budget) : null }, ]; // Empty key field = keep the stored (masked) key. if (form.api_key !== "" || p.api_key !== MASK) patches.push({ path: ["providers", name, "api_key"], value: form.api_key }); if (form.admin_key) patches.push({ path: ["providers", name, "admin_key"], value: form.admin_key }); return savePatches(patches); }); const remove = async () => { if (!(await confirmDialog({ message: t("settings.deleteProviderConfirm", { name }), danger: true, confirmLabel: t("common.delete") }))) return; void run(() => savePatches([{ path: ["providers", name], value: null }])); }; const addDiscovered = () => { const have = new Set(models.map((m) => m.id)); setModels([...models, ...discovered.filter((d) => !have.has(d.id)).map((d) => ({ id: d.id, context: d.context }))]); }; return (

setForm({ ...form, enabled: e.target.checked })} title={t("settings.enabled")} /> {name} {form.kind} {t("settings.modelCount", { n: models.length || discovered.length })} {status && ( {status.up ? t("settings.providerUp", { ms: status.latency_ms }) : status.state === "auth" ? t("settings.providerAuth") : t("settings.providerDown")} )}

{open && ( <>
setForm({ ...form, base_url: e.target.value })} /> setForm({ ...form, api_key: e.target.value })} />
{t("settings.apiKeyHint")}
setForm({ ...form, auth_prefix: e.target.value })} /> setForm({ ...form, monthly_budget: e.target.value })} /> {form.kind === "anthropic" && ( <> setForm({ ...form, admin_key: e.target.value })} />
{t("settings.adminKeyHint")}
)} setForm({ ...form, max_concurrency: e.target.value })} />
{t("settings.maxConcurrencyHint")}

{t("settings.models")} {form.base_url?.includes("openrouter.ai") && ( )} {discovered.length > 0 && ( )}

{t("settings.pricesHint")}
{priceNote &&
{priceNote}
}
{models.map((m, i) => ( {(["input_price", "output_price", "cache_read_price", "cache_write_price"] as const).map((k) => ( ))} ))}
ID {t("settings.context")} {t("settings.maxOutput")} {t("settings.effort")} {t("settings.priceIn")} {t("settings.priceOut")} {t("settings.priceCacheRead")} {t("settings.priceCacheWrite")} {t("settings.slots")} {t("settings.fallback")}
setModel(i, { id: e.target.value })} style={{ minWidth: 160 }} /> setModel(i, { context: num(e.target.value) })} /> setModel(i, { max_output: num(e.target.value) })} /> setModel(i, { reasoning: e.target.checked })} /> setModel(i, { vision: e.target.checked })} /> setModel(i, { [k]: num(e.target.value) })} /> setModel(i, { max_concurrency: num(e.target.value) })} /> setModel(i, { fallback: v || undefined })} emptyLabel="–" />
{models.length === 0 &&
{t("settings.modelsEmptyHint")}
} {view}
)}
); } function Providers({ cfg, reload }: { cfg: ConfigResponse; reload: () => void }) { const t = useT(); const [name, setName] = useState(""); const [preset, setPreset] = useState("anthropic"); const { run, view } = useSaver(reload); const providers = cfg.config.providers ?? {}; const [status, setStatus] = useState>({}); const [checking, setChecking] = useState(false); useEffect(() => { get>("/providers/status").then(setStatus).catch(() => {}); }, []); const recheck = () => { setChecking(true); post>("/providers/status") .then(setStatus) .catch(() => {}) .finally(() => setChecking(false)); }; const add = () => { const n = name.trim() || preset; if (!/^[a-zA-Z0-9_-]+$/.test(n)) return toast(t("settings.invalidName")); const p = PRESETS[preset]; const value: Record = { kind: p.kind, base_url: p.base_url, api_key: p.api_key, enabled: true, models: [] }; if (p.auth_prefix) value.auth_prefix = p.auth_prefix; void run(() => savePatches([{ path: ["providers", n], value }])); setName(""); }; return ( <>
{Object.entries(providers).map(([n, p]) => ( ))}

{t("settings.addProvider")}

setName(e.target.value)} />
{view}
); } // --- MCP -------------------------------------------------------------------- function linesToMap(text: string, sep: string): Record { const out: Record = {}; for (const line of text.split("\n")) { const i = line.indexOf(sep); if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + sep.length).trim(); } return out; } function mapToLines(m: Record | undefined, sep: string) { return Object.entries(m ?? {}) .map(([k, v]) => `${k}${sep}${v}`) .join("\n"); } function McpEditor({ name, m, reload, isNew }: { name: string; m: Json; reload: () => void; isNew?: boolean }) { const t = useT(); const status = useStore((s) => s.mcp[name]); const [form, setForm] = useState({ name, type: m.type ?? "stdio", command: m.command ?? "", args: (m.args ?? []).join("\n"), env: mapToLines(m.env, "="), url: m.url ?? "", headers: mapToLines(m.headers, ": "), enabled: m.enabled ?? true, load: m.load ?? "auto", users: (m.users ?? []).join(", "), }); const [open, setOpen] = useState(!!isNew); const { run, view } = useSaver(reload); const save = () => { const n = form.name.trim(); if (!/^[a-zA-Z0-9_-]+$/.test(n)) return toast(t("settings.invalidName")); const value: Record = { type: form.type, enabled: form.enabled }; if (form.load !== "auto") value.load = form.load; const users = form.users.split(/[\s,]+/).filter(Boolean); if (users.length) value.users = users; if (form.type === "stdio") { value.command = form.command; value.args = form.args.split("\n").map((s: string) => s.trim()).filter(Boolean); value.env = linesToMap(form.env, "="); } else { value.url = form.url; value.headers = linesToMap(form.headers, ":"); } return run(async () => { const patches: Patch[] = []; if (!isNew && n !== name) patches.push({ path: ["mcp", name], value: null }); patches.push({ path: ["mcp", n], value }); await savePatches(patches); }); }; const st = status?.status; const stateLabel = st ? t(`mcp.state.${st.state}` as Key) : isNew ? "" : t("mcp.state.unknown"); return (

{isNew ? t("settings.addMcp") : name} {!isNew && {stateLabel}} {st?.state === "connected" && {t("mcp.tools", { n: st.tools ?? 0 })}} {!isNew && ( )}

{st?.state === "failed" &&
{st.error}
} {open && ( <>
setForm({ ...form, name: e.target.value })} /> {form.type === "stdio" ? ( <> setForm({ ...form, command: e.target.value })} placeholder="npx" />