Agents page: collapse anatomy into icon cards → pretty editable modal
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 8s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped

The agent command center's BRAIN+SURFACE columns are now a grid of compact icon
cards (system prompt / how it operates / personality / skills / capabilities /
tools / memory / safety). Clicking a card opens a modal with the full content,
nicely formatted (markdown/persona/tags), editable for the four brain text files
(system_prompt, agent_md, persona, skills_md) and saved back to the .brain via
PATCH /api/claws/{id}/brain. Capabilities/tools/memory/safety render read-only
(tools keeps its Add affordance). LIVE column unchanged.

- frontend: new AnatomyGrid (icon cards + SectionModal); ClawCommandCenter swaps the
  two verbose columns for it; skills_md added to RawBrain types.
- backend: cm-brain skills_md() getter + skills_md in ClawBrainResponse so the skills
  editor pre-fills (avoids blank-overwrite).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-27 11:25:20 -07:00
co-authored by Claude Opus 4.8
parent 66c4a80985
commit d9ee9f5328
6 changed files with 215 additions and 58 deletions
@@ -0,0 +1,199 @@
"use client";
// The agent's "anatomy" as a grid of compact icon cards. Each card opens a modal
// with the full, pretty-formatted section content. The four brain text files
// (system prompt / how it operates / personality / skills) are editable in the
// modal and saved back to the .brain via PATCH /api/claws/{id}/brain; the derived
// sections (capabilities / tools / memory / safety) render read-only (tools keeps
// its "add tool" affordance).
import { useState, type ReactNode } from "react";
import { Check, Cpu, Database, Drama, Pencil, Plus, ScrollText, ShieldCheck, Workflow, Wrench, X, Zap } from "lucide-react";
import type { DemoAgent } from "@/lib/dashboard-demo";
import { MarkdownText, PersonalityBody, mono, tag, type RawBrain } from "./anatomy-cards";
type EditField = "system_prompt" | "agent_md" | "persona" | "skills_md";
interface Section {
key: string;
label: string;
icon: ReactNode;
tint: string;
status?: string; // small hint on the card (count / "editable")
field?: EditField; // present ⇒ editable + saved to the brain
value?: string; // editable source text
placeholder?: string;
body: ReactNode; // formatted, read-only display for the modal
}
function IconCard({ section, onClick }: { section: Section; onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
title={`Open ${section.label}`}
style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 11, padding: 15, borderRadius: 13, background: "#0f0f13", border: `1px solid ${section.tint}38`, boxShadow: "0 8px 22px rgba(0,0,0,.4)", cursor: "pointer", textAlign: "left", minHeight: 104, transition: "border-color .15s ease, transform .1s ease" }}
>
<span style={{ width: 36, height: 36, flex: "none", borderRadius: 10, background: `${section.tint}26`, border: `1px solid ${section.tint}45`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: section.tint }}>{section.icon}</span>
<span style={{ fontFamily: mono, fontSize: 12, fontWeight: 600, letterSpacing: ".04em", color: section.tint, lineHeight: 1.2 }}>{section.label}</span>
<span style={{ marginTop: "auto", display: "flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 9.5, color: "#6a6a72" }}>
{section.status ? <span>{section.status}</span> : null}
{section.field ? <span style={{ display: "inline-flex", alignItems: "center", gap: 3, color: "#7a7a82" }}><Pencil size={9} />edit</span> : null}
</span>
</button>
);
}
function SectionModal({ section, clawId, onClose, onSaved, onAddTool }: { section: Section; clawId: string; onClose: () => void; onSaved?: () => void; onAddTool?: () => void }) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(section.value ?? "");
const [saving, setSaving] = useState(false);
const save = async () => {
if (!section.field) return;
setSaving(true);
try {
await fetch(`/api/claws/${clawId}/brain`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [section.field]: draft }) });
setEditing(false);
onSaved?.();
} catch {
/* keep the editor open on failure */
} finally {
setSaving(false);
}
};
return (
<div
role="dialog"
aria-modal="true"
aria-label={section.label}
onClick={onClose}
style={{ position: "fixed", inset: 0, zIndex: 200, background: "rgba(0,0,0,.62)", backdropFilter: "blur(2px)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}
>
<div onClick={(e) => e.stopPropagation()} style={{ width: "min(760px, 94vw)", maxHeight: "84vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0f0f13", border: `1px solid ${section.tint}45`, boxShadow: "0 30px 80px rgba(0,0,0,.6)" }}>
{/* Header */}
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 11, padding: "16px 18px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
<span style={{ width: 30, height: 30, flex: "none", borderRadius: 8, background: `${section.tint}26`, border: `1px solid ${section.tint}45`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: section.tint }}>{section.icon}</span>
<span style={{ fontFamily: mono, fontSize: 13, fontWeight: 600, letterSpacing: ".05em", color: section.tint }}>{section.label}</span>
<span style={{ flex: 1 }} />
{section.field && !editing ? (
<button type="button" onClick={() => { setDraft(section.value ?? ""); setEditing(true); }} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "5px 11px", borderRadius: 7, border: "1px solid rgba(255,255,255,.14)", background: "transparent", color: "#cfcfd5", fontSize: 12, cursor: "pointer" }}><Pencil size={12} />Edit</button>
) : null}
<button type="button" onClick={onClose} aria-label="Close" style={{ width: 30, height: 30, flex: "none", borderRadius: 8, border: "1px solid rgba(255,255,255,.1)", background: "transparent", color: "#8a8a92", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}><X size={15} /></button>
</div>
{/* Body */}
<div style={{ minHeight: 0, flex: 1, overflowY: "auto", padding: "18px 20px" }}>
{editing ? (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
<textarea value={draft} onChange={(e) => setDraft(e.target.value)} placeholder={section.placeholder} autoFocus
style={{ width: "100%", minHeight: 320, resize: "vertical", boxSizing: "border-box", borderRadius: 9, border: "1px solid rgba(255,255,255,.14)", background: "#0b0b0e", color: "#e6e6ea", padding: 12, fontFamily: mono, fontSize: 12.5, lineHeight: 1.6 }} />
<div style={{ display: "flex", gap: 8 }}>
<button type="button" onClick={save} disabled={saving} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "7px 14px", borderRadius: 7, border: 0, background: "#5fd08a", color: "#06140c", fontSize: 12.5, fontWeight: 700, cursor: "pointer" }}><Check size={13} />{saving ? "Saving…" : "Save to brain"}</button>
<button type="button" onClick={() => setEditing(false)} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "7px 14px", borderRadius: 7, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#cfcfd5", fontSize: 12.5, cursor: "pointer" }}><X size={13} />Cancel</button>
</div>
</div>
) : (
section.body
)}
</div>
{/* Footer affordance (tools) */}
{section.key === "tools" && onAddTool && !editing ? (
<div style={{ flex: "none", padding: "12px 20px", borderTop: "1px solid rgba(255,255,255,.06)" }}>
<button type="button" onClick={() => { onClose(); onAddTool(); }} style={{ width: "100%", padding: "9px 0", borderRadius: 8, border: "1px dashed rgba(94,200,216,.4)", background: "transparent", color: "#5ec8d8", fontSize: 12.5, fontFamily: mono, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6 }}><Plus size={13} />Add tool</button>
</div>
) : null}
</div>
</div>
);
}
const muted = (t: string): ReactNode => <span style={{ fontFamily: mono, fontSize: 12.5, color: "#6a6a72" }}>{t}</span>;
const row = (a: string, b: string, bc = "#cfcfd5"): ReactNode => (
<div style={{ display: "flex", fontFamily: mono, fontSize: 12.5 }}><span style={{ flex: 1, color: "#9a9aa2" }}>{a}</span><span style={{ color: bc }}>{b}</span></div>
);
export function AnatomyGrid({ agent, brain, onSaved, onAddTool }: { agent: DemoAgent; brain?: RawBrain; onSaved?: () => void; onAddTool?: () => void }) {
const [open, setOpen] = useState<string | null>(null);
const c = agent.compartments;
const systemPrompt = brain?.system_prompt ?? agent.systemPrompt ?? "";
const skillsList = brain?.skills ?? [];
const sections: Section[] = [
{
key: "system_prompt", label: "SYSTEM PROMPT", icon: <ScrollText size={17} />, tint: "#ff6f61",
field: "system_prompt", value: systemPrompt, placeholder: "System prompt / job description…",
body: systemPrompt.trim() ? <MarkdownText text={systemPrompt} baseSize={14} /> : muted("No system prompt set — click Edit to write one."),
},
{
key: "agent_md", label: "HOW IT OPERATES", icon: <Workflow size={17} />, tint: "#ff8a7a",
field: "agent_md", value: brain?.agent_md ?? "", placeholder: "AGENTS.md — how this agent operates (workflow, rules)…",
body: brain?.agent_md?.trim() ? <MarkdownText text={brain.agent_md} baseSize={14} /> : muted("No AGENTS.md yet — click Edit to define how this agent operates."),
},
{
key: "personality", label: "PERSONALITY", icon: <Drama size={17} />, tint: "#c98af0",
field: "persona", value: brain?.personality ?? "", placeholder: "Personality — tone, traits, voice…",
body: <PersonalityBody raw={brain?.personality} fallback={c.personality} />,
},
{
key: "skills", label: "SKILLS", icon: <Zap size={17} />, tint: "#ff6f61",
status: String(skillsList.length || c.skills.length), field: "skills_md", value: brain?.skills_md ?? "",
placeholder: "Skills — a narrative of what this agent is good at…",
body: brain?.skills_md?.trim() ? (
<MarkdownText text={brain.skills_md} baseSize={14} />
) : skillsList.length ? (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{skillsList.map((s) => (
<div key={s.name}>
<div style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 700, color: "#ff8a7a", marginBottom: 4 }}>{s.name}</div>
<div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.55 }}>{s.body}</div>
</div>
))}
</div>
) : c.skills.length ? (
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{c.skills.map((s) => <span key={s} style={tag("#ff6f61")}>{s}</span>)}</div>
) : muted("No skills yet — click Edit to describe this agent's skills."),
},
{
key: "capabilities", label: "CAPABILITIES", icon: <Cpu size={17} />, tint: "#e8b465",
status: String(c.capabilities.length),
body: c.capabilities.length ? <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{c.capabilities.map((p) => <span key={p} style={tag("#e8b465")}>{p}</span>)}</div> : muted("No capabilities listed."),
},
{
key: "tools", label: "TOOLS", icon: <Wrench size={17} />, tint: "#5ec8d8",
status: String(c.tools.length),
body: c.tools.length ? (
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
{c.tools.map((t) => row(t.name, t.state === "gated" ? "gated ✓" : "blocked ", t.state === "gated" ? "#5fd08a" : "#e8b465"))}
</div>
) : muted("No tools yet — use Add tool below."),
},
{
key: "memory", label: "MEMORY", icon: <Database size={17} />, tint: "#5fd08a",
status: brain ? `${brain.stats.memories}` : undefined,
body: brain && brain.memory.length ? (
<div style={{ display: "flex", flexDirection: "column", gap: 7, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2" }}>
{brain.memory.map((m, i) => <div key={i} style={{ lineHeight: 1.5 }}>{m}</div>)}
</div>
) : muted("No memories yet — they accrue as this agent runs and chats."),
},
{
key: "safety", label: "SAFETY · §15", icon: <ShieldCheck size={17} />, tint: "#6fd0c0",
body: <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>{row("Sandbox", c.safety.sandbox, "#6fd0c0")}{row("Network", c.safety.network, "#6fd0c0")}</div>,
},
];
const active = sections.find((s) => s.key === open) ?? null;
return (
<>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: 12, alignContent: "start" }}>
{sections.map((s) => <IconCard key={s.key} section={s} onClick={() => setOpen(s.key)} />)}
</div>
{active ? <SectionModal section={active} clawId={agent.id} onClose={() => setOpen(null)} onSaved={onSaved} onAddTool={onAddTool} /> : null}
</>
);
}