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:
parent
8899dca807
commit
705160c0fd
7 changed files with 94 additions and 33 deletions
|
|
@ -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
|
list closes its terminal, and a later subagent with the same name starts
|
||||||
unpicked.
|
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
|
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
|
||||||
subagents, each a link to its terminal; on a subagent's page the current one
|
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
|
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:
|
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
|
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
|
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
|
popover moves focus into it, and Escape puts focus back on the badge. The filter
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { AgentInference } from "./AgentInference.js";
|
||||||
import { AgentInstructions } from "./AgentInstructions.js";
|
import { AgentInstructions } from "./AgentInstructions.js";
|
||||||
import { AgentLimits } from "./AgentLimits.js";
|
import { AgentLimits } from "./AgentLimits.js";
|
||||||
import { AgentPermissions } from "./AgentPermissions.js";
|
import { AgentPermissions } from "./AgentPermissions.js";
|
||||||
|
import { AgentStateRow } from "./AgentStateRow.js";
|
||||||
import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
|
import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
|
||||||
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
|
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
|
||||||
import { LinkAccountMenu } from "./LinkAccountMenu.js";
|
import { LinkAccountMenu } from "./LinkAccountMenu.js";
|
||||||
|
|
@ -76,6 +77,10 @@ export function AgentDetail({
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt>state</dt>
|
||||||
|
<dd>
|
||||||
|
<AgentStateRow key={row.name} agent={row.name} />
|
||||||
|
</dd>
|
||||||
<dt>hive</dt>
|
<dt>hive</dt>
|
||||||
<dd>{row.hive ?? "—"}</dd>
|
<dd>{row.hive ?? "—"}</dd>
|
||||||
<dt>config PR</dt>
|
<dt>config PR</dt>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -73,13 +73,13 @@
|
||||||
this: with the base padding, "thinking"/"model sonnet" sat directly
|
this: with the base padding, "thinking"/"model sonnet" sat directly
|
||||||
on top of the terminal's first line whenever content started right
|
on top of the terminal's first line whenever content started right
|
||||||
at the top (a running preview scrolled to the newest rows almost
|
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
|
always does). One badge row: the connection badge and the expand
|
||||||
narrow details panel; each keeps the 2.75em touch-target floor. */
|
trigger, each keeping the 2.75em touch-target floor. */
|
||||||
padding-block-start: 7.4em;
|
padding-block-start: 3.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The floating row over the terminal's top edge: state badges (connection
|
/* The floating row over the terminal's top edge: the connection badge
|
||||||
first, then turn_state/agent_state/model/ctx/cost) on the left, the
|
(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
|
expand trigger on the right. Positioned by `.terminal-wrap`'s own
|
||||||
`position: relative`, same anchor `@hive/shared/terminal.css`'s
|
`position: relative`, same anchor `@hive/shared/terminal.css`'s
|
||||||
`.tail-pill` uses, which anchors bottom-right and so never competes for
|
`.tail-pill` uses, which anchors bottom-right and so never competes for
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,11 @@
|
||||||
// here needs to be interactive. Sourced from `useSwarmAgentStateStream`,
|
// here needs to be interactive. Sourced from `useSwarmAgentStateStream`,
|
||||||
// not from `AgentRow` — the roster fetch `AgentsPage` already did has no
|
// not from `AgentRow` — the roster fetch `AgentsPage` already did has no
|
||||||
// notion of turn_state/model/ctx/cost, only `wanted`/`snapshot.running`.
|
// notion of turn_state/model/ctx/cost, only `wanted`/`snapshot.running`.
|
||||||
// `showHeaderBadges` (default true) exists for a caller that already
|
// Only the full-tab mount (`fullHeight`) shows the state badges: the small
|
||||||
// shows this info elsewhere and wants this cluster suppressed — checked
|
// preview in the detail panel stays free of them (mara: "the preview is small
|
||||||
// against the original plan for this page, which proposed exactly that
|
// and should not be encroached further with badges on top, but for the full
|
||||||
// for `AgentsPage`'s embedded usage; that page's detail panel turned out
|
// screen term i want it like that"), and the panel's details table lists the
|
||||||
// NOT to duplicate any of this (`AgentRow`/`AgentTypes.ts` carry none of
|
// same state in a row of its own (`AgentStateRow`).
|
||||||
// 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.
|
|
||||||
|
|
||||||
// **Expand-to-fullscreen trigger** — opens/navigates to
|
// **Expand-to-fullscreen trigger** — opens/navigates to
|
||||||
// `/agents/:name/terminal` (`AgentTerminalPage`) via `useDynamicTabs`,
|
// `/agents/:name/terminal` (`AgentTerminalPage`) via `useDynamicTabs`,
|
||||||
|
|
@ -84,7 +80,7 @@ const TURN_STATE_TONE: Record<string, BadgeTone> = {
|
||||||
compacting: "warning",
|
compacting: "warning",
|
||||||
};
|
};
|
||||||
|
|
||||||
function tokenTotal(u: TokenUsage | null): number | null {
|
export function tokenTotal(u: TokenUsage | null): number | null {
|
||||||
if (!u) return null;
|
if (!u) return null;
|
||||||
// Matches `Root.tsx`'s own `tokenTotal`: input + both cache buckets,
|
// Matches `Root.tsx`'s own `tokenTotal`: input + both cache buckets,
|
||||||
// `output_tokens` deliberately excluded (shown in a title breakdown
|
// `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
|
// 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.
|
// connection one. Siblings only, no wrapper: they sit in the caller's row.
|
||||||
function StateBadges({
|
export function StateBadges({
|
||||||
header,
|
header,
|
||||||
ctx,
|
ctx,
|
||||||
cost,
|
cost,
|
||||||
|
withModel = true,
|
||||||
}: {
|
}: {
|
||||||
header: NonNullable<ReturnType<typeof useSwarmAgentStateStream>["header"]>;
|
header: NonNullable<ReturnType<typeof useSwarmAgentStateStream>["header"]>;
|
||||||
ctx: number | null;
|
ctx: number | null;
|
||||||
cost: number | null;
|
cost: number | null;
|
||||||
|
// Off where the model is shown beside them, as in the details table.
|
||||||
|
withModel?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -158,15 +157,17 @@ function StateBadges({
|
||||||
{header.agent_state !== "up" && (
|
{header.agent_state !== "up" && (
|
||||||
<Badge tone="warning" value={header.agent_state} />
|
<Badge tone="warning" value={header.agent_state} />
|
||||||
)}
|
)}
|
||||||
<Badge
|
{withModel && (
|
||||||
label="model"
|
<Badge
|
||||||
value={header.model}
|
label="model"
|
||||||
title={
|
value={header.model}
|
||||||
header.resolved_model
|
title={
|
||||||
? `resolved to ${header.resolved_model}`
|
header.resolved_model
|
||||||
: undefined
|
? `resolved to ${header.resolved_model}`
|
||||||
}
|
: undefined
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{ctx !== null && (
|
{ctx !== null && (
|
||||||
<Badge
|
<Badge
|
||||||
label="ctx"
|
label="ctx"
|
||||||
|
|
@ -188,7 +189,6 @@ function StateBadges({
|
||||||
export function AgentTermPreview({
|
export function AgentTermPreview({
|
||||||
agentName,
|
agentName,
|
||||||
subagent,
|
subagent,
|
||||||
showHeaderBadges = true,
|
|
||||||
fullHeight = false,
|
fullHeight = false,
|
||||||
leading,
|
leading,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -196,7 +196,8 @@ export function AgentTermPreview({
|
||||||
// Set: this is one of `agentName`'s subagents, output only like the
|
// Set: this is one of `agentName`'s subagents, output only like the
|
||||||
// agent's own.
|
// agent's own.
|
||||||
subagent?: string;
|
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;
|
fullHeight?: boolean;
|
||||||
// A badge ahead of the connection badge, for the full-tab mount's
|
// A badge ahead of the connection badge, for the full-tab mount's
|
||||||
// link back to the agent's details.
|
// link back to the agent's details.
|
||||||
|
|
@ -204,8 +205,10 @@ export function AgentTermPreview({
|
||||||
}) {
|
}) {
|
||||||
const paths = termPaths(agentName, subagent);
|
const paths = termPaths(agentName, subagent);
|
||||||
const { rows, connection, reconnect } = useSwarmTermStream(paths.stream);
|
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(
|
const { header, reconnect: reconnectState } = useSwarmAgentStateStream(
|
||||||
paths.state,
|
fullHeight ? paths.state : null,
|
||||||
);
|
);
|
||||||
const { open: openTab } = useDynamicTabs();
|
const { open: openTab } = useDynamicTabs();
|
||||||
const logRef = useRef<HTMLDivElement>(null);
|
const logRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -259,9 +262,7 @@ export function AgentTermPreview({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{header && showHeaderBadges && (
|
{header && <StateBadges header={header} ctx={ctx} cost={cost} />}
|
||||||
<StateBadges header={header} ctx={ctx} cost={cost} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!fullHeight && (
|
{!fullHeight && (
|
||||||
<Badge
|
<Badge
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ export function AgentTerminalPage({
|
||||||
agentName={name}
|
agentName={name}
|
||||||
subagent={subagent}
|
subagent={subagent}
|
||||||
fullHeight
|
fullHeight
|
||||||
showHeaderBadges
|
|
||||||
leading={
|
leading={
|
||||||
<>
|
<>
|
||||||
<span ref={badgeRef}>
|
<span ref={badgeRef}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue