Compare commits
4 changed files with 5 additions and 41 deletions
|
|
@ -39,9 +39,7 @@ terminal, and below it the agent's **subagents**: every subagent the swarm
|
||||||
queue holds terminal rows for from the last 24 hours. Picking one shows its
|
queue holds terminal rows for from the last 24 hours. Picking one shows its
|
||||||
terminal in the same preview, and **expand** opens it in its own tab, as it
|
terminal in the same preview, and **expand** opens it in its own tab, as it
|
||||||
does for the agent. The list refreshes every 10 seconds, so a subagent the
|
does for the agent. The list refreshes every 10 seconds, so a subagent the
|
||||||
agent spawns shows up once it writes output. A subagent that drops out of the
|
agent spawns shows up once it writes output.
|
||||||
list closes its terminal, and a later subagent with the same name starts
|
|
||||||
unpicked.
|
|
||||||
|
|
||||||
On a terminal's own page (`/agents/<name>/terminal`, or a subagent's) the
|
On a terminal's own page (`/agents/<name>/terminal`, or a subagent's) the
|
||||||
header has an **N running subagents** badge. It opens a list of the agent's
|
header has an **N running subagents** badge. It opens a list of the agent's
|
||||||
|
|
|
||||||
|
|
@ -8,27 +8,18 @@
|
||||||
// agent's own: subagents take no input.
|
// agent's own: subagents take no input.
|
||||||
//
|
//
|
||||||
// Mount it keyed by agent name: the refresh timer is armed once per mount.
|
// Mount it keyed by agent name: the refresh timer is armed once per mount.
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import { AgentTermPreview } from "./AgentTermPreview.js";
|
import { AgentTermPreview } from "./AgentTermPreview.js";
|
||||||
import { STALE_S, stillListed } from "./subagent-list.js";
|
import { STALE_S } from "./subagent-list.js";
|
||||||
import { useSubagentList } from "./useSubagentList.js";
|
import { useSubagentList } from "./useSubagentList.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import "./SubagentTerms.css";
|
import "./SubagentTerms.css";
|
||||||
|
|
||||||
export function SubagentTerms({ agentName }: { agentName: string }) {
|
export function SubagentTerms({ agentName }: { agentName: string }) {
|
||||||
const { subagents, error } = useSubagentList(agentName);
|
const { subagents, error } = useSubagentList(agentName);
|
||||||
const [picked, setPicked] = useState<string | null>(null);
|
const [selected, setSelected] = 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
|
// 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
|
// "none" line — and so does the first fetch while it is in flight. A
|
||||||
|
|
@ -58,7 +49,7 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
|
||||||
? `hide ${name}'s terminal`
|
? `hide ${name}'s terminal`
|
||||||
: `show ${name}'s terminal`
|
: `show ${name}'s terminal`
|
||||||
}
|
}
|
||||||
onClick={() => setPicked(name === selected ? null : name)}
|
onClick={() => setSelected(name === selected ? null : name)}
|
||||||
/>
|
/>
|
||||||
{last_row_unix !== null && (
|
{last_row_unix !== null && (
|
||||||
<span class="ui-subagent-terms-age">
|
<span class="ui-subagent-terms-age">
|
||||||
|
|
|
||||||
|
|
@ -48,18 +48,3 @@ export function splitRunning(
|
||||||
export function runningLabel(count: number): string {
|
export function runningLabel(count: number): string {
|
||||||
return count === 1 ? "1 running subagent" : `${count} running subagents`;
|
return count === 1 ? "1 running subagent" : `${count} running subagents`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The picked subagent while the list still holds it, else `null`. The list
|
|
||||||
* refreshes on a timer and can drop a subagent whose stream is gone, so a
|
|
||||||
* pick outlives its badge unless it is checked against the list. `null`
|
|
||||||
* list (not fetched yet) holds no pick either.
|
|
||||||
*/
|
|
||||||
export function stillListed(
|
|
||||||
picked: string | null,
|
|
||||||
subs: Subagent[] | null,
|
|
||||||
): string | null {
|
|
||||||
return picked !== null && subs?.some((s) => s.name === picked)
|
|
||||||
? picked
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
isRunning,
|
isRunning,
|
||||||
runningLabel,
|
runningLabel,
|
||||||
splitRunning,
|
splitRunning,
|
||||||
stillListed,
|
|
||||||
} from "../src/agent/terminal/subagent-list.ts";
|
} from "../src/agent/terminal/subagent-list.ts";
|
||||||
|
|
||||||
const NOW = 1_000_000;
|
const NOW = 1_000_000;
|
||||||
|
|
@ -60,12 +59,3 @@ test("the badge counts in words", () => {
|
||||||
assert.equal(runningLabel(1), "1 running subagent");
|
assert.equal(runningLabel(1), "1 running subagent");
|
||||||
assert.equal(runningLabel(4), "4 running subagents");
|
assert.equal(runningLabel(4), "4 running subagents");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a pick holds only while the refreshed list still has it", () => {
|
|
||||||
const subs = [sub("a", 10), sub("b", 20)];
|
|
||||||
assert.equal(stillListed("a", subs), "a");
|
|
||||||
assert.equal(stillListed("gone", subs), null);
|
|
||||||
assert.equal(stillListed("a", []), null);
|
|
||||||
assert.equal(stillListed("a", null), null);
|
|
||||||
assert.equal(stillListed(null, subs), null);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue