// — the MVP mara's own ruling asked for: not a separate // page, a small preview in an opened agent card on `AgentsPage`, with // an `AgentInputForm` under an agent's terminal (a subagent's has none: it // takes no input of its own). Consumes swarm-controller's // `GET /api/agents/{name}/term/stream`, or for a subagent // `GET /api/agents/{name}/subagents/{subagent}/term/stream`, via // `useSwarmTermStream`, rendering through the same `@hive/shared` `Row` // component the per-hive local terminal uses — "share components between hive and swarm level // term as much as possible" (mara). // // **Floating header badges** — turn state / model / ctx / cost parity // with the per-agent page's own `StatusChips` — mara: "dont make it a // header though - make it floating elements on top of the terminal", so // this is a small `Badge` cluster pinned to the terminal box's own // corner (`.terminal-wrap`'s `position: relative`, same anchor // `@hive/shared/terminal.css`'s `.tail-pill` already uses), not a // separate layout row. Read-only, no click targets: pause/resume already // has its own control surface (`WantedMenu` on `AgentsPage`), so nothing // here needs to be interactive. Sourced from `useSwarmAgentStateStream`, // not from `AgentRow` — the roster fetch `AgentsPage` already did has no // notion of turn_state/model/ctx/cost, only `wanted`/`snapshot.running`. // Only the full-tab mount (`fullHeight`) shows the state badges: the small // preview in an opened card stays free of them (mara: "the preview is small // and should not be encroached further with badges on top, but for the full // screen term i want it like that"), and the card's overview lists the // same state in a row of its own (`AgentStateRow`). // **Expand-to-fullscreen trigger** — opens/navigates to // `/agents/:name/terminal` (`AgentTerminalPage`) via `useDynamicTabs`, // same header-tab mechanism `Shell`'s own nav row now shares. Lives here // (not on `AgentsPage`) so both the embedded preview and the future full // page mount the identical trigger without either needing to know where // the other one is. Suppressed via `fullHeight` — the full-page mount // already IS the destination, so it has nothing further to expand to. import { useLayoutEffect, useRef, useState } from "preact/hooks"; import type { ComponentChildren } from "preact"; import { Badge, type BadgeTone } from "@hive/shared/badge.js"; import { ExpandIcon } from "@hive/shared/icons.js"; import { Row } from "@hive/shared/term-row.js"; import { useDynamicTabs } from "../../shell/useDynamicTabs.js"; import { useSwarmTermStream, type ConnectionState, } from "./useSwarmTermStream.js"; import { useSwarmAgentStateStream } from "./useSwarmAgentStateStream.js"; import { termPaths } from "./term-paths.js"; import { StateBadges, tokenTotal } from "../state/StateBadges.js"; import { AgentInputForm } from "./AgentInputForm.js"; import { CancelTurnBadge } from "./CancelTurnBadge.js"; import "./AgentTermPreview.css"; const NEAR_BOTTOM_PX = 48; const CONNECTION_LABEL: Record = { connecting: "connecting…", open: "live", reconnecting: "reconnecting…", closed: "disconnected", }; const CONNECTION_TONE: Record = { connecting: "neutral", open: "positive", reconnecting: "warning", closed: "negative", }; export function AgentTermPreview({ agentName, subagent, fullHeight = false, leading, trailing, }: { agentName: string; // Set: this is one of `agentName`'s subagents, output only like the // agent's own. subagent?: string; // The full-tab mount: fills the tab, has no expand trigger, and shows the // agent's state badges beside the connection one. fullHeight?: boolean; // A badge ahead of the connection badge, for the full-tab mount's // link back to the agent's details. leading?: ComponentChildren; // Icon-only badges in the top right corner, for the full-tab mount's own // controls (a group pane's maximize and close). trailing?: ComponentChildren; }) { const paths = termPaths(agentName, subagent); const { rows, connection, reconnect } = useSwarmTermStream(paths.stream); // The small preview has no state badges, so it does not follow the state // stream at all. const { header, reconnect: reconnectState } = useSwarmAgentStateStream( fullHeight ? paths.state : null, ); const { open: openTab } = useDynamicTabs(); const logRef = useRef(null); const [stickToBottom, setStickToBottom] = useState(true); const ctx = header ? tokenTotal(header.ctx_usage) : null; const cost = header ? tokenTotal(header.cost_usage) : null; 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 `@hive/agent`'s `LiveStream` uses. useLayoutEffect(() => { const el = logRef.current; if (el && stickToBottom) el.scrollTop = el.scrollHeight - el.clientHeight; }, [rows, stickToBottom]); return (
{/* One floating row over the terminal's top edge: the state badges (connection first) on the left, the expand trigger on the right. The full-tab mount is already the destination, so it has no expand trigger. */}
{leading} {connection === "closed" && ( { reconnect(); reconnectState(); }} /> )} {header && ( )} {header && subagent === undefined && ( )}
{!fullHeight && ( } value="expand" title={`open ${paths.label}'s terminal in its own tab`} onClick={() => openTab({ href: paths.page, label: paths.label })} /> )} {trailing !== undefined && (
{trailing}
)}
{rows.length === 0 ? (
{/* Explicit `.row-content` wrapper, not a bare text child — `.live .row`'s CSS grid (terminal.css) has a narrow first column reserved for a row icon; a row's lone child with no explicit `grid-column` auto-places into that column instead of the content one, wrapping the text one or two characters per line — mara reported exactly this on the live "connecting…" placeholder. `Row.tsx`'s own icon-less flat row already documents and avoids this same trap. */} {connection === "open" ? "(connected — waiting for events)" : CONNECTION_LABEL[connection]}
) : ( rows.map((row) => ) )}
{subagent === undefined && }
); }