Header-bar parity for the swarm-level agent terminal, per mara's ruling
"dont make it a header though - make it floating elements on top of the
terminal". Adds useSwarmAgentStateStream (SSE consumer for
swarm-controller's GET /api/agents/{name}/state/stream) and renders a
small read-only Badge cluster absolutely positioned in
AgentTermPreview's terminal box corner, reusing the same anchor pattern
@hive/shared/terminal.css's .tail-pill already uses.
Shows turn_state always, agent_state only when it isn't the boring "up"
case, model (with resolved-model tooltip), and ctx/cost usage. No badge
renders until the first header event lands - the swarm queue's
agent-state subject is transition-only with no seed/replay, and no
swarm-level endpoint today can seed turn_state/model/ctx/cost
synchronously.
Vocabulary (state tones, token-total/format helpers) ported from
@hive/agent's Root.tsx/lib/format.ts rather than imported, matching the
existing "small helper, not worth a cross-package hop" call
useSwarmTermStream's own appendRow already made.
Verified by rendering the real component tree against a stubbed
EventSource and screenshotting the result - the badge cluster initially
overlapped the terminal's first row of text, fixed by giving this
preview's own .live.terminal extra top padding.
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
// 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<AgentStateHeader | null>(null);
|
|
const [connection, setConnection] = useState<ConnectionState>("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 };
|
|
}
|