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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ export function AgentTerminalPage({
|
|||
agentName={name}
|
||||
subagent={subagent}
|
||||
fullHeight
|
||||
showHeaderBadges
|
||||
leading={
|
||||
<>
|
||||
<span ref={badgeRef}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue