// Live SSE consumer for the swarm-relayed agent turn-state header // (`GET /api/agents/{name}/state/stream`, swarm-controller's // `agent_state_stream.rs`). Same transport shape as `useSwarmTermStream` // — one core-NATS subject relayed as bare SSE `data`, no envelope, no // replay — but the payload here is a full header that *replaces* the // last one rather than an appendable row, so there is nothing to // coalesce: each event is just the latest value. // // **No seed on attach.** `agent_state_stream.rs`'s own module doc names // this limitation: the subject is published on transition only, so an // agent that has sat idle since before this hook mounted announces // nothing until its next change. `header` stays `null` until the first // event lands — callers should render no badges in that gap rather than // a stale or guessed one (there is no swarm-level endpoint today that // can seed turn_state/model/ctx/cost synchronously — `GET // /api/agents/status`'s snapshot carries only `status_text`/`running`). import { useEffect, useState } from "preact/hooks"; export type ConnectionState = "connecting" | "open" | "reconnecting" | "closed"; export interface TokenUsage { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number; } // Mirrors `hive_agent::swarm_agent_state::AgentStateMsg` field-for-field // — see that struct's own doc comment for why `turn_state_since` (an // ISO 8601 string here, unlike the per-agent page's unix-seconds // `/api/state`) and `agent_state` (the swarm `AgentState` enum, not a // bare `paused: bool`) diverge from the per-agent page's own shape. export interface AgentStateHeader { turn_state: "idle" | "thinking" | "compacting"; turn_state_since: string; agent_state: "up" | "offline" | "paused" | "destroyed"; model: string; resolved_model: string | null; context_window_tokens: number; ctx_usage: TokenUsage | null; cost_usage: TokenUsage | null; } export interface UseSwarmAgentStateStreamResult { header: AgentStateHeader | null; connection: ConnectionState; } export function useSwarmAgentStateStream( streamUrl: string, ): UseSwarmAgentStateStreamResult { const [header, setHeader] = useState(null); const [connection, setConnection] = useState("connecting"); useEffect(() => { setHeader(null); setConnection("connecting"); const es = new EventSource(streamUrl); es.onopen = () => setConnection("open"); es.onmessage = (e) => { try { setHeader(JSON.parse(e.data) as AgentStateHeader); } catch { // Malformed payload: drop it silently. Unlike the terminal // preview there is no row list to append a "[parse err]" line // to here, and one bad header isn't worth degrading the whole // badge cluster over — the next valid header replaces it. } }; es.onerror = () => { setConnection( es.readyState === 0 /* CONNECTING */ ? "reconnecting" : "closed", ); }; return () => es.close(); }, [streamUrl]); return { header, connection }; }