agent ui: reset cancel-turn busy state via .finally(), not a thinking-flip effect

argus (PR review): the thinking-flip useEffect left the button stuck
disabled if the /api/cancel POST itself failed while the turn was
still genuinely thinking, since nothing re-fires the effect. Switch
onCancelTurn's signature to () => Promise<void> and reset cancelBusy
in a .finally() on that promise instead — fires on success or
failure alike, matching app.js's original
postCancelTurn().finally(() => { btn.disabled = false; }).
This commit is contained in:
iris 2026-08-30 03:46:09 +02:00 committed by mara
commit 49c3b86dfd
2 changed files with 14 additions and 14 deletions

View file

@ -155,7 +155,7 @@ export function Root() {
paused={false} paused={false}
onTogglePause={() => {}} onTogglePause={() => {}}
thinking={false} thinking={false}
onCancelTurn={() => {}} onCancelTurn={() => Promise.resolve()}
/> />
</Header> </Header>
<main className="agent-main"> <main className="agent-main">
@ -210,12 +210,12 @@ export function Root() {
); );
}} }}
thinking={effectiveTurnState === 'thinking'} thinking={effectiveTurnState === 'thinking'}
onCancelTurn={() => { onCancelTurn={() =>
postCancelTurn().then((r) => { postCancelTurn().then((r) => {
if (!r.ok) liveStreamRef.current?.pushNote(`✗ /cancel failed${r.detail ? ': ' + r.detail : ''}`); if (!r.ok) liveStreamRef.current?.pushNote(`✗ /cancel failed${r.detail ? ': ' + r.detail : ''}`);
refresh(); refresh();
}); })
}} }
/> />
</Header> </Header>
<main className="agent-main"> <main className="agent-main">

View file

@ -9,7 +9,7 @@
// selection state live in the caller (`Root.tsx`, via the // selection state live in the caller (`Root.tsx`, via the
// `useAgentState` hook), so this component can still be demoed and // `useAgentState` hook), so this component can still be demoed and
// reviewed against plain sample data independent of live `/api/state`. // reviewed against plain sample data independent of live `/api/state`.
import { useEffect, useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import { Badge, type BadgeTone } from '@hive/shared/badge.js'; import { Badge, type BadgeTone } from '@hive/shared/badge.js';
import { Dropdown, type DropdownOption } from '@hive/shared/dropdown.js'; import { Dropdown, type DropdownOption } from '@hive/shared/dropdown.js';
import './StatusChips.css'; import './StatusChips.css';
@ -32,7 +32,7 @@ export interface StatusChipsProps {
onTogglePause: () => void; onTogglePause: () => void;
lastTurnLabel?: string; lastTurnLabel?: string;
thinking: boolean; thinking: boolean;
onCancelTurn: () => void; onCancelTurn: () => Promise<void>;
} }
const MODEL_DESCRIPTIONS: Record<string, string> = { const MODEL_DESCRIPTIONS: Record<string, string> = {
@ -105,14 +105,14 @@ export function StatusChips({
thinking, thinking,
onCancelTurn, onCancelTurn,
}: StatusChipsProps) { }: 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); const [cancelBusy, setCancelBusy] = useState(false);
// The button unmounts (not just hides) once `thinking` flips false, but
// `cancelBusy` lives in this component's own state across re-renders —
// reset it here so a *later* turn doesn't inherit a stale "busy" from a
// previous cancel click.
useEffect(() => {
if (!thinking) setCancelBusy(false);
}, [thinking]);
return ( return (
<div class="status-chips"> <div class="status-chips">
<Badge value={aliveLabel} tone={aliveTone} /> <Badge value={aliveLabel} tone={aliveTone} />
@ -160,7 +160,7 @@ export function StatusChips({
disabled={cancelBusy} disabled={cancelBusy}
onClick={() => { onClick={() => {
setCancelBusy(true); setCancelBusy(true);
onCancelTurn(); onCancelTurn().finally(() => setCancelBusy(false));
}} }}
> >
cancel turn cancel turn