// — the agent page's status row: the consolidated status // badge (alive/thinking/paused/etc., with pause/resume + cancel-turn in // its dropdown), the model and effort (display-only: the swarm sets // them), 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 type { InferenceSource } from "../types.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; /** Where `model` comes from (`/api/state` `model_source`). */ modelSource?: InferenceSource; /** Hidden when empty: an ACP session that offers no effort now. */ effort?: string | null; effortSource?: InferenceSource; ctxLabel?: string; costLabel?: string; paused: boolean; onTogglePause: () => void; lastTurnLabel?: string; thinking: boolean; onCancelTurn: () => Promise; /** Drops all prior `--continue` context and starts fresh. Always * offered, same as logout — the old app.js overflow menu had it as * an unconditional click target too, this is just the click path * catching up to what `/new-session` already does. See * `termActions.ts`'s `postNewSession`. */ onNewSession: () => Promise; /** Rotates OAuth creds + parks the agent in needs-login. Always * offered (unlike cancel-turn, not gated on `thinking`) — the old * page's overflow menu had it unconditionally too. See * `termActions.ts`'s `postLogout` for what it actually does. */ onLogout: () => Promise; } const SOURCE_LABEL: Record = { swarm: "set on the swarm", agent_nix: "from agent.nix", local_file: "from its own earlier pick", default: "the built-in default", }; // 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, onNewSession, onLogout, }: Pick< StatusChipsProps, | "statusLabel" | "statusTone" | "statusTooltip" | "paused" | "onTogglePause" | "thinking" | "onCancelTurn" | "onNewSession" | "onLogout" >) { const [open, setOpen] = useState(false); const [confirmCancel, setConfirmCancel] = useState(false); const [confirmNewSession, setConfirmNewSession] = useState(false); const [confirmLogout, setConfirmLogout] = useState(false); const anchorRef = useRef(null); function close() { setOpen(false); setConfirmCancel(false); setConfirmNewSession(false); setConfirmLogout(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, }); } // Always offered, regardless of turn state — same as the old overflow // menu's unconditional ↻ new-session and 🔓 logout entries. options.push({ value: "new-session", label: confirmNewSession ? "↻ click again to confirm" : "↻ new session", danger: true, }); options.push({ value: "logout", label: confirmLogout ? "🔓 click again to confirm" : "🔓 logout", danger: true, }); return (
setOpen((o) => !o)} expanded={open} /> { if (value === "toggle-pause") { onTogglePause(); close(); return; } if (value === "cancel-turn") { // First click arms it, second fires. if (!confirmCancel) { setConfirmCancel(true); return; } onCancelTurn(); close(); return; } if (value === "new-session") { // Same arm-then-fire shape as cancel-turn/logout. if (!confirmNewSession) { setConfirmNewSession(true); return; } onNewSession(); close(); return; } // value === 'logout': same arm-then-fire shape as cancel-turn. if (!confirmLogout) { setConfirmLogout(true); return; } onLogout(); close(); }} onClose={close} anchorRef={anchorRef} />
); } export function StatusChips({ statusLabel, statusTone, statusTooltip, model, resolvedModel, modelSource, effort, effortSource, ctxLabel, costLabel, paused, onTogglePause, lastTurnLabel, thinking, onCancelTurn, onNewSession, onLogout, }: StatusChipsProps) { return (
{effort ? ( ) : null} {ctxLabel ? ( ) : null} {costLabel ? ( ) : null} {lastTurnLabel ? ( {lastTurnLabel} ) : null}
); }