Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
iris 2026-10-28 20:54:16 +01:00 • committed by mara
commit da5fcbfd9b
4 changed files with 219 additions and 25 deletions

View file

@ -12,7 +12,7 @@ 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, stillListed } from "./subagent-list.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";
@ -41,7 +41,9 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
<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 }) => {
{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 (
@ -60,6 +62,7 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
}
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} />

View file

@ -45,3 +45,9 @@
.ui-subagents-menu-back {
color: var(--blue);
}
/* What it runs sits after the name, not centred between name and age. */
.ui-subagents-menu-run {
margin-right: auto;
margin-left: 0.6em;
}

View file

@ -17,7 +17,12 @@ import { Panel } from "../../ui/panel/Panel.js";
import { Popover } from "../../ui/popover/Popover.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import { termPaths } from "./AgentTermPreview.js";
import { runningLabel, splitRunning, type Subagent } from "./subagent-list.js";
import {
runLabel,
runningLabel,
splitRunning,
type Subagent,
} from "./subagent-list.js";
import { useSubagentList } from "./useSubagentList.js";
import "./SubagentsMenu.css";
@ -41,6 +46,11 @@ function Entry({
aria-current={current ? "page" : undefined}
>
<span class="ui-subagents-menu-name">{sub.name}</span>
{runLabel(sub) && (
<span class="ui-subagents-menu-age ui-subagents-menu-run">
{runLabel(sub)}
</span>
)}
{sub.last_row_unix !== null && (
<span class="ui-subagents-menu-age">
<RelativeTime epochMs={sub.last_row_unix * 1000} />

View file

@ -8,6 +8,19 @@ export interface Subagent {
name: string;
/** Unix seconds of its newest terminal row, `null` when not readable. */
last_row_unix: number | null;
/** The model it runs; `null` on the harness's default or when not readable. */
model: string | null;
/** Its reasoning effort; `null` when not readable. */
effort: string | null;
}
/**
* What a subagent runs, as one short label: `opus · high`. Whichever of the
* two the controller could not read is left out, and an empty string means
* there is nothing to show.
*/
export function runLabel(sub: Pick<Subagent, "model" | "effort">): string {
return [sub.model, sub.effort].filter((v) => v).join(" · ");
}
/** A subagent quiet for longer than this is no longer counted as running. */