hyperhive/frontend/packages/agent/src/components/StatusChips.tsx
atlas 39b95c2ede treefmt: apply prettier
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.
2026-09-02 15:25:07 +02:00

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>
);
}