mara on #5034: "effort shares /models fate". The per-agent web UI no longer sets effort: the `/effort` slash command, the effort picker, `POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer, `EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are gone, and `lib/modelEffort.ts` with them. The effort chip is display-only and shows its source (`effort_source` in `/api/state`); on ACP it stays hidden while the session offers no effort level. The read side is unchanged: with no swarm value an agent runs an existing `hyperhive-effort` file, else its agent.nix effort, else medium. Existing files are not touched. Refs #5034
250 lines
7.6 KiB
TypeScript
250 lines
7.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 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<void>;
|
|
/** 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<void>;
|
|
/** 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<void>;
|
|
}
|
|
|
|
const SOURCE_LABEL: Record<InferenceSource, string> = {
|
|
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<HTMLDivElement>(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 (
|
|
<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;
|
|
}
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StatusChips({
|
|
statusLabel,
|
|
statusTone,
|
|
statusTooltip,
|
|
model,
|
|
resolvedModel,
|
|
modelSource,
|
|
effort,
|
|
effortSource,
|
|
ctxLabel,
|
|
costLabel,
|
|
paused,
|
|
onTogglePause,
|
|
lastTurnLabel,
|
|
thinking,
|
|
onCancelTurn,
|
|
onNewSession,
|
|
onLogout,
|
|
}: StatusChipsProps) {
|
|
return (
|
|
<div class="status-chips">
|
|
<StatusMenu
|
|
statusLabel={statusLabel}
|
|
statusTone={statusTone}
|
|
statusTooltip={statusTooltip}
|
|
paused={paused}
|
|
onTogglePause={onTogglePause}
|
|
thinking={thinking}
|
|
onCancelTurn={onCancelTurn}
|
|
onNewSession={onNewSession}
|
|
onLogout={onLogout}
|
|
/>
|
|
<Badge
|
|
label="model"
|
|
value={model}
|
|
title={
|
|
[
|
|
modelSource ? SOURCE_LABEL[modelSource] : null,
|
|
resolvedModel ? `resolved to ${resolvedModel}` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ") || undefined
|
|
}
|
|
/>
|
|
{effort ? (
|
|
<Badge
|
|
label="effort"
|
|
value={effort}
|
|
title={effortSource ? SOURCE_LABEL[effortSource] : undefined}
|
|
/>
|
|
) : 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>
|
|
);
|
|
}
|