Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
247 lines
6.6 KiB
TypeScript
247 lines
6.6 KiB
TypeScript
// <StatusChips> — the agent page's status row: the consolidated status
|
|
// badge (alive/thinking/paused/etc., with pause/resume + cancel-turn in
|
|
// its dropdown), the model + effort pickers, and context/cost usage.
|
|
// This is the concrete fix for the design guide's own named anti-example
|
|
// (the old page's model/effort *pickers* lived in the `⋯` overflow menu
|
|
// while the current model/effort only showed as a disconnected chip) —
|
|
// every badge here IS the control for what it displays, composed from
|
|
// `@hive/shared`'s `Badge`/`Dropdown` (../../shared/src/badge,
|
|
// ../../shared/src/dropdown). Presentational only: formatting +
|
|
// selection state live in the caller (`Root.tsx`, via the
|
|
// `useAgentState` hook), so this component can still be demoed and
|
|
// reviewed against plain sample data independent of live `/api/state`.
|
|
import { useRef, useState } from "preact/hooks";
|
|
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
|
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
|
|
import "./StatusChips.css";
|
|
|
|
export interface StatusChipsProps {
|
|
statusLabel: string;
|
|
statusTone: BadgeTone;
|
|
statusTooltip?: string;
|
|
model: string;
|
|
/** Concrete model id the last completed turn actually ran on, e.g.
|
|
* "claude-sonnet-4-5-20260805" — shown as the badge's tooltip so the
|
|
* operator can tell what an alias resolved to. `undefined` before any
|
|
* turn has completed. */
|
|
resolvedModel?: string;
|
|
availableModels: string[];
|
|
onSelectModel: (name: string) => void;
|
|
effort: string;
|
|
availableEfforts: string[];
|
|
onSelectEffort: (level: string) => void;
|
|
ctxLabel?: string;
|
|
costLabel?: string;
|
|
paused: boolean;
|
|
onTogglePause: () => void;
|
|
lastTurnLabel?: string;
|
|
thinking: boolean;
|
|
onCancelTurn: () => Promise<void>;
|
|
}
|
|
|
|
const MODEL_DESCRIPTIONS: Record<string, string> = {
|
|
haiku: "fast",
|
|
sonnet: "balanced",
|
|
opus: "powerful",
|
|
};
|
|
const EFFORT_DESCRIPTIONS: Record<string, string> = {
|
|
low: "",
|
|
medium: "default",
|
|
high: "",
|
|
xhigh: "",
|
|
max: "",
|
|
};
|
|
|
|
function Picker({
|
|
label,
|
|
value,
|
|
options,
|
|
descriptions,
|
|
onSelect,
|
|
title,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
options: string[];
|
|
descriptions: Record<string, string>;
|
|
onSelect: (v: string) => void;
|
|
title?: string;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const anchorRef = useRef<HTMLDivElement>(null);
|
|
const dropdownOptions: DropdownOption[] = options.map((name) => ({
|
|
value: name,
|
|
label: name,
|
|
description: descriptions[name] || undefined,
|
|
}));
|
|
return (
|
|
<div class="status-chip-anchor" ref={anchorRef}>
|
|
<Badge
|
|
label={label}
|
|
value={value}
|
|
onClick={() => setOpen((o) => !o)}
|
|
expanded={open}
|
|
title={title}
|
|
/>
|
|
<Dropdown
|
|
open={open}
|
|
options={dropdownOptions}
|
|
activeValue={value}
|
|
label={`select ${label}`}
|
|
onSelect={(v) => {
|
|
onSelect(v);
|
|
setOpen(false);
|
|
}}
|
|
onClose={() => setOpen(false)}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// The consolidated alive/thinking/paused badge (mara: "why do we
|
|
// have separate alive badge? ... it could be one badge that shows
|
|
// thinking / idle / paused / ... with a dropdown that opens when
|
|
// clicked to pause/unpause or cancel turn"). Cancel-turn is a
|
|
// click-again-to-confirm row rather than a native confirm dialog (mara:
|
|
// "make the menu entry a 'click again to confirm'") — armed state resets
|
|
// whenever the dropdown closes, so a stray reopen never fires it early.
|
|
function StatusMenu({
|
|
statusLabel,
|
|
statusTone,
|
|
statusTooltip,
|
|
paused,
|
|
onTogglePause,
|
|
thinking,
|
|
onCancelTurn,
|
|
}: Pick<
|
|
StatusChipsProps,
|
|
| "statusLabel"
|
|
| "statusTone"
|
|
| "statusTooltip"
|
|
| "paused"
|
|
| "onTogglePause"
|
|
| "thinking"
|
|
| "onCancelTurn"
|
|
>) {
|
|
const [open, setOpen] = useState(false);
|
|
const [confirmCancel, setConfirmCancel] = useState(false);
|
|
const anchorRef = useRef<HTMLDivElement>(null);
|
|
|
|
function close() {
|
|
setOpen(false);
|
|
setConfirmCancel(false);
|
|
}
|
|
|
|
const options: DropdownOption[] = [
|
|
{ value: "toggle-pause", label: paused ? "▶ resume" : "⏸ pause" },
|
|
];
|
|
if (thinking) {
|
|
options.push({
|
|
value: "cancel-turn",
|
|
label: confirmCancel ? "■ click again to confirm" : "■ cancel turn",
|
|
danger: true,
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div class="status-chip-anchor" ref={anchorRef}>
|
|
<Badge
|
|
value={statusLabel}
|
|
tone={statusTone}
|
|
title={statusTooltip}
|
|
onClick={() => setOpen((o) => !o)}
|
|
expanded={open}
|
|
/>
|
|
<Dropdown
|
|
open={open}
|
|
options={options}
|
|
label="agent status"
|
|
onSelect={(value) => {
|
|
if (value === "toggle-pause") {
|
|
onTogglePause();
|
|
close();
|
|
return;
|
|
}
|
|
// value === 'cancel-turn': first click arms it, second fires.
|
|
if (!confirmCancel) {
|
|
setConfirmCancel(true);
|
|
return;
|
|
}
|
|
onCancelTurn();
|
|
close();
|
|
}}
|
|
onClose={close}
|
|
anchorRef={anchorRef}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StatusChips({
|
|
statusLabel,
|
|
statusTone,
|
|
statusTooltip,
|
|
model,
|
|
resolvedModel,
|
|
availableModels,
|
|
onSelectModel,
|
|
effort,
|
|
availableEfforts,
|
|
onSelectEffort,
|
|
ctxLabel,
|
|
costLabel,
|
|
paused,
|
|
onTogglePause,
|
|
lastTurnLabel,
|
|
thinking,
|
|
onCancelTurn,
|
|
}: StatusChipsProps) {
|
|
return (
|
|
<div class="status-chips">
|
|
<StatusMenu
|
|
statusLabel={statusLabel}
|
|
statusTone={statusTone}
|
|
statusTooltip={statusTooltip}
|
|
paused={paused}
|
|
onTogglePause={onTogglePause}
|
|
thinking={thinking}
|
|
onCancelTurn={onCancelTurn}
|
|
/>
|
|
<Picker
|
|
label="model"
|
|
value={model}
|
|
options={availableModels}
|
|
descriptions={MODEL_DESCRIPTIONS}
|
|
onSelect={onSelectModel}
|
|
title={resolvedModel ? `resolved to ${resolvedModel}` : undefined}
|
|
/>
|
|
{availableEfforts.length ? (
|
|
<Picker
|
|
label="effort"
|
|
value={effort}
|
|
options={availableEfforts}
|
|
descriptions={EFFORT_DESCRIPTIONS}
|
|
onSelect={onSelectEffort}
|
|
/>
|
|
) : null}
|
|
{ctxLabel ? (
|
|
<Badge
|
|
label="ctx"
|
|
value={ctxLabel}
|
|
title="tokens used in the current context window"
|
|
/>
|
|
) : null}
|
|
{costLabel ? (
|
|
<Badge
|
|
label="cost"
|
|
value={costLabel}
|
|
title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"
|
|
/>
|
|
) : null}
|
|
{lastTurnLabel ? (
|
|
<span class="status-chips-last-turn">{lastTurnLabel}</span>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|