From 89c147b742f62022e139de5e1f4a8387224778de Mon Sep 17 00:00:00 2001 From: Omar Sobh Date: Fri, 26 Jun 2026 13:30:58 -0700 Subject: [PATCH] Command center: edit brain sections inline from the cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/cm-api/src/lib.rs | 2 +- crates/cm-api/src/routes/claws.rs | 47 +++++++++++++++ .../dashboard/ClawCommandCenter.tsx | 18 +++--- .../components/dashboard/anatomy-cards.tsx | 57 ++++++++++++++++++- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/crates/cm-api/src/lib.rs b/crates/cm-api/src/lib.rs index d51cc28..cd5ba1c 100644 --- a/crates/cm-api/src/lib.rs +++ b/crates/cm-api/src/lib.rs @@ -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), diff --git a/crates/cm-api/src/routes/claws.rs b/crates/cm-api/src/routes/claws.rs index 6ad67e9..962eb02 100644 --- a/crates/cm-api/src/routes/claws.rs +++ b/crates/cm-api/src/routes/claws.rs @@ -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, + pub agent_md: Option, + pub persona: Option, + pub skills_md: Option, +} + +/// `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, + Authed(user): Authed, + Path(id): Path, + Json(req): Json, +) -> Result, 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, Authed(user): Authed, diff --git a/frontend/src/components/dashboard/ClawCommandCenter.tsx b/frontend/src/components/dashboard/ClawCommandCenter.tsx index f1c03d1..e1a44c2 100644 --- a/frontend/src/components/dashboard/ClawCommandCenter.tsx +++ b/frontend/src/components/dashboard/ClawCommandCenter.tsx @@ -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 */}
- - {brain?.agent_md ? ( - } collapsible> - - - ) : null} + + } collapsible> + + {brain?.agent_md ? : No AGENTS.md yet — click ✎ to define how this agent operates.} + + } collapsible> - + + + }>
diff --git a/frontend/src/components/dashboard/anatomy-cards.tsx b/frontend/src/components/dashboard/anatomy-cards.tsx index 07772be..6b8d5b0 100644 --- a/frontend/src/components/dashboard/anatomy-cards.tsx +++ b/frontend/src/components/dashboard/anatomy-cards.tsx @@ -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 ( +
+