Command center: edit brain sections inline from the cards
ci / gates (push) Failing after 11s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped

Add PATCH /api/claws/{id}/brain (edit_brain): writes any of system_prompt / agent_md
/ persona / skills_md into the claw's .brain (best-effort) + persists system_prompt
to Postgres (authoritative) + commits a ClawSync revision.

Frontend: a reusable EditableSection (pencil → textarea → Save/Cancel → PATCH →
re-fetch brain). The SYSTEM PROMPT, HOW I OPERATE (AGENTS.md), and PERSONALITY cards
in the command center are now editable inline; saving writes back to the mapped
brain section. AGENTS.md card now always shows (so it can be authored when empty).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-26 13:30:58 -07:00
co-authored by Claude Opus 4.8
parent 9fca3f6676
commit 89c147b742
4 changed files with 112 additions and 12 deletions
+1 -1
View File
@@ -172,7 +172,7 @@ pub fn router(state: AppState) -> Router {
"/api/claws/{id}/compartments",
get(routes::claws::compartments),
)
.route("/api/claws/{id}/brain", get(routes::claws::brain))
.route("/api/claws/{id}/brain", get(routes::claws::brain).patch(routes::claws::edit_brain))
.route(
"/api/claws/{id}/brain/push",
axum::routing::post(routes::claws::push_brain),
+47
View File
@@ -218,6 +218,53 @@ fn load_brain(agent: &Agent, skills: &[(String, String)]) -> ClawBrainResponse {
}
}
/// Edit one or more brain sections from the command-center cards.
#[derive(Deserialize)]
pub struct BrainEdit {
pub system_prompt: Option<String>,
pub agent_md: Option<String>,
pub persona: Option<String>,
pub skills_md: Option<String>,
}
/// `PATCH /api/claws/{id}/brain` — edit individual `.brain` sections inline from
/// the dashboard cards. Each present field is written to the claw's brain;
/// `system_prompt` is also persisted to Postgres (authoritative). Commits a
/// ClawSync revision so every edit is reversible.
pub async fn edit_brain(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<AgentId>,
Json(req): Json<BrainEdit>,
) -> Result<Json<Value>, ApiError> {
let agent = workspace_agent(&state, &user, id).await?;
// system_prompt stays authoritative in Postgres.
if let Some(sp) = req.system_prompt.as_ref() {
let _ = cm_db::repo::agents::update_profile(
&state.pool, agent.id, None, None, Some(sp.trim()), None, None, None,
)
.await;
}
// Mirror every edited section into the `.brain` (best-effort) + snapshot.
let path = brain_dir().join(format!("claw_{}.h5", agent.id));
if let Ok(mut brain) = cm_brain::ClawBrain::open_or_create(&path, &agent.id.to_string()) {
if let Some(sp) = req.system_prompt.as_ref() {
let _ = brain.set_system_prompt(sp);
}
if let Some(a) = req.agent_md.as_ref() {
let _ = brain.set_agent_md(a);
}
if let Some(p) = req.persona.as_ref() {
let _ = brain.set_personality(p);
}
if let Some(s) = req.skills_md.as_ref() {
let _ = brain.set_skills_md(s);
}
let _ = brain.commit(Some("edited from dashboard"));
}
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn brain(
State(state): State<AppState>,
Authed(user): Authed,
@@ -11,7 +11,7 @@ import { Activity, Brain, Camera, Cpu, Database, Drama, Settings, ShieldCheck, W
import type { DemoAgent } from "@/lib/dashboard-demo";
import { useAgentTelemetry, useLiveEvent } from "@/lib/live/useClawmatesLive";
import { AnatomyCard, MarkdownText, PersonalityBody, SystemPromptCard, mono, tag, type RawBrain } from "./anatomy-cards";
import { AnatomyCard, EditableSection, MarkdownText, PersonalityBody, SystemPromptCard, mono, tag, type RawBrain } from "./anatomy-cards";
import { AvatarModal } from "./AvatarModal";
import { AddToolModal } from "./AddToolModal";
@@ -231,14 +231,16 @@ export function ClawCommandCenter({
{/* BRAIN */}
<div style={colStyle}>
<SystemPromptCard prompt={agent.systemPrompt} />
{brain?.agent_md ? (
<AnatomyCard tint="#ff8a7a" label="HOW I OPERATE · AGENTS.md" icon={<Workflow size={15} />} collapsible>
<MarkdownText text={brain.agent_md} baseSize={13.5} />
</AnatomyCard>
) : null}
<SystemPromptCard prompt={agent.systemPrompt} clawId={agent.id} onSaved={onToolsChanged} />
<AnatomyCard tint="#ff8a7a" label="HOW I OPERATE · AGENTS.md" icon={<Workflow size={15} />} collapsible>
<EditableSection clawId={agent.id} field="agent_md" value={brain?.agent_md ?? ""} onSaved={onToolsChanged} placeholder="AGENTS.md — how this agent operates (workflow, rules)…">
{brain?.agent_md ? <MarkdownText text={brain.agent_md} baseSize={13.5} /> : <span style={{ fontFamily: mono, fontSize: 11.5, color: "#6a6a72" }}>No AGENTS.md yet click to define how this agent operates.</span>}
</EditableSection>
</AnatomyCard>
<AnatomyCard tint="#c98af0" label="PERSONALITY" icon={<Drama size={15} />} collapsible>
<PersonalityBody raw={brain?.personality} fallback={c.personality} />
<EditableSection clawId={agent.id} field="persona" value={brain?.personality ?? ""} onSaved={onToolsChanged} placeholder="Personality — tone, traits, voice…">
<PersonalityBody raw={brain?.personality} fallback={c.personality} />
</EditableSection>
</AnatomyCard>
<AnatomyCard tint="#ff6f61" label="SKILLS" count={String(c.skills.length)} icon={<Zap size={15} />}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
@@ -5,10 +5,56 @@
// from Dashboard.tsx's in-file helpers.
import { useState, type CSSProperties } from "react";
import { ScrollText } from "lucide-react";
import { Check, Pencil, ScrollText, X } from "lucide-react";
export const mono = "'JetBrains Mono', ui-monospace, monospace";
/** A brain section: rendered read-only with a pencil; click to edit as raw text
* and PATCH it back to `/api/claws/{id}/brain`. `children` is the display;
* `value` is the editable source. `onSaved` should re-fetch the brain. */
export function EditableSection({ clawId, field, value, onSaved, children, placeholder }: {
clawId: string;
field: "system_prompt" | "agent_md" | "persona" | "skills_md";
value: string;
onSaved?: () => void;
children: React.ReactNode;
placeholder?: string;
}) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(value);
const [saving, setSaving] = useState(false);
const save = async () => {
setSaving(true);
try {
await fetch(`/api/claws/${clawId}/brain`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [field]: draft }) });
setEditing(false);
onSaved?.();
} catch {
/* keep the editor open on failure */
} finally {
setSaving(false);
}
};
if (editing) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<textarea value={draft} onChange={(e) => setDraft(e.target.value)} placeholder={placeholder} autoFocus
style={{ width: "100%", minHeight: 130, resize: "vertical", boxSizing: "border-box", borderRadius: 8, border: "1px solid rgba(255,255,255,.14)", background: "#0b0b0e", color: "#e6e6ea", padding: 10, fontFamily: mono, fontSize: 12.5, lineHeight: 1.55 }} />
<div style={{ display: "flex", gap: 8 }}>
<button type="button" onClick={save} disabled={saving} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "5px 12px", borderRadius: 7, border: 0, background: "#5fd08a", color: "#06140c", fontSize: 12, fontWeight: 700, cursor: "pointer" }}><Check size={13} />{saving ? "Saving…" : "Save"}</button>
<button type="button" onClick={() => setEditing(false)} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "5px 12px", borderRadius: 7, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#cfcfd5", fontSize: 12, cursor: "pointer" }}><X size={13} />Cancel</button>
</div>
</div>
);
}
return (
<div style={{ position: "relative" }}>
<button type="button" onClick={() => { setDraft(value); setEditing(true); }} aria-label="Edit" title="Edit" style={{ position: "absolute", top: -2, right: -2, zIndex: 2, display: "inline-flex", alignItems: "center", justifyContent: "center", width: 24, height: 24, borderRadius: 6, border: "1px solid rgba(255,255,255,.1)", background: "rgba(8,8,10,.55)", color: "#8a8a92", cursor: "pointer" }}><Pencil size={12} /></button>
{children}
</div>
);
}
// `GET /api/claws/{id}/brain` — the claw's .brain (cm-brain / ClawhDF5).
export type RawBrain = {
exists: boolean;
@@ -66,8 +112,9 @@ export function CollapseTick({ open, color, onClick }: { open: boolean; color: s
);
}
export function SystemPromptCard({ prompt }: { prompt: string }) {
export function SystemPromptCard({ prompt, clawId, onSaved }: { prompt: string; clawId?: string; onSaved?: () => void }) {
const [open, setOpen] = useState(true);
const display = prompt?.trim() ? <MarkdownText text={prompt} baseSize={14} /> : <p style={{ fontSize: 13, color: "#6a6a72", margin: 0 }}>No system prompt set.</p>;
return (
<div style={{ borderRadius: 12, background: "#0f0f13", border: "1px solid rgba(255,111,97,.3)", padding: "16px 18px", boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: open ? 13 : 0 }}>
@@ -76,7 +123,11 @@ export function SystemPromptCard({ prompt }: { prompt: string }) {
<span style={{ flex: 1 }} />
<CollapseTick open={open} color="#ff8a7a" onClick={() => setOpen((o) => !o)} />
</div>
{open ? (prompt?.trim() ? <MarkdownText text={prompt} baseSize={14} /> : <p style={{ fontSize: 13, color: "#6a6a72", margin: 0 }}>No system prompt set.</p>) : null}
{open ? (
clawId ? (
<EditableSection clawId={clawId} field="system_prompt" value={prompt ?? ""} onSaved={onSaved} placeholder="System prompt / job description…">{display}</EditableSection>
) : display
) : null}
</div>
);
}