Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/agent/terminal/SubagentTerms.tsx
iris 664409c87f swarm-ui: a running-subagents menu in the full-screen terminal header
An N running subagents badge opens a list of the agent's subagents, each a
link to its terminal, so you can switch without going back to the agents page.
A subagent counts as running when it published a row in the last 15 minutes;
the quiet ones are listed below it, dimmed. The list under the preview and the
menu now share one fetch hook and one definition of running.
2026-10-18 01:46:49 +02:00

74 lines
3 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 { 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 } 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 [selected, setSelected] = useState<string | null>(null);
// 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(({ name, last_row_unix }) => {
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={() => setSelected(name === selected ? null : name)}
/>
{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>
);
}