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.
74 lines
3 KiB
TypeScript
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>
|
|
);
|
|
}
|