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:
parent
5dff508e79
commit
593923375c
15 changed files with 232 additions and 22 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue