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:
parent
4bd81d5b0f
commit
5d5df29687
8 changed files with 295 additions and 200 deletions
|
|
@ -9,22 +9,12 @@
|
|||
.ui-agent-instructions-body {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.ui-agent-instructions-text textarea,
|
||||
.ui-agent-instructions-theirs {
|
||||
.ui-agent-instructions-text textarea {
|
||||
font-family: var(--mono, ui-monospace, monospace);
|
||||
font-size: 0.9em;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ui-agent-instructions-theirs {
|
||||
margin: 0;
|
||||
max-height: 16em;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--border, currentColor);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ui-agent-instructions-status {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { Preformatted } from "../../ui/preformatted/Preformatted.js";
|
||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
|
||||
|
|
@ -261,9 +262,9 @@ function InstructionsForm({
|
|||
{agent} changed AGENTS.md since you started. Your text is kept
|
||||
above. Here is the file now:
|
||||
</p>
|
||||
<pre class="ui-agent-instructions-theirs" tabIndex={0}>
|
||||
<Preformatted wrap maxHeight="16em">
|
||||
{theirs.content === "" ? "(empty)" : theirs.content}
|
||||
</pre>
|
||||
</Preformatted>
|
||||
<ActionRow class="ui-agent-instructions-actions">
|
||||
<Button onClick={keepMine}>
|
||||
keep my text, replace {agent}'s changes
|
||||
|
|
|
|||
|
|
@ -7,11 +7,8 @@
|
|||
// an agent that is offline and so never reports. When the browser gives up on
|
||||
// the stream, a **reconnect** badge opens it again, as the terminal's does.
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import {
|
||||
StateBadges,
|
||||
termPaths,
|
||||
tokenTotal,
|
||||
} from "../terminal/AgentTermPreview.js";
|
||||
import { StateBadges, tokenTotal } from "../state/StateBadges.js";
|
||||
import { termPaths } from "../terminal/term-paths.js";
|
||||
import { useSwarmAgentStateStream } from "../terminal/useSwarmAgentStateStream.js";
|
||||
import "./AgentStateRow.css";
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
|
|||
import { AgentIcon } from "../AgentIcon.js";
|
||||
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
||||
import { AgentDetail } from "../detail/AgentDetail.js";
|
||||
import { termPaths } from "../terminal/AgentTermPreview.js";
|
||||
import { termPaths } from "../terminal/term-paths.js";
|
||||
import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
|
||||
import type { AgentActions } from "./AgentActions.js";
|
||||
import { WantedMenu } from "./WantedMenu.js";
|
||||
|
|
|
|||
112
frontend/packages/swarm-ui/src/agent/state/StateBadges.tsx
Normal file
112
frontend/packages/swarm-ui/src/agent/state/StateBadges.tsx
Normal 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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -42,11 +42,9 @@ import {
|
|||
useSwarmTermStream,
|
||||
type ConnectionState,
|
||||
} from "./useSwarmTermStream.js";
|
||||
import {
|
||||
useSwarmAgentStateStream,
|
||||
type TokenUsage,
|
||||
} from "./useSwarmAgentStateStream.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { useSwarmAgentStateStream } from "./useSwarmAgentStateStream.js";
|
||||
import { termPaths } from "./term-paths.js";
|
||||
import { StateBadges, tokenTotal } from "../state/StateBadges.js";
|
||||
import { AgentInputForm } from "./AgentInputForm.js";
|
||||
import { CancelTurnBadge } from "./CancelTurnBadge.js";
|
||||
import "./AgentTermPreview.css";
|
||||
|
|
@ -67,140 +65,6 @@ const CONNECTION_TONE: Record<ConnectionState, BadgeTone> = {
|
|||
closed: "negative",
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// The paths one terminal is reached at. A subagent's terminal sits under its
|
||||
// agent's, and has no turn-state header: the swarm relays none for it.
|
||||
interface TermPaths {
|
||||
label: string;
|
||||
page: string;
|
||||
stream: string;
|
||||
state: string | null;
|
||||
}
|
||||
|
||||
export function termPaths(agentName: string, subagent?: string): TermPaths {
|
||||
const agent = `/agents/${encodeURIComponent(agentName)}`;
|
||||
if (subagent === undefined) {
|
||||
return {
|
||||
label: agentName,
|
||||
page: `${agent}/terminal`,
|
||||
stream: `/api${agent}/term/stream`,
|
||||
state: `/api${agent}/state/stream`,
|
||||
};
|
||||
}
|
||||
const sub = `${agent}/subagents/${encodeURIComponent(subagent)}`;
|
||||
return {
|
||||
label: `${agentName}/${subagent}`,
|
||||
page: `${sub}/terminal`,
|
||||
stream: `/api${sub}/term/stream`,
|
||||
state: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentTermPreview({
|
||||
agentName,
|
||||
subagent,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { Badge } from "@hive/shared/badge.js";
|
|||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { Popover } from "../../ui/popover/Popover.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { termPaths } from "./AgentTermPreview.js";
|
||||
import { termPaths } from "./term-paths.js";
|
||||
import {
|
||||
runLabel,
|
||||
runningLabel,
|
||||
|
|
|
|||
27
frontend/packages/swarm-ui/src/agent/terminal/term-paths.ts
Normal file
27
frontend/packages/swarm-ui/src/agent/terminal/term-paths.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// The paths one terminal is reached at. A subagent's terminal sits under its
|
||||
// agent's, and has no turn-state header: the swarm relays none for it.
|
||||
interface TermPaths {
|
||||
label: string;
|
||||
page: string;
|
||||
stream: string;
|
||||
state: string | null;
|
||||
}
|
||||
|
||||
export function termPaths(agentName: string, subagent?: string): TermPaths {
|
||||
const agent = `/agents/${encodeURIComponent(agentName)}`;
|
||||
if (subagent === undefined) {
|
||||
return {
|
||||
label: agentName,
|
||||
page: `${agent}/terminal`,
|
||||
stream: `/api${agent}/term/stream`,
|
||||
state: `/api${agent}/state/stream`,
|
||||
};
|
||||
}
|
||||
const sub = `${agent}/subagents/${encodeURIComponent(subagent)}`;
|
||||
return {
|
||||
label: `${agentName}/${subagent}`,
|
||||
page: `${sub}/terminal`,
|
||||
stream: `/api${sub}/term/stream`,
|
||||
state: null,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue