Command center: edit brain sections inline from the cards
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:
co-authored by
Claude Opus 4.8
parent
9fca3f6676
commit
89c147b742
@@ -172,7 +172,7 @@ pub fn router(state: AppState) -> Router {
|
|||||||
"/api/claws/{id}/compartments",
|
"/api/claws/{id}/compartments",
|
||||||
get(routes::claws::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(
|
.route(
|
||||||
"/api/claws/{id}/brain/push",
|
"/api/claws/{id}/brain/push",
|
||||||
axum::routing::post(routes::claws::push_brain),
|
axum::routing::post(routes::claws::push_brain),
|
||||||
|
|||||||
@@ -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(
|
pub async fn brain(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Authed(user): Authed,
|
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 type { DemoAgent } from "@/lib/dashboard-demo";
|
||||||
import { useAgentTelemetry, useLiveEvent } from "@/lib/live/useClawmatesLive";
|
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 { AvatarModal } from "./AvatarModal";
|
||||||
import { AddToolModal } from "./AddToolModal";
|
import { AddToolModal } from "./AddToolModal";
|
||||||
|
|
||||||
@@ -231,14 +231,16 @@ export function ClawCommandCenter({
|
|||||||
|
|
||||||
{/* BRAIN */}
|
{/* BRAIN */}
|
||||||
<div style={colStyle}>
|
<div style={colStyle}>
|
||||||
<SystemPromptCard prompt={agent.systemPrompt} />
|
<SystemPromptCard prompt={agent.systemPrompt} clawId={agent.id} onSaved={onToolsChanged} />
|
||||||
{brain?.agent_md ? (
|
<AnatomyCard tint="#ff8a7a" label="HOW I OPERATE · AGENTS.md" icon={<Workflow size={15} />} collapsible>
|
||||||
<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)…">
|
||||||
<MarkdownText text={brain.agent_md} baseSize={13.5} />
|
{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>}
|
||||||
</AnatomyCard>
|
</EditableSection>
|
||||||
) : null}
|
</AnatomyCard>
|
||||||
<AnatomyCard tint="#c98af0" label="PERSONALITY" icon={<Drama size={15} />} collapsible>
|
<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>
|
||||||
<AnatomyCard tint="#ff6f61" label="SKILLS" count={String(c.skills.length)} icon={<Zap size={15} />}>
|
<AnatomyCard tint="#ff6f61" label="SKILLS" count={String(c.skills.length)} icon={<Zap size={15} />}>
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
|
||||||
|
|||||||
@@ -5,10 +5,56 @@
|
|||||||
// from Dashboard.tsx's in-file helpers.
|
// from Dashboard.tsx's in-file helpers.
|
||||||
|
|
||||||
import { useState, type CSSProperties } from "react";
|
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";
|
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).
|
// `GET /api/claws/{id}/brain` — the claw's .brain (cm-brain / ClawhDF5).
|
||||||
export type RawBrain = {
|
export type RawBrain = {
|
||||||
exists: boolean;
|
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 [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 (
|
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={{ 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 }}>
|
<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 }} />
|
<span style={{ flex: 1 }} />
|
||||||
<CollapseTick open={open} color="#ff8a7a" onClick={() => setOpen((o) => !o)} />
|
<CollapseTick open={open} color="#ff8a7a" onClick={() => setOpen((o) => !o)} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user