Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/agent/src/components/StatusChips.tsx
atlas ed271e655f agents: remove the hive-level /effort control too
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
2026-10-11 01:28:44 +02:00

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