swarm-ui: floating turn-state/model/ctx/cost badges on the agent term preview
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.
This commit is contained in:
parent
273b2895a1
commit
d9c8b7ca8f
3 changed files with 211 additions and 6 deletions
|
|
@ -0,0 +1,79 @@
|
|||
// 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 };
|
||||
}
|
||||
Loading…
Reference in a new issue