agent page: consolidate alive/thinking/paused into one status badge
mara, #3757: fold the separate alive badge into the turn-state badge, with pause/resume + cancel-turn moved into its dropdown. Cancel-turn is click-again-to-confirm, not a modal. model/effort/ctx/cost stay as separate badges — not asked to fold those in too.
This commit is contained in:
parent
d6ca52535f
commit
1e4399d49e
5 changed files with 135 additions and 71 deletions
|
|
@ -1,9 +1,10 @@
|
|||
// <StatusChips> — the agent page's status row: alive/turn-state, the
|
||||
// model + effort pickers, context/cost usage, and pause/resume. 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
|
||||
// <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
|
||||
|
|
@ -15,11 +16,9 @@ import { Dropdown, type DropdownOption } from '@hive/shared/dropdown.js';
|
|||
import './StatusChips.css';
|
||||
|
||||
export interface StatusChipsProps {
|
||||
aliveLabel: string;
|
||||
aliveTone: BadgeTone;
|
||||
stateLabel: string;
|
||||
stateTone: BadgeTone;
|
||||
stateTooltip?: string;
|
||||
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
|
||||
|
|
@ -92,12 +91,79 @@ function Picker({
|
|||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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">
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatusChips({
|
||||
aliveLabel,
|
||||
aliveTone,
|
||||
stateLabel,
|
||||
stateTone,
|
||||
stateTooltip,
|
||||
statusLabel,
|
||||
statusTone,
|
||||
statusTooltip,
|
||||
model,
|
||||
resolvedModel,
|
||||
availableModels,
|
||||
|
|
@ -113,18 +179,17 @@ export function StatusChips({
|
|||
thinking,
|
||||
onCancelTurn,
|
||||
}: StatusChipsProps) {
|
||||
// `.finally()` on the caller's promise (not a `thinking`-flip effect):
|
||||
// re-enables the button once the request actually settles, success or
|
||||
// failure alike — ported from app.js's original
|
||||
// `postCancelTurn().finally(() => { btn.disabled = false; })`. A
|
||||
// `thinking`-only reset would leave the button stuck disabled on a
|
||||
// failed request (argus, PR review): the turn is still in flight, so
|
||||
// `thinking` never flips to trigger a reset.
|
||||
const [cancelBusy, setCancelBusy] = useState(false);
|
||||
return (
|
||||
<div class="status-chips">
|
||||
<Badge value={aliveLabel} tone={aliveTone} />
|
||||
<Badge value={stateLabel} tone={stateTone} title={stateTooltip} />
|
||||
<StatusMenu
|
||||
statusLabel={statusLabel}
|
||||
statusTone={statusTone}
|
||||
statusTooltip={statusTooltip}
|
||||
paused={paused}
|
||||
onTogglePause={onTogglePause}
|
||||
thinking={thinking}
|
||||
onCancelTurn={onCancelTurn}
|
||||
/>
|
||||
<Picker
|
||||
label="model"
|
||||
value={model}
|
||||
|
|
@ -156,25 +221,7 @@ export function StatusChips({
|
|||
title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"
|
||||
/>
|
||||
) : null}
|
||||
<Badge
|
||||
value={paused ? '▶ resume' : '⏸ pause'}
|
||||
tone={paused ? 'warning' : 'neutral'}
|
||||
onClick={onTogglePause}
|
||||
/>
|
||||
{lastTurnLabel ? <span class="status-chips-last-turn">{lastTurnLabel}</span> : null}
|
||||
{thinking ? (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-cancel-turn"
|
||||
disabled={cancelBusy}
|
||||
onClick={() => {
|
||||
setCancelBusy(true);
|
||||
onCancelTurn().finally(() => setCancelBusy(false));
|
||||
}}
|
||||
>
|
||||
■ cancel turn
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue