agents page: 2-col metric grid + stacked full-width rows
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 3m12s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 34s

Reshuffles ClawCommandCenter so the layout stays calm whether or
not the computer panel is slid in from the right.

Top row — was 5 tiles in a horizontal strip that got crowded when
the computer opened; now a `grid-template-columns: repeat(2, 1fr)`
grid that wraps naturally to 3 rows for the 5 tiles.
- Doors / Memory (row 1)
- Loops / Spend (row 2)
- Activity · Live (row 3, spans 1 / -1 so it uses the full width)

Activity here is a new compact ActivityTile — same tool-call
bucket signal as the old bar viz, distilled into MetricTile shape
so it fits the grid. Shows total calls in the rolling 22s window
with a mini sparkline of per-second counts.

Main section — was a 2-column split (LIVE column + Anatomy column)
that shifted around when the panel opened. Now a single vertical
stack, every row full-width:
  Working on Now
  Reasoning Stream
  Throughput  (new ThroughputCard — wraps the same tok/min data
               in an AnatomyCard so the sparkline gets room; the
               old top-row ThroughputTile is retired)
  Anatomy Grid  (the "Dot Brain" and its neighbours)

Old ActivityBars component is retired — its signal lives in
ActivityTile at the top now. Unused colStyle removed.

Net: the top-of-fold reads as a scanner (four tiny numbers + one
live pulse), the stack below reads as narrative (what's happening,
how it thinks, how much it's producing, what it is). Both scroll
independently of the computer panel.
This commit is contained in:
Omar Sobh
2026-07-09 11:44:59 -07:00
parent acd2a0f287
commit aed21b654c
@@ -116,8 +116,10 @@ function ReasoningStream({ agentId }: { agentId: string }) {
); );
} }
function ActivityBars({ agentId }: { agentId: string }) { // Compact top-row tile — shows tool-call rate as a number with a mini
// Bucket tool-call counts into ~20 rolling time slots (one tick/sec). // sparkline of the rolling per-second counts. Same signal as the bigger
// bar viz, distilled into MetricTile shape so it sits in the 2×2 grid.
function ActivityTile({ agentId }: { agentId: string }) {
const SLOTS = 22; const SLOTS = 22;
const buckets = useRef<number[]>(new Array(SLOTS).fill(0)); const buckets = useRef<number[]>(new Array(SLOTS).fill(0));
const [bars, setBars] = useState<number[]>(() => new Array(SLOTS).fill(0)); const [bars, setBars] = useState<number[]>(() => new Array(SLOTS).fill(0));
@@ -131,31 +133,51 @@ function ActivityBars({ agentId }: { agentId: string }) {
}, 1000); }, 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
const max = Math.max(1, ...bars); const total = bars.reduce((a, b) => a + b, 0);
return ( return (
<AnatomyCard tint="#5ec8d8" label="ACTIVITY · LIVE" icon={<Activity size={15} />}> <MetricTile
<div style={{ display: "flex", alignItems: "flex-end", gap: 3, height: 64 }}> label="ACTIVITY · LIVE"
{bars.map((b, i) => ( value={fmt(total)}
<div key={i} style={{ flex: 1, height: `${Math.max(3, (b / max) * 100)}%`, borderRadius: 2, background: b > 0 ? "linear-gradient(180deg,#5ec8d8,#3a8694)" : "rgba(255,255,255,.06)", transition: "height .3s ease" }} /> unit={`calls/${SLOTS}s`}
))} tint="#5ec8d8"
</div> spark={bars}
<div style={{ fontFamily: mono, fontSize: 9.5, color: "#5a5a62", marginTop: 6 }}>tool calls · last {SLOTS}s</div> />
</AnatomyCard>
); );
} }
// Throughput tile with a rolling sparkline — keyed by agent so it resets on switch. // Full-width row — same throughput data as the old top tile, but wrapped
function ThroughputTile({ value }: { value: number }) { // in an AnatomyCard so the sparkline gets real room to breathe. Reads
// telemetry directly so the parent only has to pass the agent id.
function ThroughputCard({ agentId, value }: { agentId: string; value: number }) {
const buf = useRef<number[]>([]); const buf = useRef<number[]>([]);
const [spark, setSpark] = useState<number[]>([]); const [spark, setSpark] = useState<number[]>([]);
useEffect(() => { useEffect(() => {
buf.current = [...buf.current, value].slice(-24); buf.current = [...buf.current, value].slice(-60);
}, [value]); }, [value]);
useEffect(() => { useEffect(() => {
const id = setInterval(() => setSpark(buf.current.slice()), 1000); const id = setInterval(() => setSpark(buf.current.slice()), 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
return <MetricTile label="THROUGHPUT" value={fmt(value)} unit="tok/min" tint="#5ec8d8" spark={spark} />; const max = Math.max(1, ...spark);
const min = Math.min(0, ...spark);
const range = Math.max(1, max - min);
const w = 100;
const h = 100;
const step = spark.length > 1 ? w / (spark.length - 1) : w;
const points = spark
.map((v, i) => `${(i * step).toFixed(2)},${(h - ((v - min) / range) * h).toFixed(2)}`)
.join(" ");
return (
<AnatomyCard tint="#5ec8d8" label="THROUGHPUT" icon={<Activity size={15} />} key={`throughput-${agentId}`}>
<div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
<div style={{ fontSize: 28, fontWeight: 700, color: "#e6e6ea", letterSpacing: "-.02em" }}>{fmt(value)}</div>
<div style={{ fontFamily: mono, fontSize: 11, color: "#8a8a92" }}>tok/min</div>
</div>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: "100%", height: 84, marginTop: 6 }}>
<polyline points={points} fill="none" stroke="#5ec8d8" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
</svg>
</AnatomyCard>
);
} }
// ── Command center ─────────────────────────────────────────────────────────── // ── Command center ───────────────────────────────────────────────────────────
@@ -182,7 +204,6 @@ export function ClawCommandCenter({
const tele = useAgentTelemetry(agent.id); const tele = useAgentTelemetry(agent.id);
const doors = tele?.doorsPending ?? 0; const doors = tele?.doorsPending ?? 0;
const colStyle: React.CSSProperties = { flex: "1 1 0", minWidth: 0, minHeight: 0, overflowY: "auto", display: "flex", flexDirection: "column", gap: 14, paddingBottom: 8 };
return ( return (
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "#08080a" }}> <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "#08080a" }}>
@@ -201,29 +222,40 @@ export function ClawCommandCenter({
<span style={{ flex: 1 }} /> <span style={{ flex: 1 }} />
</div> </div>
{/* Metric band — per-agent. */} {/* Metric grid — 2-column, wraps to 3 rows for the 5 tiles.
<div style={{ flex: "none", display: "flex", gap: 10, padding: "12px 22px" }}> Activity now lives here (top-of-fold quick-glance); the beefier
<ThroughputTile key={agent.id} value={tele?.tokensPerMin ?? 0} /> Throughput viz moved down into the main stack for room to
<MetricTile label="SPEND" value={(tele?.costPerHr ?? 0).toFixed(2)} unit="cr/hr" tint="#e8b465" /> breathe. Doors/Memory sit in row 1, Loops/Spend row 2, Activity
<MetricTile label="LOOPS" value={`${tele?.loops ?? 0}`} unit="active" tint="#5fd08a" /> spans row 3 by itself. */}
<MetricTile label="MEMORY" value={fmt(brain?.stats.memories ?? 0)} unit="notes" /> <div
style={{
flex: "none",
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: 10,
padding: "12px 22px",
}}
>
<MetricTile label="DOORS" value={`${doors}`} unit={doors > 0 ? "pending" : "clear"} tint={doors > 0 ? "#e8b465" : "#5fd08a"} /> <MetricTile label="DOORS" value={`${doors}`} unit={doors > 0 ? "pending" : "clear"} tint={doors > 0 ? "#e8b465" : "#5fd08a"} />
<MetricTile label="MEMORY" value={fmt(brain?.stats.memories ?? 0)} unit="notes" />
<MetricTile label="LOOPS" value={`${tele?.loops ?? 0}`} unit="active" tint="#5fd08a" />
<MetricTile label="SPEND" value={(tele?.costPerHr ?? 0).toFixed(2)} unit="cr/hr" tint="#e8b465" />
<div style={{ gridColumn: "1 / -1" }}>
<ActivityTile key={`act-tile-${agent.id}`} agentId={agent.id} />
</div>
</div> </div>
{/* Three independently-scrolling columns. */} {/* Main stack — every row full-width so the layout stays calm
<div style={{ flex: "1 1 0", minHeight: 0, display: "flex", gap: 14, padding: "4px 22px 18px" }}> whether the computer panel is open or closed. Working on Now →
{/* LIVE */} Reasoning Stream → Throughput (moved here from the top row) →
<div style={{ ...colStyle, flex: "1.15 1 0" }}> Anatomy Grid (Dot Brain and its neighbours). Scrolls as one
column; the computer panel takes its own scroll to the right. */}
<div style={{ flex: "1 1 0", minHeight: 0, display: "flex", flexDirection: "column", gap: 14, padding: "4px 22px 18px", overflowY: "auto" }}>
<WorkingOnNow key={`won-${agent.id}`} agentId={agent.id} /> <WorkingOnNow key={`won-${agent.id}`} agentId={agent.id} />
<ReasoningStream key={`rs-${agent.id}`} agentId={agent.id} /> <ReasoningStream key={`rs-${agent.id}`} agentId={agent.id} />
<ActivityBars key={`act-${agent.id}`} agentId={agent.id} /> <ThroughputCard key={`tp-${agent.id}`} agentId={agent.id} value={tele?.tokensPerMin ?? 0} />
</div>
{/* ANATOMY — compact icon cards; click one to open its full, pretty, editable content. */}
<div style={{ ...colStyle, flex: "2 1 0" }}>
<AnatomyGrid agent={agent} brain={brain} onSaved={onToolsChanged} onAddTool={() => setAddToolOpen(true)} /> <AnatomyGrid agent={agent} brain={brain} onSaved={onToolsChanged} onAddTool={() => setAddToolOpen(true)} />
</div> </div>
</div>
{avatarOpen ? ( {avatarOpen ? (
<AvatarModal clawId={agent.id} clawName={agent.name} current={shownAvatar} onClose={() => setAvatarOpen(false)} onSaved={(url) => { setImgUrl(url); router.refresh(); }} /> <AvatarModal clawId={agent.id} clawName={agent.name} current={shownAvatar} onClose={() => setAvatarOpen(false)} onSaved={(url) => { setImgUrl(url); router.refresh(); }} />