/** * @file AgentInstructionsSettings — Settings → AI tools → My agent * instructions (issue #67): the account's own rules in markdown, put before * every workspace's own instructions in each new Claude or Codex session of a * workspace it owns (`GET/PUT /api/me/instructions`). Saved per account. A * failed load says so with a Retry instead of leaving a dead, empty editor — * the editor stays read-only until the saved text is known, so a save can * never overwrite instructions it did not load. **Choose file** reads a local * Markdown/text file in the browser into the draft (nothing is uploaded until * Save, and Discard brings the saved text back). "Applies to" lists only the * installed, enabled tools whose panes the server says receive them. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useRef, useState, type ChangeEvent } from "react"; import { FileUp } from "lucide-react"; import { useTranslation } from "react-i18next"; import { api, type AgentInstructionsView } from "../../lib/api"; import { useAiTools } from "../../hooks/useAiTools"; import { AiToolIcon } from "../AiToolIcon"; import { toast } from "../../lib/toast"; import { SettingsSection } from "./SettingsRow"; export function AgentInstructionsSettings() { const { t } = useTranslation("settings"); const [saved, setSaved] = useState(null); const [draft, setDraft] = useState(""); const [max, setMax] = useState(); const [toolIds, setToolIds] = useState([]); const { tools } = useAiTools(); const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const fileInput = useRef(null); const apply = (r: AgentInstructionsView) => { setSaved(r.instructions); setDraft(r.instructions); setMax(r.max); setToolIds(r.tools ?? []); }; const load = () => { setLoadError(null); api.me .instructions() .then(apply) .catch((err) => setLoadError(err instanceof Error ? err.message : String(err))); }; useEffect(load, []); async function save() { setSaving(true); try { apply(await api.me.saveInstructions(draft)); toast.success(t("agentInstructions.saved")); } catch (err) { toast.error( t("agentInstructions.saveFailed", { message: err instanceof Error ? err.message : "" }) ); } finally { setSaving(false); } } async function pickFile(e: ChangeEvent) { const file = e.target.files?.[0]; e.target.value = ""; // the same file can be picked again after a Discard if (!file) return; const text = await file.text(); if (max && text.length > max) { toast.error( t("agentInstructions.fileTooBig", { name: file.name, size: text.length.toLocaleString(), max: max.toLocaleString(), }) ); return; } setDraft(text); } const appliesTo = tools.filter((x) => x.installed && x.enabled && toolIds.includes(x.id)); const ready = saved !== null; const dirty = ready && draft !== saved; return (
{appliesTo.length > 0 && (
{t("agentInstructions.appliesTo")} {appliesTo.map((x) => ( {x.label} ))}
)}