import { Icon } from "../Icon";
import { useState } from "react";
import { del, patch } from "../../api";
import { useFetch } from "../../hooks";
import { useT } from "../../i18n";
import type { AgentInfo } from "../../types";
import { ModelSelect } from "../ModelSelect";
import { confirmDialog, promptDialog, toast } from "../../dialogs";
type Patch = { path: string[]; value: unknown };
export interface ToolInfo {
name: string;
read_only: boolean;
description: string;
}
/** Tool checkboxes: nothing checked = all tools. */
export function ToolPicker({ tools, value, onChange, readOnly }: { tools: ToolInfo[]; value: string[]; onChange: (v: string[]) => void; readOnly: boolean }) {
const t = useT();
const toggle = (n: string) => onChange(value.includes(n) ? value.filter((x) => x !== n) : [...value, n]);
// Names configured by hand (e.g. MCP patterns) stay selected even if not listed.
const extra = value.filter((v) => !tools.some((x) => x.name === v));
return (
);
}
const WRITER_PROMPT = `You are a technical writer. Produce clear, well-structured text (Markdown) for
documentation, summaries, step-by-step guides and translations.
Return only the finished text.`;
function AgentRow({ a, raw, tools, onSaved }: { a: AgentInfo; raw: any; tools: ToolInfo[]; onSaved: (ok: boolean, text: string) => void }) {
const t = useT();
const [open, setOpen] = useState(!!raw && !raw.prompt && !raw.description && raw.mode === "primary");
const [form, setForm] = useState({
model: a.model ?? "",
max_parallel: a.max_parallel ? String(a.max_parallel) : "",
description: raw?.description ?? "",
tools: (raw?.tools ?? []) as string[],
read_only: !!(raw?.read_only ?? a.read_only),
style: (raw?.style ?? a.style ?? "coding") as "coding" | "chat",
prompt: raw?.prompt ?? "",
mode: raw?.mode ?? a.mode,
refine_rounds: String(raw?.refine?.rounds ?? a.refine?.rounds ?? 0),
refine_model: raw?.refine?.model ?? "",
refine_criteria: raw?.refine?.criteria ?? "",
refine_after_todos: (raw?.refine?.after_todos ?? a.refine?.after_todos ?? true) as boolean,
reverse_tools: !!(raw?.reverse_tools ?? a.reverse),
});
const builtin = a.source === "builtin" || (a.source === "config" && !raw?.prompt && ["build", "plan", "craft", "chat", "general", "explore"].includes(a.name));
const save = async () => {
const base = ["agents", a.name];
const patches: Patch[] = [
{ path: [...base, "model"], value: form.model || null },
{ path: [...base, "max_parallel"], value: form.max_parallel ? Number(form.max_parallel) : null },
];
if (a.mode === "primary") {
const rounds = Math.max(0, Math.min(10, Number(form.refine_rounds) || 0));
patches.push({
path: [...base, "refine"],
value: rounds || form.refine_model || form.refine_criteria ? { rounds, model: form.refine_model || null, criteria: form.refine_criteria || null, after_todos: form.refine_after_todos } : null,
});
}
if (a.name !== "craft") patches.push({ path: [...base, "reverse_tools"], value: form.reverse_tools || null });
if (!builtin || form.description) patches.push({ path: [...base, "description"], value: form.description || null });
if (!builtin) {
patches.push({ path: [...base, "mode"], value: form.mode });
patches.push({ path: [...base, "tools"], value: form.tools.length ? form.tools : null });
patches.push({ path: [...base, "read_only"], value: form.read_only || null });
patches.push({ path: [...base, "style"], value: form.style === "chat" ? "chat" : null });
patches.push({ path: [...base, "prompt"], value: form.prompt || null });
}
try {
await patch("/config", { patches });
onSaved(true, t("settings.saved"));
} catch (e: any) {
onSaved(false, e.message);
}
};
const removeFile = async () => {
if (!(await confirmDialog({ message: t("agents.deleteFile", { path: a.path ?? a.name }), danger: true }))) return;
try {
await del(`/agents/${encodeURIComponent(a.name)}`);
onSaved(true, t("settings.saved"));
} catch (e: any) {
onSaved(false, e.message);
}
};
const reset = async () => {
if (!(await confirmDialog({ message: t(builtin ? "agents.resetConfirm" : "agents.deleteConfirm", { name: a.name }), danger: true }))) return;
try {
await patch("/config", { patches: [{ path: ["agents", a.name], value: null }] });
onSaved(true, t("settings.saved"));
} catch (e: any) {
onSaved(false, e.message);
}
};
return (
{a.name}
{a.mode === "primary" ? t("agents.modePrimary") : t("agents.modeSubagent")}
{t(`agents.source.${a.source}` as any)}
{a.read_only && {t("agents.readOnly")}}
{!builtin && (a.source === "config" || a.source === "file") && (
)}
{a.description}
{t("agents.model")}:
{a.model || t("agents.sessionModel")}
{a.max_parallel ? {t("agents.maxParallelShort", { n: a.max_parallel })} : null}
{a.refine?.rounds ? {t("agents.refineShort", { n: a.refine.rounds })} : null}
{open && (
<>
{a.source === "config" && builtin && (
)}
>
)}
);
}
export function AgentsEditor({ rawAgents, reloadConfig }: { rawAgents: Record; reloadConfig: () => void }) {
const t = useT();
const { data: all, reload } = useFetch("/agents");
// Personal modes are managed under "My modes".
const agents = all?.filter((a) => a.source !== "personal");
const tools = useFetch("/agents/tools").data ?? [];
const cmds = useFetch<{ commands: any[]; skills: any[] }>("/commands").data;
const [notice, setNotice] = useState<{ ok: boolean; text: string }>();
const [name, setName] = useState("");
const saved = (ok: boolean, text: string) => {
setNotice({ ok, text });
if (ok) {
reload();
reloadConfig();
}
};
const add = async (template: "writer" | "blank" | "mode") => {
const n = name.trim() || (template === "writer" ? "writer" : "");
if (!/^[a-zA-Z0-9_-]+$/.test(n)) return toast(t("settings.invalidName"));
const value =
template === "writer"
? { mode: "subagent", description: "Writes documentation, summaries, guides and translations.", tools: ["read", "glob", "grep"], max_parallel: 1, prompt: WRITER_PROMPT }
: template === "mode"
? { mode: "primary" }
: { mode: "subagent", description: "", prompt: "" };
try {
await patch("/config", { patches: [{ path: ["agents", n], value }] });
setName("");
saved(true, t("settings.saved"));
} catch (e: any) {
saved(false, e.message);
}
};
const key = (a: AgentInfo) => a.name + JSON.stringify(rawAgents[a.name] ?? {}) + (a.model ?? "");
return (
<>
{t("agents.intro")}
{notice && {notice.text}
}
{t("agents.primaryTitle")}
{agents?.filter((a) => a.mode === "primary").map((a) => )}
{t("agents.subagentTitle")}
{agents?.filter((a) => a.mode !== "primary").map((a) => )}
{t("agents.commands")}
{cmds?.commands.length ? cmds.commands.map((c) =>
/{c.name} – {c.description}
) :
–
}
{t("agents.skills")}
{cmds?.skills.length ? cmds.skills.map((s) =>
{s.name} – {s.description}
) :
–
}
>
);
}