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

@ -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}>