World: Phase D — Observe live mission-control panel
frontend/src/components/observe/ObservePanel.tsx — a live mission-control view driven entirely by the taxonomy feed (telemetry strip, doors awaiting approval, agent statuses, and a rolling activity stream of real reasoning + tool calls; the feed is ref-buffered + flushed on an interval so reasoning bursts don't re-render per token). Rendered as the World slide-out's system view (replaces the empty-state placeholder). With the run_events normalization, this shows REAL agent reasoning, tool calls, and doors. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
380b95da2a
commit
b1d18504c3
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
// Observe — live mission-control, driven entirely by the Clawmates taxonomy feed
|
||||
// (so it's independent of the world graph's seed data). Shows the telemetry strip,
|
||||
// doors awaiting approval, agent statuses, and a rolling activity stream of real
|
||||
// reasoning + tool calls. Rendered in the World slide-out as its system view.
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useLiveEvent, useLiveState } from "@/lib/live/useClawmatesLive";
|
||||
|
||||
const mono = "'Geist Mono', ui-monospace, monospace";
|
||||
|
||||
interface FeedItem {
|
||||
id: number;
|
||||
kind: "tool" | "think" | "door";
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
interface Door {
|
||||
doorId: string;
|
||||
agentId: string;
|
||||
action: string;
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
const short = (id: string) => (id.length > 8 ? id.slice(0, 6) : id);
|
||||
|
||||
function Section({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
|
||||
<span style={{ fontFamily: mono, fontSize: 9, letterSpacing: ".12em", color: "#5a5a62" }}>{label}</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObservePanel() {
|
||||
const telemetry = useLiveState<"telemetry", { doorsPending?: number; loops?: number; costPerHr?: number; tokensPerMin?: number }>(
|
||||
"telemetry",
|
||||
(d) => d,
|
||||
{},
|
||||
);
|
||||
|
||||
const [doors, setDoors] = useState<Door[]>([]);
|
||||
useLiveEvent("door.request", (d) =>
|
||||
setDoors((cur) => [{ doorId: d.doorId, agentId: d.agentId, action: d.action, summary: d.summary }, ...cur.filter((x) => x.doorId !== d.doorId)].slice(0, 6)),
|
||||
);
|
||||
useLiveEvent("door.resolve", (d) => setDoors((cur) => cur.filter((x) => x.doorId !== d.doorId)));
|
||||
|
||||
const [agents, setAgents] = useState<Record<string, { status: string; role?: string }>>({});
|
||||
useLiveEvent("agent.status", (d) => setAgents((cur) => ({ ...cur, [d.agentId]: { status: d.status, role: d.role } })));
|
||||
|
||||
// Rolling activity feed — buffered in a ref and flushed on an interval so a
|
||||
// burst of reasoning deltas doesn't trigger a re-render per token.
|
||||
const buf = useRef<FeedItem[]>([]);
|
||||
const seq = useRef(0);
|
||||
const [feed, setFeed] = useState<FeedItem[]>([]);
|
||||
const push = (kind: FeedItem["kind"], text: string, color: string) => {
|
||||
buf.current = [{ id: seq.current++, kind, text, color }, ...buf.current].slice(0, 60);
|
||||
};
|
||||
useLiveEvent("agent.tool.call", (d) =>
|
||||
push("tool", `${short(d.agentId)} → ${d.tool}${d.target ? ` ${d.target}` : ""}`, "#5ec8d8"),
|
||||
);
|
||||
useLiveEvent("agent.reasoning.delta", (d) => {
|
||||
const t = d.text.trim();
|
||||
if (t) push("think", `${short(d.agentId)} · ${t}`, "#9a9aa2");
|
||||
});
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setFeed(buf.current.slice()), 300);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const statusColor = (s: string) => (s === "working" ? "#5ec8d8" : s === "online" ? "#5fd08a" : "#5a5a62");
|
||||
const pill = (label: string, value: string | number) => (
|
||||
<div style={{ flex: 1, borderRadius: 10, background: "#101014", border: "1px solid rgba(255,255,255,.06)", padding: "9px 6px", textAlign: "center" }}>
|
||||
<div style={{ fontSize: 17, fontWeight: 700, color: "#f3f3f5" }}>{value}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 8.5, letterSpacing: ".06em", color: "#6a6a72", marginTop: 2 }}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const agentList = Object.entries(agents);
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span className="cm-blink" style={{ width: 7, height: 7, borderRadius: "50%", background: "#5ec8d8" }} />
|
||||
<span style={{ fontFamily: mono, fontSize: 12, letterSpacing: ".06em", color: "#cfcfd5", fontWeight: 600 }}>LIVE OBSERVE</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{pill("DOORS", telemetry.doorsPending ?? 0)}
|
||||
{pill("LOOPS", telemetry.loops ?? 0)}
|
||||
{pill("AGENTS", agentList.length)}
|
||||
</div>
|
||||
|
||||
{doors.length > 0 ? (
|
||||
<Section label="DOORS AWAITING APPROVAL">
|
||||
{doors.map((d) => (
|
||||
<div key={d.doorId} style={{ borderRadius: 10, background: "rgba(232,180,101,.08)", border: "1px solid rgba(232,180,101,.3)", padding: "9px 11px" }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, color: "#f0d9a8" }}>{d.action}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, color: "#9a9aa2", marginTop: 2 }}>{short(d.agentId)}{d.summary ? ` · ${d.summary}` : ""}</div>
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{agentList.length > 0 ? (
|
||||
<Section label="AGENTS">
|
||||
{agentList.map(([id, a]) => (
|
||||
<div key={id} style={{ display: "flex", alignItems: "center", gap: 9, padding: "8px 11px", borderRadius: 10, background: "#101014", border: "1px solid rgba(255,255,255,.06)" }}>
|
||||
<span style={{ width: 8, height: 8, flex: "none", borderRadius: "50%", background: statusColor(a.status) }} />
|
||||
<span style={{ flex: 1, minWidth: 0, fontSize: 12.5, color: "#eaeaee", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.role || short(id)}</span>
|
||||
<span style={{ fontFamily: mono, fontSize: 9.5, color: statusColor(a.status) }}>{a.status}</span>
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
<Section label="LIVE ACTIVITY">
|
||||
{feed.length === 0 ? (
|
||||
<span style={{ fontFamily: mono, fontSize: 11, color: "#3a3a40" }}>waiting for agent activity…</span>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
{feed.map((f) => (
|
||||
<div key={f.id} style={{ display: "flex", gap: 8, fontFamily: mono, fontSize: 10.5, lineHeight: 1.5 }}>
|
||||
<span style={{ flex: "none", color: f.color }}>{f.kind === "tool" ? "⚙" : "›"}</span>
|
||||
<span style={{ flex: 1, minWidth: 0, color: "#cfcfd5", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{f.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ObservePanel;
|
||||
Reference in New Issue
Block a user