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
28 changed files with 295 additions and 200 deletions
|
|
@ -9,22 +9,12 @@
|
||||||
.ui-agent-instructions-body {
|
.ui-agent-instructions-body {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
.ui-agent-instructions-text textarea,
|
.ui-agent-instructions-text textarea {
|
||||||
.ui-agent-instructions-theirs {
|
|
||||||
font-family: var(--mono, ui-monospace, monospace);
|
font-family: var(--mono, ui-monospace, monospace);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
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 {
|
.ui-agent-instructions-status {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--muted);
|
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 { Badge } from "@hive/shared/badge.js";
|
||||||
import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
||||||
import { Button } from "../../ui/button/Button.js";
|
import { Button } from "../../ui/button/Button.js";
|
||||||
|
import { Preformatted } from "../../ui/preformatted/Preformatted.js";
|
||||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||||
import { Panel } from "../../ui/panel/Panel.js";
|
import { Panel } from "../../ui/panel/Panel.js";
|
||||||
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.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
|
{agent} changed AGENTS.md since you started. Your text is kept
|
||||||
above. Here is the file now:
|
above. Here is the file now:
|
||||||
</p>
|
</p>
|
||||||
<pre class="ui-agent-instructions-theirs" tabIndex={0}>
|
<Preformatted wrap maxHeight="16em">
|
||||||
{theirs.content === "" ? "(empty)" : theirs.content}
|
{theirs.content === "" ? "(empty)" : theirs.content}
|
||||||
</pre>
|
</Preformatted>
|
||||||
<ActionRow class="ui-agent-instructions-actions">
|
<ActionRow class="ui-agent-instructions-actions">
|
||||||
<Button onClick={keepMine}>
|
<Button onClick={keepMine}>
|
||||||
keep my text, replace {agent}'s changes
|
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
|
// 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.
|
// the stream, a **reconnect** badge opens it again, as the terminal's does.
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import {
|
import { StateBadges, tokenTotal } from "../state/StateBadges.js";
|
||||||
StateBadges,
|
import { termPaths } from "../terminal/term-paths.js";
|
||||||
termPaths,
|
|
||||||
tokenTotal,
|
|
||||||
} from "../terminal/AgentTermPreview.js";
|
|
||||||
import { useSwarmAgentStateStream } from "../terminal/useSwarmAgentStateStream.js";
|
import { useSwarmAgentStateStream } from "../terminal/useSwarmAgentStateStream.js";
|
||||||
import "./AgentStateRow.css";
|
import "./AgentStateRow.css";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
|
||||||
import { AgentIcon } from "../AgentIcon.js";
|
import { AgentIcon } from "../AgentIcon.js";
|
||||||
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
||||||
import { AgentDetail } from "../detail/AgentDetail.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 { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
|
||||||
import type { AgentActions } from "./AgentActions.js";
|
import type { AgentActions } from "./AgentActions.js";
|
||||||
import { WantedMenu } from "./WantedMenu.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,
|
useSwarmTermStream,
|
||||||
type ConnectionState,
|
type ConnectionState,
|
||||||
} from "./useSwarmTermStream.js";
|
} from "./useSwarmTermStream.js";
|
||||||
import {
|
import { useSwarmAgentStateStream } from "./useSwarmAgentStateStream.js";
|
||||||
useSwarmAgentStateStream,
|
import { termPaths } from "./term-paths.js";
|
||||||
type TokenUsage,
|
import { StateBadges, tokenTotal } from "../state/StateBadges.js";
|
||||||
} from "./useSwarmAgentStateStream.js";
|
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
|
||||||
import { AgentInputForm } from "./AgentInputForm.js";
|
import { AgentInputForm } from "./AgentInputForm.js";
|
||||||
import { CancelTurnBadge } from "./CancelTurnBadge.js";
|
import { CancelTurnBadge } from "./CancelTurnBadge.js";
|
||||||
import "./AgentTermPreview.css";
|
import "./AgentTermPreview.css";
|
||||||
|
|
@ -67,140 +65,6 @@ const CONNECTION_TONE: Record<ConnectionState, BadgeTone> = {
|
||||||
closed: "negative",
|
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({
|
export function AgentTermPreview({
|
||||||
agentName,
|
agentName,
|
||||||
subagent,
|
subagent,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { Badge } from "@hive/shared/badge.js";
|
||||||
import { Panel } from "../../ui/panel/Panel.js";
|
import { Panel } from "../../ui/panel/Panel.js";
|
||||||
import { Popover } from "../../ui/popover/Popover.js";
|
import { Popover } from "../../ui/popover/Popover.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import { termPaths } from "./AgentTermPreview.js";
|
import { termPaths } from "./term-paths.js";
|
||||||
import {
|
import {
|
||||||
runLabel,
|
runLabel,
|
||||||
runningLabel,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,8 @@ import { FormStack } from "../ui/form-stack/FormStack.js";
|
||||||
import { Hint, type HintTone } from "../ui/hint/Hint.js";
|
import { Hint, type HintTone } from "../ui/hint/Hint.js";
|
||||||
import { FormResult } from "../ui/submit/FormResult.js";
|
import { FormResult } from "../ui/submit/FormResult.js";
|
||||||
import type { SubmitState } from "../ui/submit/submit.js";
|
import type { SubmitState } from "../ui/submit/submit.js";
|
||||||
|
import { Preformatted } from "../ui/preformatted/Preformatted.js";
|
||||||
|
import { SectionHeading } from "../ui/section-heading/SectionHeading.js";
|
||||||
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
||||||
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
|
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
|
||||||
import { Popover } from "../ui/popover/Popover.js";
|
import { Popover } from "../ui/popover/Popover.js";
|
||||||
|
|
@ -667,6 +669,24 @@ export function ComponentsPage() {
|
||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title="SectionHeading and Preformatted">
|
||||||
|
<Sample label="SectionHeading">
|
||||||
|
<SectionHeading>resource caps</SectionHeading>
|
||||||
|
</Sample>
|
||||||
|
<Sample label="Preformatted: scrolls sideways, and wrapped">
|
||||||
|
<Preformatted maxHeight="6em">
|
||||||
|
{
|
||||||
|
"error: builder for '/nix/store/…drv' failed with exit code 1; last 10 log lines follow below, long lines keep their length and scroll sideways"
|
||||||
|
}
|
||||||
|
</Preformatted>
|
||||||
|
<Preformatted wrap maxHeight="6em">
|
||||||
|
{
|
||||||
|
"A file's text, wrapped to the width of the block so it can be read without scrolling sideways."
|
||||||
|
}
|
||||||
|
</Preformatted>
|
||||||
|
</Sample>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section title="ExpandableCard">
|
<Section title="ExpandableCard">
|
||||||
<Sample label="closed, open, and the loading twin">
|
<Sample label="closed, open, and the loading twin">
|
||||||
<ExpandableCard
|
<ExpandableCard
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,7 @@
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.create-agent-heading {
|
.create-agent-heading {
|
||||||
margin: 0.5em 0 0;
|
margin-top: 0.5em;
|
||||||
font-size: 1em;
|
|
||||||
}
|
}
|
||||||
.create-agent-footer {
|
.create-agent-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import {
|
||||||
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
|
||||||
import { FormField } from "../ui/form-field/FormField.js";
|
import { FormField } from "../ui/form-field/FormField.js";
|
||||||
import { Hint } from "../ui/hint/Hint.js";
|
import { Hint } from "../ui/hint/Hint.js";
|
||||||
|
import { SectionHeading } from "../ui/section-heading/SectionHeading.js";
|
||||||
import { useSubmit } from "../ui/submit/useSubmit.js";
|
import { useSubmit } from "../ui/submit/useSubmit.js";
|
||||||
import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js";
|
import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -456,7 +457,9 @@ export function CreateAgentForm({
|
||||||
Optional: leave a box empty to set nothing, and the hive's or
|
Optional: leave a box empty to set nothing, and the hive's or
|
||||||
the agent's own value applies.
|
the agent's own value applies.
|
||||||
</p>
|
</p>
|
||||||
<h3 class="create-agent-heading">resource caps</h3>
|
<SectionHeading class="create-agent-heading">
|
||||||
|
resource caps
|
||||||
|
</SectionHeading>
|
||||||
<LimitsFields
|
<LimitsFields
|
||||||
idPrefix="create-limits"
|
idPrefix="create-limits"
|
||||||
cpu={cpu}
|
cpu={cpu}
|
||||||
|
|
@ -464,7 +467,9 @@ export function CreateAgentForm({
|
||||||
onCpu={setCpu}
|
onCpu={setCpu}
|
||||||
onMemory={setMemory}
|
onMemory={setMemory}
|
||||||
/>
|
/>
|
||||||
<h3 class="create-agent-heading">inference</h3>
|
<SectionHeading class="create-agent-heading">
|
||||||
|
inference
|
||||||
|
</SectionHeading>
|
||||||
<InferenceFields
|
<InferenceFields
|
||||||
idPrefix="create-inference"
|
idPrefix="create-inference"
|
||||||
draft={inference}
|
draft={inference}
|
||||||
|
|
@ -496,7 +501,9 @@ export function CreateAgentForm({
|
||||||
today's literal behaviour: mara's call on this copy was to
|
today's literal behaviour: mara's call on this copy was to
|
||||||
read as finished, not as a running commentary on partial
|
read as finished, not as a running commentary on partial
|
||||||
implementation. */}
|
implementation. */}
|
||||||
<h3 class="create-agent-heading">what this creates</h3>
|
<SectionHeading class="create-agent-heading">
|
||||||
|
what this creates
|
||||||
|
</SectionHeading>
|
||||||
<p>
|
<p>
|
||||||
Creating queues everything a new agent needs: a swarm-level
|
Creating queues everything a new agent needs: a swarm-level
|
||||||
identity, a config repo on the forge with the operator added as
|
identity, a config repo on the forge with the operator added as
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,8 @@ import { Badge } from "@hive/shared/badge.js";
|
||||||
import { CloseIcon, ExpandIcon } from "@hive/shared/icons.js";
|
import { CloseIcon, ExpandIcon } from "@hive/shared/icons.js";
|
||||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||||
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
||||||
import {
|
import { AgentTermPreview } from "../../agent/terminal/AgentTermPreview.js";
|
||||||
AgentTermPreview,
|
import { termPaths } from "../../agent/terminal/term-paths.js";
|
||||||
termPaths,
|
|
||||||
} from "../../agent/terminal/AgentTermPreview.js";
|
|
||||||
import {
|
import {
|
||||||
agentsIn,
|
agentsIn,
|
||||||
groupHref,
|
groupHref,
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,8 @@ import { Badge } from "@hive/shared/badge.js";
|
||||||
import { AgentDetailPopover } from "../../agent/detail/AgentDetailPopover.js";
|
import { AgentDetailPopover } from "../../agent/detail/AgentDetailPopover.js";
|
||||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||||
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
import { SubagentsMenu } from "../../agent/terminal/SubagentsMenu.js";
|
||||||
import {
|
import { AgentTermPreview } from "../../agent/terminal/AgentTermPreview.js";
|
||||||
AgentTermPreview,
|
import { termPaths } from "../../agent/terminal/term-paths.js";
|
||||||
termPaths,
|
|
||||||
} from "../../agent/terminal/AgentTermPreview.js";
|
|
||||||
import { useAgentFavicon } from "../../agent/terminal/useAgentFavicon.js";
|
import { useAgentFavicon } from "../../agent/terminal/useAgentFavicon.js";
|
||||||
import {
|
import {
|
||||||
groupHref,
|
groupHref,
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,3 @@
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
/* What nix printed: kept as printed, scrolls sideways rather than wrapping. */
|
/* What nix printed: kept as printed, scrolls sideways rather than wrapping. */
|
||||||
.ui-builds-tail {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.6em 0.75em;
|
|
||||||
max-height: 24em;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 0.4em;
|
|
||||||
background: color-mix(in srgb, var(--crust) 60%, transparent);
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
ExpandableCardListSkeleton,
|
ExpandableCardListSkeleton,
|
||||||
} from "../../ui/expandable-card/ExpandableCard.js";
|
} from "../../ui/expandable-card/ExpandableCard.js";
|
||||||
import { Hint } from "../../ui/hint/Hint.js";
|
import { Hint } from "../../ui/hint/Hint.js";
|
||||||
|
import { Preformatted } from "../../ui/preformatted/Preformatted.js";
|
||||||
import { Toolbar } from "../../ui/toolbar/Toolbar.js";
|
import { Toolbar } from "../../ui/toolbar/Toolbar.js";
|
||||||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -23,7 +24,7 @@ import {
|
||||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import { durationLabel } from "../review-runs/RunsTypes.js";
|
import { durationLabel } from "../../util.js";
|
||||||
import {
|
import {
|
||||||
OUTCOME_LABEL,
|
OUTCOME_LABEL,
|
||||||
OUTCOME_TONE,
|
OUTCOME_TONE,
|
||||||
|
|
@ -80,7 +81,7 @@ function BuildCard({
|
||||||
<>
|
<>
|
||||||
<Hint>{body.label}</Hint>
|
<Hint>{body.label}</Hint>
|
||||||
{body.preformatted ? (
|
{body.preformatted ? (
|
||||||
<pre class="ui-builds-tail">{body.text}</pre>
|
<Preformatted>{body.text}</Preformatted>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-builds-text">{body.text}</p>
|
<p class="ui-builds-text">{body.text}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* A finished run's table, the overall verdict under it and the head commit. */
|
||||||
|
.ui-tryout-overall,
|
||||||
|
.ui-tryout-sha {
|
||||||
|
margin: 0.75em 0 0;
|
||||||
|
}
|
||||||
|
.ui-tryout-sha {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// <TryOutResult> — a finished try-out: one row per classification, the
|
||||||
|
// overall verdict under them, and the head commit the run read. One column per
|
||||||
|
// field of a row, where the posted review folds them into four
|
||||||
|
// (`docs/4-implementation/swarm/review-classifier.md`); no token
|
||||||
|
// counts, cache counts or cost, here or anywhere in the UI.
|
||||||
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
|
import { Hint } from "../../ui/hint/Hint.js";
|
||||||
|
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||||
|
import type {
|
||||||
|
TryOutResult as Result,
|
||||||
|
TryOutRow,
|
||||||
|
} from "../review-tryout/TryOutTypes.js";
|
||||||
|
import "./TryOutResult.css";
|
||||||
|
|
||||||
|
const VERDICT_LABEL: Record<TryOutRow["verdict"], string> = {
|
||||||
|
match: "match",
|
||||||
|
"no-match": "no match",
|
||||||
|
"not-applicable": "not applicable",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLUMNS: TableColumn<TryOutRow>[] = [
|
||||||
|
{ key: "name", header: "classification", render: (r) => r.name },
|
||||||
|
{ key: "polarity", header: "polarity", render: (r) => r.polarity },
|
||||||
|
{
|
||||||
|
key: "verdict",
|
||||||
|
header: "verdict",
|
||||||
|
render: (r) => (r.error ? "—" : VERDICT_LABEL[r.verdict]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "certainty",
|
||||||
|
header: "certainty",
|
||||||
|
render: (r) => (r.error ? "—" : r.certainty),
|
||||||
|
},
|
||||||
|
{ key: "threshold", header: "threshold", render: (r) => r.threshold },
|
||||||
|
{
|
||||||
|
key: "counted",
|
||||||
|
header: "counted",
|
||||||
|
// A counted row shows whether it passed its polarity; one below its
|
||||||
|
// threshold does not count and has no pass or fail.
|
||||||
|
render: (r) =>
|
||||||
|
r.error ? "—" : r.counted ? (r.passed ? "yes ✅" : "yes ❌") : "no",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "reason",
|
||||||
|
header: "reason",
|
||||||
|
cellClass: "ui-table-prose",
|
||||||
|
// A row that could not run says why where the reason would be.
|
||||||
|
render: (r) =>
|
||||||
|
r.error ? (
|
||||||
|
<Hint tone="error" as="span" class="ui-tryout-row-error">
|
||||||
|
{r.error}
|
||||||
|
</Hint>
|
||||||
|
) : (
|
||||||
|
r.reason
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** What the table needs of a run: the try-out's answer, or a listed run's. The
|
||||||
|
* head commit is absent for a run that never read one. */
|
||||||
|
export type TryOutAnswer = Pick<Result, "rows" | "overall"> & {
|
||||||
|
head_sha?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TryOutResult({
|
||||||
|
result,
|
||||||
|
filterKey = "swarm-ui:review-tryout:table-filters",
|
||||||
|
}: {
|
||||||
|
result: TryOutAnswer;
|
||||||
|
/** Where the column filters are remembered; a page showing several tables
|
||||||
|
* keeps them apart from the try-out's. */
|
||||||
|
filterKey?: string;
|
||||||
|
}) {
|
||||||
|
const rows = result.rows ?? [];
|
||||||
|
return (
|
||||||
|
<div class="ui-tryout-result">
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
rows={rows}
|
||||||
|
// By position: two classifications may share a name, and a shared key
|
||||||
|
// would let one result render over the other.
|
||||||
|
rowKey={(r) => String(rows.indexOf(r))}
|
||||||
|
storageKey={filterKey}
|
||||||
|
emptyMessage="the run returned no classifications"
|
||||||
|
/>
|
||||||
|
{result.overall ? (
|
||||||
|
<p class="ui-tryout-overall">
|
||||||
|
<strong>Overall: </strong>
|
||||||
|
<Badge
|
||||||
|
tone={result.overall === "approve" ? "positive" : "negative"}
|
||||||
|
value={result.overall === "approve" ? "approve" : "request changes"}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{result.head_sha ? (
|
||||||
|
<p class="ui-tryout-sha">
|
||||||
|
Ran on head <code>{result.head_sha.slice(0, 12)}</code>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -21,13 +21,13 @@ import {
|
||||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import { TryOutResult } from "../review-tryout/TryOutResult.js";
|
import { TryOutResult } from "../review-common/TryOutResult.js";
|
||||||
|
import { durationLabel } from "../../util.js";
|
||||||
import {
|
import {
|
||||||
OUTCOME_LABEL,
|
OUTCOME_LABEL,
|
||||||
OUTCOME_TONE,
|
OUTCOME_TONE,
|
||||||
TRIGGER_LABEL,
|
TRIGGER_LABEL,
|
||||||
classificationCount,
|
classificationCount,
|
||||||
durationLabel,
|
|
||||||
type ClassifierRun,
|
type ClassifierRun,
|
||||||
} from "./RunsTypes.js";
|
} from "./RunsTypes.js";
|
||||||
import "./ReviewRuns.css";
|
import "./ReviewRuns.css";
|
||||||
|
|
|
||||||
|
|
@ -60,16 +60,6 @@ export const TRIGGER_LABEL: Record<RunTrigger, string> = {
|
||||||
review: "requested review",
|
review: "requested review",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** How long a run took, as `45s`, `3m 12s` or `1h 4m`. A clock that ran
|
|
||||||
* backwards reads as `0s`. */
|
|
||||||
export function durationLabel(startedUnix: number, finishedUnix: number) {
|
|
||||||
const total = Math.max(0, Math.round(finishedUnix - startedUnix));
|
|
||||||
if (total < 60) return `${total}s`;
|
|
||||||
const minutes = Math.floor(total / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m ${total % 60}s`;
|
|
||||||
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** How many classifications ran, in words. A run that never got as far as
|
/** How many classifications ran, in words. A run that never got as far as
|
||||||
* running any has no rows. */
|
* running any has no rows. */
|
||||||
export function classificationCount(run: ClassifierRun): string {
|
export function classificationCount(run: ClassifierRun): string {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import { ExternalLink } from "../../ui/external-link/ExternalLink.js";
|
||||||
import { Panel } from "../../ui/panel/Panel.js";
|
import { Panel } from "../../ui/panel/Panel.js";
|
||||||
import { TextField } from "../../ui/text-field/TextField.js";
|
import { TextField } from "../../ui/text-field/TextField.js";
|
||||||
import { ClassificationEditor } from "./ClassificationEditor.js";
|
import { ClassificationEditor } from "./ClassificationEditor.js";
|
||||||
import { TryOutResult } from "./TryOutResult.js";
|
import { TryOutResult } from "../review-common/TryOutResult.js";
|
||||||
import {
|
import {
|
||||||
addProblem,
|
addProblem,
|
||||||
draftFromRule,
|
draftFromRule,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue