Agents page: reorganize into the Agent Command Center (per-agent live metrics)
ci / gates (push) Failing after 13s
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

Replace the single centered "anatomy profile" + resizable chat split with an
operator command center: compact 62px identity strip → 5-tile per-agent metrics
band → three independently-scrolling LIVE · BRAIN · SURFACE columns. Chat moves to
the computer's Chat app; the right computer pullout (DevicePanel) is untouched.

- backend: cm-api/routes/world.rs emits per-agent `telemetry{agentId,tokensPerMin,
  costPerHr,loops,doorsPending}` in the SSE loop, from 4 batched GROUP BY queries
  (usage_events tokens/min + credits/hr, active routines, pending approvals) — all
  real, no migration. taxonomy `telemetry` gains optional agentId; stateKey now
  keys it per-agent so slices don't clobber.
- frontend: new ClawCommandCenter + anatomy-cards (shared cards extracted from
  Dashboard); useAgentTelemetry(agentId) feeds the metric band (Doors amber>0/
  green=0); LIVE column streams the agent's task.update / reasoning.delta /
  tool.call (replaces the mocked VitalsCard heatmap with a live activity chart).
- Dashboard: left region → full-height ClawCommandCenter; chat launcher opens the
  computer Chat app; removed the dead anatomy cluster + unused imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-26 11:34:12 -07:00
co-authored by Claude Opus 4.8
parent 731adb6449
commit 11a1f22daa
9 changed files with 2883 additions and 261 deletions
@@ -0,0 +1,134 @@
"use client";
// Shared anatomy-card primitives — extracted so the agent command center
// (ClawCommandCenter) reuses the exact cards/styling. Behavior copied verbatim
// from Dashboard.tsx's in-file helpers.
import { useState, type CSSProperties } from "react";
import { ScrollText } from "lucide-react";
export const mono = "'JetBrains Mono', ui-monospace, monospace";
// `GET /api/claws/{id}/brain` — the claw's .brain (cm-brain / ClawhDF5).
export type RawBrain = {
exists: boolean;
system_prompt: string | null;
personality: string | null;
skills: { name: string; body: string }[];
tools: { name: string; state: string }[];
memory: string[];
stats: { skills: number; tools: number; memories: number };
} | null;
// Inline markdown: **bold** and `code` → styled spans (everything else verbatim).
function renderInline(text: string, kp: string): React.ReactNode[] {
return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).filter(Boolean).map((p, i) => {
if (p.startsWith("**") && p.endsWith("**")) return <strong key={`${kp}-${i}`} style={{ color: "#fff", fontWeight: 700 }}>{p.slice(2, -2)}</strong>;
if (p.startsWith("`") && p.endsWith("`")) return <code key={`${kp}-${i}`} style={{ fontFamily: mono, fontSize: "0.9em", background: "rgba(255,255,255,.06)", padding: "1px 5px", borderRadius: 4, color: "#e6e6ea" }}>{p.slice(1, -1)}</code>;
return <span key={`${kp}-${i}`}>{p}</span>;
});
}
// A tiny, dependency-free Markdown renderer: headings, bullet lists, bold/code,
// paragraphs. Turns the raw prompt into readable prose instead of literal `##`.
export function MarkdownText({ text, baseSize = 14 }: { text: string; baseSize?: number }) {
const lines = text.replace(/\r\n/g, "\n").split("\n");
const blocks: React.ReactNode[] = [];
let para: string[] = [];
let list: string[] = [];
const flushPara = () => {
if (para.length) { const k = `p${blocks.length}`; blocks.push(<p key={k} style={{ margin: "0 0 10px", fontSize: baseSize, lineHeight: 1.65, color: "#d6d6dc" }}>{renderInline(para.join(" "), k)}</p>); para = []; }
};
const flushList = () => {
if (list.length) { const k = `u${blocks.length}`; blocks.push(<ul key={k} style={{ margin: "0 0 10px", paddingLeft: 18, display: "flex", flexDirection: "column", gap: 5 }}>{list.map((li, i) => <li key={i} style={{ fontSize: baseSize, lineHeight: 1.55, color: "#d6d6dc" }}>{renderInline(li, `${k}-${i}`)}</li>)}</ul>); list = []; }
};
for (const raw of lines) {
const line = raw.replace(/\s+$/, "");
const h = line.match(/^(#{1,6})\s+(.*)$/);
const li = line.match(/^\s*[-*]\s+(.*)$/);
if (h) { flushPara(); flushList(); const lvl = h[1].length; const size = lvl <= 1 ? baseSize + 5 : lvl === 2 ? baseSize + 2 : baseSize + 1; const k = `h${blocks.length}`; blocks.push(<div key={k} style={{ fontSize: size, fontWeight: 700, color: "#fff", letterSpacing: "-.01em", margin: blocks.length ? "15px 0 7px" : "0 0 7px" }}>{renderInline(h[2], k)}</div>); }
else if (li) { flushPara(); list.push(li[1]); }
else if (line.trim() === "") { flushPara(); flushList(); }
else { flushList(); para.push(line.replace(/^\s*>\s?/, "")); }
}
flushPara(); flushList();
return <div>{blocks}</div>;
}
// A same-colored triangle toggle: points down when open, left when closed.
export function CollapseTick({ open, color, onClick }: { open: boolean; color: string; onClick: () => void }) {
return (
<button type="button" onClick={onClick} aria-label={open ? "Collapse" : "Expand"} aria-expanded={open}
style={{ flex: "none", border: 0, background: "transparent", cursor: "pointer", color, padding: 4, display: "inline-flex", alignItems: "center", justifyContent: "center" }}>
<span style={{ display: "inline-block", fontSize: 11, lineHeight: 1, transform: open ? "rotate(0deg)" : "rotate(90deg)", transition: "transform .15s ease" }}></span>
</button>
);
}
export function SystemPromptCard({ prompt }: { prompt: string }) {
const [open, setOpen] = useState(true);
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 }}>
<span style={{ width: 26, height: 26, flex: "none", borderRadius: 7, background: "rgba(255,111,97,.18)", border: "1px solid rgba(255,111,97,.35)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#ff8a7a" }}><ScrollText size={15} /></span>
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, letterSpacing: ".05em", color: "#ff8a7a" }}>SYSTEM PROMPT</span>
<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}
</div>
);
}
// Personality is often stored as JSON (traits/tone/…). Render it as labeled
// sections instead of dumping raw JSON; fall back to tags / text otherwise.
export function PersonalityBody({ raw, fallback }: { raw: string | null | undefined; fallback: string[] }) {
const tags = (arr: string[]) => <div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>{arr.map((p, i) => <span key={i} style={tag("#c98af0")}>{p}</span>)}</div>;
const trimmed = (raw ?? "").trim();
let parsed: unknown;
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
try { parsed = JSON.parse(trimmed); } catch { parsed = undefined; }
}
if (Array.isArray(parsed)) return tags(parsed.map((x) => String(x)));
if (parsed && typeof parsed === "object") {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
{Object.entries(parsed as Record<string, unknown>).map(([k, v]) => (
<div key={k}>
<div style={{ fontFamily: mono, fontSize: 10.5, letterSpacing: ".08em", color: "#b9a6e0", marginBottom: 5, textTransform: "uppercase" }}>{k.replace(/_/g, " ")}</div>
{Array.isArray(v)
? tags(v.map((x) => String(x)))
: v && typeof v === "object"
? <div style={{ display: "flex", flexDirection: "column", gap: 3, fontSize: 13, color: "#cfcfd5", lineHeight: 1.55 }}>{Object.entries(v as Record<string, unknown>).map(([kk, vv]) => <div key={kk}><span style={{ color: "#8a8a92" }}>{kk}:</span> {String(vv)}</div>)}</div>
: <div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.55 }}>{String(v)}</div>}
</div>
))}
</div>
);
}
if (fallback.length) return tags(fallback);
if (trimmed) return <div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.6 }}>{trimmed}</div>;
return <span style={{ fontSize: 12, color: "#6a6a72" }}></span>;
}
export function AnatomyCard({ tint, label, count, icon, collapsible, children }: { tint: string; label: string; count?: string; icon?: React.ReactNode; collapsible?: boolean; children: React.ReactNode }) {
const [open, setOpen] = useState(true);
const collapsed = !!collapsible && !open;
return (
<div style={{ width: "100%" }}>
<div style={{ boxSizing: "border-box", display: "flex", flexDirection: "column", borderRadius: 12, background: "#0f0f13", border: `1px solid ${tint}38`, padding: 12, boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: collapsed ? 0 : 11 }}>
<span style={{ width: 26, height: 26, flex: "none", borderRadius: 7, background: `${tint}26`, border: `1px solid ${tint}40`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: tint }}>{icon}</span>
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, letterSpacing: ".05em", color: tint }}>{label}</span>
{count || collapsible ? <span style={{ flex: 1 }} /> : null}
{count ? <span style={{ fontFamily: mono, fontSize: 10, color: "#5a5a62" }}>{count}</span> : null}
{collapsible ? <CollapseTick open={open} color={tint} onClick={() => setOpen((o) => !o)} /> : null}
</div>
{!collapsed ? <div>{children}</div> : null}
</div>
</div>
);
}
// A pill-shaped tag, tinted to its section's accent. `dim` is the muted "+N" variant.
export const tag = (color = "#9a9aa2", dim = false): CSSProperties => ({ fontFamily: mono, fontSize: 12, color: dim ? "#8a8a92" : color, padding: "4px 10px", borderRadius: 7, background: dim ? "rgba(255,255,255,.04)" : `${color}1f`, border: `1px solid ${dim ? "rgba(255,255,255,.08)" : `${color}45`}` });