Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/agent/terminal/SubagentTerms.tsx
iris da5fcbfd9b subagents publish their model and effort as headers on every row, and the swarm shows them
The subagent daemon stamps Swarm-Model and Swarm-Effort on each row it
publishes. The list endpoint reads them off the newest row it already fetches,
so a long-running subagent keeps them past the stream's 24 hour window, and the
terminal page's menu and the agent panel's list show them beside the name.
2026-10-28 21:55:02 +01:00

86 lines
3.6 KiB
TypeScript

// <SubagentTerms> — an agent's subagents, under its own terminal preview in
// `AgentsPage`'s detail panel. The list is `GET /api/agents/{name}/subagents`:
// the subagents the swarm queue holds terminal rows for, each with the
// publish time of its newest row. Newest first, with a ticking age beside
// the name; one quiet for `STALE_S` is dimmed. Subagents are spawned on
// demand, so `useSubagentList` refetches it. Picking one shows its
// terminal in `AgentTermPreview`, expand-to-tab included. Read-only like the
// agent's own: subagents take no input.
//
// Mount it keyed by agent name: the refresh timer is armed once per mount.
import { useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge } from "@hive/shared/badge.js";
import { AgentTermPreview } from "./AgentTermPreview.js";
import { STALE_S, runLabel, stillListed } from "./subagent-list.js";
import { useSubagentList } from "./useSubagentList.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import "./SubagentTerms.css";
export function SubagentTerms({ agentName }: { agentName: string }) {
const { subagents, error } = useSubagentList(agentName);
const [picked, setPicked] = useState<string | null>(null);
// The refresh can drop the picked subagent (its stream is gone). Only a
// name still in the list counts as selected, so the terminal closes with
// its badge and a later subagent of the same name does not reopen it.
const selected = stillListed(picked, subagents);
useEffect(() => {
if (picked !== null && subagents !== null && selected === null) {
setPicked(null);
}
}, [picked, subagents, selected]);
// An agent with no subagents shows nothing here — no heading, no
// "none" line — and so does the first fetch while it is in flight. A
// failed fetch still shows, so the error is not swallowed.
if (!error && (subagents === null || subagents.length === 0)) return null;
const nowS = Date.now() / 1000;
return (
<section class="ui-subagent-terms" aria-label={`${agentName}'s subagents`}>
<div class="ui-subagent-terms-list">
<span class="ui-subagent-terms-heading">subagents</span>
{subagents?.map((sub) => {
const { name, last_row_unix } = sub;
const runs = runLabel(sub);
const stale =
last_row_unix !== null && nowS - last_row_unix > STALE_S;
return (
<span
key={name}
class={`ui-subagent-terms-item${stale ? " ui-subagent-terms-item-stale" : ""}`}
>
<Badge
variant={name === selected ? "default" : "quiet"}
value={name}
expanded={name === selected}
title={
name === selected
? `hide ${name}'s terminal`
: `show ${name}'s terminal`
}
onClick={() => setPicked(name === selected ? null : name)}
/>
{runs && <span class="ui-subagent-terms-age">{runs}</span>}
{last_row_unix !== null && (
<span class="ui-subagent-terms-age">
<RelativeTime epochMs={last_row_unix * 1000} />
</span>
)}
</span>
);
})}
</div>
{error && (
<ApiErrorPanel
problem={error}
context={`failed to list ${agentName}'s subagents`}
/>
)}
{selected && (
<AgentTermPreview agentName={agentName} subagent={selected} />
)}
</section>
);
}