swarm-ui: split pages/agents into two pages and the agent components
pages/agents held the roster page, the terminal page and every component under them. It keeps the two pages; the components move to src/agent, by concern: roster (card, wanted menu, actions), detail (panel body, dialog, inference, linked accounts, link menu) and terminal (preview, input, subagents, stream hooks, favicon), with the shared row type and icon at the top. Moves and import rewrites only.
This commit is contained in:
parent
fd6953ed7c
commit
74e588415b
27 changed files with 28 additions and 25 deletions
|
|
@ -1,107 +0,0 @@
|
|||
// <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 it is refetched every `REFRESH_MS`. 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 { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { AgentTermPreview } from "./AgentTermPreview.js";
|
||||
import { useRefreshInterval } from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import "./SubagentTerms.css";
|
||||
|
||||
const REFRESH_MS = 10_000;
|
||||
const STALE_S = 15 * 60;
|
||||
|
||||
interface Subagent {
|
||||
name: string;
|
||||
last_row_unix: number | null;
|
||||
}
|
||||
|
||||
// Newest row first; a subagent whose time could not be read sorts last.
|
||||
function byRecency(a: Subagent, b: Subagent): number {
|
||||
const ta = a.last_row_unix ?? -Infinity;
|
||||
const tb = b.last_row_unix ?? -Infinity;
|
||||
if (ta !== tb) return tb > ta ? 1 : -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export function SubagentTerms({ agentName }: { agentName: string }) {
|
||||
const [subagents, setSubagents] = useState<Subagent[] | null>(null);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
const res = await fetch(
|
||||
`/api/agents/${encodeURIComponent(agentName)}/subagents`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
setError(await readApiError(res));
|
||||
return;
|
||||
}
|
||||
setSubagents(((await res.json()) as Subagent[]).sort(byRecency));
|
||||
setError(null);
|
||||
}
|
||||
|
||||
useRefreshInterval(REFRESH_MS, () => {
|
||||
refresh().catch((e: unknown) => setError({ detail: String(e) }));
|
||||
});
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue