Agents page: reorganize into the Agent Command Center (per-agent live metrics)
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:
co-authored by
Claude Opus 4.8
parent
731adb6449
commit
11a1f22daa
@@ -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`}` });
|
||||
Reference in New Issue
Block a user