Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: read-only agent terminal page consuming the swarm term stream

Moves the TermMsg rendering pipeline (Row.tsx, termMsg.ts, linkify.tsx,
markdown.ts) from @hive/agent into @hive/shared, so swarm-ui becomes a
second consumer of it instead of forking a copy -- CSS was already
shared (@hive/shared/terminal.css). marked+dompurify move from
@hive/agent's deps to @hive/shared's; swarm-ui picks them up
transitively, no new direct dep there.

New swarm-ui route /agents/:name/term (AgentTermPage), linked from
AgentsPage's detail panel via a "terminal" badge next to "link matrix
account". Consumes GET /api/agents/{name}/term/stream: unlike
@hive/agent's own useLiveStream (TermEnvelope-wrapped, history/backfill
dance), the swarm relay forwards one bare TermMsg per SSE event with no
envelope and no history endpoint -- useSwarmTermStream is a much
smaller hook for that shape (EventSource -> parse -> coalesce, nothing
to buffer/dedupe/backfill against).

Verified against a live SSE mock (screenshots in /agents/iris/state/screenshots/
3801-agents-detail-panel-terminal-badge.png and
3801-agent-term-page-live-rows.png -- real rows rendering through the
shared Row component, not just a build/typecheck pass).
This commit is contained in:
iris 2026-09-13 18:28:50 +02:00 • committed by mara
commit 593923375c
15 changed files with 232 additions and 22 deletions

View file

@ -0,0 +1,88 @@
// <AgentTermPage> — read-only live terminal for one agent, consuming
// swarm-controller's `GET /api/agents/{name}/term/stream`. First
// per-agent detail route in swarm-ui (linked from `AgentsPage`'s detail
// panel). Live-only, no backfill/history — see `useSwarmTermStream`'s
// doc for why a fresh visit starts empty rather than replaying anything.
//
// Auto-scroll is the simple "stick to bottom unless the operator
// scrolled up" rule `@hive/agent`'s `LiveStream` uses, minus its
// load-more/history plumbing (nothing to load older here).
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { Link, useParams } from "wouter-preact";
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { Row } from "@hive/shared/term-row.js";
import { Panel } from "../../ui/panel/Panel.js";
import { useSwarmTermStream, type ConnectionState } from "./useSwarmTermStream.js";
import "./AgentTermPage.css";
const NEAR_BOTTOM_PX = 48;
const CONNECTION_LABEL: Record<ConnectionState, string> = {
connecting: "connecting…",
open: "live",
reconnecting: "reconnecting…",
closed: "disconnected",
};
const CONNECTION_TONE: Record<ConnectionState, BadgeTone> = {
connecting: "neutral",
open: "positive",
reconnecting: "warning",
closed: "negative",
};
export function AgentTermPage() {
const { name } = useParams<{ name: string }>();
const { rows, connection } = useSwarmTermStream(
`/api/agents/${encodeURIComponent(name ?? "")}/term/stream`,
);
const logRef = useRef<HTMLDivElement>(null);
const [stickToBottom, setStickToBottom] = useState(true);
function handleScroll() {
const el = logRef.current;
if (!el) return;
setStickToBottom(
el.scrollHeight - el.scrollTop - el.clientHeight <= NEAR_BOTTOM_PX,
);
}
// Runs after Preact has committed new rows, so `scrollHeight` is
// already current — same ordering `LiveStream`'s own layout effect
// relies on.
useLayoutEffect(() => {
const el = logRef.current;
if (el && stickToBottom) el.scrollTop = el.scrollHeight - el.clientHeight;
}, [rows, stickToBottom]);
return (
<Panel
title={`terminal — ${name}`}
icon="🖥️"
actions={
<Link to="/agents" class="ui-agent-term-back">
← agents
</Link>
}
>
<Badge
class="ui-agent-term-status"
tone={CONNECTION_TONE[connection]}
value={CONNECTION_LABEL[connection]}
/>
<div class="terminal-wrap">
<div class="live terminal" ref={logRef} onScroll={handleScroll}>
{rows.length === 0 ? (
<div class="row note">
{connection === "open"
? "(connected — waiting for events)"
: CONNECTION_LABEL[connection]}
</div>
) : (
rows.map((row) => <Row key={row.key} row={row} />)
)}
</div>
</div>
</Panel>
);
}