Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
Author SHA1 Message Date
iris
49c3b86dfd 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; }).
2026-08-30 04:05:26 +02:00
iris
b753ec2093 agent ui: restore the cancel-turn button dropped in the Preact rewrite
Root-caused mara's 'no interrupt button on agent term anymore' report:
the pre-rewrite app.js had a visible <button id=cancel-btn> (class
btn-cancel-turn, already-styled CSS still in agent.css but orphaned)
shown only while a turn was in flight, wired to /api/cancel. The
Preact rewrite (agent terminal Preact rewrite) ported the /cancel
slash command but never re-added the visible button, so interrupting
a turn now requires typing a command instead of clicking.

Adds thinking/onCancelTurn props to StatusChips, rendering the same
btn-cancel-turn markup only while turn_state === thinking, wired to
the existing postCancelTurn() (termActions.ts) + refresh(). Verified
with real headless-chromium screenshots against a mock /api/state:
button renders while thinking, is absent while idle.
2026-08-30 04:05:26 +02:00
2 changed files with 35 additions and 0 deletions

View file

@ -25,6 +25,7 @@ import { fmtAge, fmtTokens } from './lib/format.js';
import { resolveDashboardBase } from './lib/dashboardBase.js';
import { submitPauseResume } from './lib/pauseAction.js';
import { postModel, postEffort } from './lib/modelEffort.js';
import { postCancelTurn } from './lib/termActions.js';
import type { TokenUsage } from './types.js';
type OpenPanel = 'inbox' | 'todos' | null;
@ -153,6 +154,8 @@ export function Root() {
onSelectEffort={() => {}}
paused={false}
onTogglePause={() => {}}
thinking={false}
onCancelTurn={() => Promise.resolve()}
/>
</Header>
<main className="agent-main">
@ -206,6 +209,13 @@ export function Root() {
() => refresh(),
);
}}
thinking={effectiveTurnState === 'thinking'}
onCancelTurn={() =>
postCancelTurn().then((r) => {
if (!r.ok) liveStreamRef.current?.pushNote(`✗ /cancel failed${r.detail ? ': ' + r.detail : ''}`);
refresh();
})
}
/>
</Header>
<main className="agent-main">

View file

@ -31,6 +31,8 @@ export interface StatusChipsProps {
paused: boolean;
onTogglePause: () => void;
lastTurnLabel?: string;
thinking: boolean;
onCancelTurn: () => Promise<void>;
}
const MODEL_DESCRIPTIONS: Record<string, string> = {
@ -100,7 +102,17 @@ export function StatusChips({
paused,
onTogglePause,
lastTurnLabel,
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} />
@ -141,6 +153,19 @@ export function StatusChips({
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>
);
}