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
@@ -122,6 +122,76 @@ async fn doors_pending(pool: &PgPool, ws: WorkspaceId) -> i64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Live per-agent metrics for the agent command center's metric band.
|
||||
#[derive(Default)]
|
||||
struct AgentTele {
|
||||
tokens_per_min: i64,
|
||||
cost_per_hr: f64,
|
||||
loops: i64,
|
||||
doors: i64,
|
||||
}
|
||||
|
||||
/// Per-agent telemetry for every agent in the workspace, in 4 grouped queries
|
||||
/// (not N×4): tokens over the last minute, credits over the last hour, active
|
||||
/// routines, and pending approvals — all keyed by agent id.
|
||||
async fn agent_telemetry(pool: &PgPool, ws: WorkspaceId) -> std::collections::HashMap<String, AgentTele> {
|
||||
let mut m: std::collections::HashMap<String, AgentTele> = std::collections::HashMap::new();
|
||||
let id_of = |r: &sqlx::postgres::PgRow| r.get::<uuid::Uuid, _>("agent_id").to_string();
|
||||
|
||||
// tokens in the last minute ≈ tokens/min.
|
||||
for r in sqlx::query(
|
||||
"SELECT agent_id, SUM(tokens_in + tokens_out)::bigint AS v FROM usage_events
|
||||
WHERE workspace_id = $1 AND agent_id IS NOT NULL AND created_at > now() - interval '1 minute'
|
||||
GROUP BY agent_id",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
m.entry(id_of(&r)).or_default().tokens_per_min = r.get("v");
|
||||
}
|
||||
// credits spent in the last hour ≈ spend/hr.
|
||||
for r in sqlx::query(
|
||||
"SELECT agent_id, SUM(credits)::float8 AS v FROM usage_events
|
||||
WHERE workspace_id = $1 AND agent_id IS NOT NULL AND created_at > now() - interval '1 hour'
|
||||
GROUP BY agent_id",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
m.entry(id_of(&r)).or_default().cost_per_hr = r.get("v");
|
||||
}
|
||||
// active routines = loops.
|
||||
for r in sqlx::query(
|
||||
"SELECT r.agent_id AS agent_id, count(*)::bigint AS v FROM routines r
|
||||
JOIN agents a ON a.id = r.agent_id
|
||||
WHERE a.workspace_id = $1 AND r.status = 'active' GROUP BY r.agent_id",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
m.entry(id_of(&r)).or_default().loops = r.get("v");
|
||||
}
|
||||
// pending approvals = doors.
|
||||
for r in sqlx::query(
|
||||
"SELECT requested_by_agent AS agent_id, count(*)::bigint AS v FROM approvals
|
||||
WHERE workspace_id = $1 AND status = 'pending' GROUP BY requested_by_agent",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
m.entry(id_of(&r)).or_default().doors = r.get("v");
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// `GET /api/world/live` — the taxonomy SSE feed.
|
||||
pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) -> impl IntoResponse {
|
||||
let pool = state.pool.clone();
|
||||
@@ -212,6 +282,22 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
|
||||
}
|
||||
}
|
||||
|
||||
// Per-agent telemetry → the command-center metric band (real data:
|
||||
// usage_events tokens/credits, active routines, pending approvals).
|
||||
let tele = agent_telemetry(&pool, ws).await;
|
||||
for a in &roster {
|
||||
let id = a.id.to_string();
|
||||
let t = tele.get(&id);
|
||||
yield sse("telemetry", json!({
|
||||
"agentId": id,
|
||||
"tokensPerMin": t.map(|x| x.tokens_per_min).unwrap_or(0),
|
||||
"costPerHr": t.map(|x| x.cost_per_hr).unwrap_or(0.0),
|
||||
"loops": t.map(|x| x.loops).unwrap_or(0),
|
||||
"doorsPending": t.map(|x| x.doors).unwrap_or(0),
|
||||
}));
|
||||
}
|
||||
|
||||
// Workspace-wide telemetry (top-bar pills / Observe system strip).
|
||||
yield sse(
|
||||
"telemetry",
|
||||
json!({ "doorsPending": doors_pending(&pool, ws).await, "loops": runs.len() }),
|
||||
|
||||
Reference in New Issue
Block a user