Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: move the terminal preview's state badges into a state row of the agent details table

The small preview in the detail panel keeps only the connection badge and
expand, and no longer follows the agent's state stream; the full terminal
tab keeps all its badges. The table gets a state row with the turn state, a
paused badge, and the context and billed tokens (not the model, which the
inference row shows). The preview's top padding drops to one badge row, and
the showHeaderBadges prop goes since the full-tab mount is now the only one
with badges.
This commit is contained in:
iris 2026-10-25 14:52:57 +01:00
commit 705160c0fd
7 changed files with 94 additions and 33 deletions

View file

@ -48,6 +48,14 @@ agent spawns shows up once it writes output. A subagent that drops out of the
list closes its terminal, and a later subagent with the same name starts
unpicked.
The preview carries only the connection badge and **expand**, so it stays clear
of the text. What the agent is doing shows in the details table above it, in
the **state** row: the turn state (idle, thinking or compacting), a **paused**
badge for a paused agent, and the tokens in its context and billed. The row
reads "waiting for the agent's report…" until the first report arrives, since
the agent sends one when its state changes and once a minute. The terminal's
own tab keeps these badges, with the model, beside the connection badge.
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
subagents, each a link to its terminal; on a subagent's page the current one
@ -77,7 +85,7 @@ with no icon keeps the hyperhive mark.
The tab also has an agent badge, with the agent's icon and name, ahead of the
connection badge. It opens the agent's details in a popover under the badge:
the same status, wanted state, inference settings, resource caps, permissions and linked accounts as the
the same status, state, wanted state, inference settings, resource caps, permissions and linked accounts as the
panel on `/agents`, without the terminal preview. The popover doesn't dim the
page. Selecting outside it, or Escape, closes it. From the keyboard, opening a
popover moves focus into it, and Escape puts focus back on the badge. The filter

View file

@ -14,6 +14,7 @@ import { AgentInference } from "./AgentInference.js";
import { AgentInstructions } from "./AgentInstructions.js";
import { AgentLimits } from "./AgentLimits.js";
import { AgentPermissions } from "./AgentPermissions.js";
import { AgentStateRow } from "./AgentStateRow.js";
import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
import { LinkAccountMenu } from "./LinkAccountMenu.js";
@ -76,6 +77,10 @@ export function AgentDetail({
/>
) : null}
</dd>
<dt>state</dt>
<dd>
<AgentStateRow key={row.name} agent={row.name} />
</dd>
<dt>hive</dt>
<dd>{row.hive ?? "—"}</dd>
<dt>config PR</dt>

View file

@ -0,0 +1,11 @@
/* The badges of the details table's state row, wrapping rather than clipping
in a narrow panel. */
.ui-agent-state-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25em;
}
.ui-agent-state-waiting {
opacity: 0.7;
}

View file

@ -0,0 +1,37 @@
// <AgentStateRow> — an agent's turn state, whether it is paused, and the
// tokens in its context and billed, as the details table's "state" row. The
// terminal preview below the table carries no badges of its own (the full
// terminal tab does), so this is where the panel says what the agent is doing.
// Follows the agent's state stream, which announces on a change and once a
// minute, so the row reads "waiting" until the first report lands.
import {
StateBadges,
termPaths,
tokenTotal,
} from "../terminal/AgentTermPreview.js";
import { useSwarmAgentStateStream } from "../terminal/useSwarmAgentStateStream.js";
import "./AgentStateRow.css";
export function AgentStateRow({ agent }: { agent: string }) {
const { header } = useSwarmAgentStateStream(termPaths(agent).state);
if (!header) {
return (
<span
class="ui-agent-state-waiting"
title="the agent reports its state when it changes, and once a minute"
>
waiting for the agent's report…
</span>
);
}
return (
<div class="ui-agent-state-row">
<StateBadges
header={header}
ctx={tokenTotal(header.ctx_usage)}
cost={tokenTotal(header.cost_usage)}
withModel={false}
/>
</div>
);
}

View file

@ -73,13 +73,13 @@
this: with the base padding, "thinking"/"model sonnet" sat directly
on top of the terminal's first line whenever content started right
at the top (a running preview scrolled to the newest rows almost
always does). Sized for two badge rows, since the badges wrap in the
narrow details panel; each keeps the 2.75em touch-target floor. */
padding-block-start: 7.4em;
always does). One badge row: the connection badge and the expand
trigger, each keeping the 2.75em touch-target floor. */
padding-block-start: 3.4em;
}
/* The floating row over the terminal's top edge: state badges (connection
first, then turn_state/agent_state/model/ctx/cost) on the left, the
/* The floating row over the terminal's top edge: the connection badge
(and on the full tab turn_state/agent_state/model/ctx/cost) on the left, the
expand trigger on the right. Positioned by `.terminal-wrap`'s own
`position: relative`, same anchor `@hive/shared/terminal.css`'s
`.tail-pill` uses, which anchors bottom-right and so never competes for

View file

@ -19,15 +19,11 @@
// here needs to be interactive. Sourced from `useSwarmAgentStateStream`,
// not from `AgentRow` — the roster fetch `AgentsPage` already did has no
// notion of turn_state/model/ctx/cost, only `wanted`/`snapshot.running`.
// `showHeaderBadges` (default true) exists for a caller that already
// shows this info elsewhere and wants this cluster suppressed — checked
// against the original plan for this page, which proposed exactly that
// for `AgentsPage`'s embedded usage; that page's detail panel turned out
// NOT to duplicate any of this (`AgentRow`/`AgentTypes.ts` carry none of
// turn_state/model/ctx/cost), so `AgentsPage` leaves this at its default
// rather than hiding the only place swarm-ui shows it — see the commit
// message for the full reasoning. The prop still exists because
// suppressing it is a real, reusable need in principle, just not here.
// Only the full-tab mount (`fullHeight`) shows the state badges: the small
// preview in the detail panel stays free of them (mara: "the preview is small
// and should not be encroached further with badges on top, but for the full
// screen term i want it like that"), and the panel's details table lists the
// same state in a row of its own (`AgentStateRow`).
// **Expand-to-fullscreen trigger** — opens/navigates to
// `/agents/:name/terminal` (`AgentTerminalPage`) via `useDynamicTabs`,
@ -84,7 +80,7 @@ const TURN_STATE_TONE: Record<string, BadgeTone> = {
compacting: "warning",
};
function tokenTotal(u: TokenUsage | null): number | null {
export function tokenTotal(u: TokenUsage | null): number | null {
if (!u) return null;
// Matches `Root.tsx`'s own `tokenTotal`: input + both cache buckets,
// `output_tokens` deliberately excluded (shown in a title breakdown
@ -133,14 +129,17 @@ export function termPaths(agentName: string, subagent?: string): TermPaths {
// Turn state, agent state, model, ctx and cost, as the badges beside the
// connection one. Siblings only, no wrapper: they sit in the caller's row.
function StateBadges({
export function StateBadges({
header,
ctx,
cost,
withModel = true,
}: {
header: NonNullable<ReturnType<typeof useSwarmAgentStateStream>["header"]>;
ctx: number | null;
cost: number | null;
// Off where the model is shown beside them, as in the details table.
withModel?: boolean;
}) {
return (
<>
@ -158,15 +157,17 @@ function StateBadges({
{header.agent_state !== "up" && (
<Badge tone="warning" value={header.agent_state} />
)}
<Badge
label="model"
value={header.model}
title={
header.resolved_model
? `resolved to ${header.resolved_model}`
: undefined
}
/>
{withModel && (
<Badge
label="model"
value={header.model}
title={
header.resolved_model
? `resolved to ${header.resolved_model}`
: undefined
}
/>
)}
{ctx !== null && (
<Badge
label="ctx"
@ -188,7 +189,6 @@ function StateBadges({
export function AgentTermPreview({
agentName,
subagent,
showHeaderBadges = true,
fullHeight = false,
leading,
}: {
@ -196,7 +196,8 @@ export function AgentTermPreview({
// Set: this is one of `agentName`'s subagents, output only like the
// agent's own.
subagent?: string;
showHeaderBadges?: boolean;
// The full-tab mount: fills the tab, has no expand trigger, and shows the
// agent's state badges beside the connection one.
fullHeight?: boolean;
// A badge ahead of the connection badge, for the full-tab mount's
// link back to the agent's details.
@ -204,8 +205,10 @@ export function AgentTermPreview({
}) {
const paths = termPaths(agentName, subagent);
const { rows, connection, reconnect } = useSwarmTermStream(paths.stream);
// The small preview has no state badges, so it does not follow the state
// stream at all.
const { header, reconnect: reconnectState } = useSwarmAgentStateStream(
paths.state,
fullHeight ? paths.state : null,
);
const { open: openTab } = useDynamicTabs();
const logRef = useRef<HTMLDivElement>(null);
@ -259,9 +262,7 @@ export function AgentTermPreview({
}}
/>
)}
{header && showHeaderBadges && (
<StateBadges header={header} ctx={ctx} cost={cost} />
)}
{header && <StateBadges header={header} ctx={ctx} cost={cost} />}
</div>
{!fullHeight && (
<Badge

View file

@ -62,7 +62,6 @@ export function AgentTerminalPage({
agentName={name}
subagent={subagent}
fullHeight
showHeaderBadges
leading={
<>
<span ref={badgeRef}>