Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: shared code leaves page folders; SectionHeading and Preformatted replace per-page copies

durationLabel moves to util.ts, termPaths to agent/terminal/term-paths.ts,
StateBadges and tokenTotal to agent/state, and TryOutResult to
pages/review-common, so the builds, runs and agent pages no longer import
across page folders. SectionHeading is the body-size section heading the
create form and the egress card each styled; Preformatted is the scrolling
text block of the builds tail and the AGENTS.md view, which now share one
chrome and are both keyboard-scrollable. The components page shows both.
This commit is contained in:
iris 2026-11-06 19:09:18 +01:00
commit 5d5df29687

View file

@ -0,0 +1,112 @@
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import {
useSwarmAgentStateStream,
type TokenUsage,
} from "../terminal/useSwarmAgentStateStream.js";
// Ported, not imported, same call `useSwarmTermStream`'s own `appendRow`
// already made for this file's sibling hook: `@hive/agent`'s equivalents
// (`STATE_LABELS`/`tokenTotal`/`fmtTokens` in `Root.tsx`/`lib/format.ts`)
// live in a different package this one doesn't otherwise depend on, and
// a cross-package hop isn't worth it for three small, stable helpers.
// Vocabulary (glyphless — the per-agent page's own glyphs are part of
// its bigger consolidated status badge, not this smaller read-only one)
// kept in exact parity with `Root.tsx`'s `STATE_LABELS` so "idle" here
// means the same thing it means there.
const TURN_STATE_TONE: Record<string, BadgeTone> = {
idle: "neutral",
thinking: "accent",
compacting: "warning",
};
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
// there, not carried at all here — this badge has no breakdown to put
// it in).
return (
(u.input_tokens ?? 0) +
(u.cache_read_input_tokens ?? 0) +
(u.cache_creation_input_tokens ?? 0)
);
}
function fmtTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
if (n >= 1_000) return Math.round(n / 1000) + "k";
return String(n);
}
// 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.
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 (
<>
<Badge
tone={TURN_STATE_TONE[header.turn_state] ?? "neutral"}
value={
<>
{header.turn_state} (
<RelativeTime epochMs={Date.parse(header.turn_state_since)} />)
</>
}
title="how long the agent has been in this state"
/>
{/* Only the non-boring case gets a badge — a healthy running
agent (`agent_state: "up"`) doesn't need one for the
obvious default. In practice this only ever fires for
"paused": `hive-agent` can only honestly publish "up" or
"paused" on this subject (see `AgentStateMsg::agent_state`'s
own doc), never "offline"/"destroyed" — a stopped or
destroyed agent publishes nothing at all. */}
{header.agent_state !== "up" && (
<Badge tone="warning" value={header.agent_state} />
)}
{withModel && (
<Badge
label="model"
value={header.model}
title={
header.resolved_model
? `resolved to ${header.resolved_model}`
: undefined
}
/>
)}
{withModel && (
<Badge
label="effort"
value={header.effort}
title={`set by: ${header.effort_source}`}
/>
)}
{ctx !== null && (
<Badge
label="ctx"
value={fmtTokens(ctx)}
title="tokens used in the current context window"
/>
)}
{cost !== null && (
<Badge
label="cost"
value={fmtTokens(cost)}
title="cumulative tokens billed across the last turn"
/>
)}
</>
);
}