Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the cancel badge clears when the next turn starts

This commit is contained in:
iris 2026-10-28 20:16:11 +01:00
commit 8060e91939
2 changed files with 65 additions and 6 deletions

View file

@ -1,12 +1,19 @@
// <CancelTurnBadge> — "cancel turn" among the state badges over the terminal.
// Shown only while the agent is in a turn (`canCancel`). POSTs
// `/api/agents/{name}/cancel`; the answer says whether a turn was running, and
// the badge shows that (or the failure) in place of its label until the next
// click, so a cancel is never silently a no-op.
import { useState } from "preact/hooks";
// the badge shows that (or the failure) in place of its label, so a cancel is
// never silently a no-op. The answer clears when the next turn starts
// (`statusOnTurnChange`), so every turn can be cancelled; a failure can also be
// retried by clicking it.
import { useEffect, useRef, useState } from "preact/hooks";
import { Badge } from "@hive/shared/badge.js";
import { readApiError } from "@hive/shared/api-error.js";
import { canCancel, cancelNote, cancelPath } from "./cancel.js";
import {
canCancel,
cancelNote,
cancelPath,
statusOnTurnChange,
} from "./cancel.js";
type CancelState =
| { status: "idle" }
@ -22,6 +29,13 @@ export function CancelTurnBadge({
turnState: string;
}) {
const [state, setState] = useState<CancelState>({ status: "idle" });
const prevTurn = useRef<string | undefined>(undefined);
useEffect(() => {
const next = statusOnTurnChange(state.status, prevTurn.current, turnState);
prevTurn.current = turnState;
if (next !== state.status) setState({ status: next } as CancelState);
}, [turnState]);
async function cancel() {
setState({ status: "cancelling" });
@ -42,8 +56,6 @@ export function CancelTurnBadge({
}
}
// The answer outlives the turn it stopped: once the agent leaves
// `thinking`, the note is the only thing worth keeping on screen.
if (state.status === "done") {
return <Badge variant="quiet" value={cancelNote(state.acted)} />;
}

View file

@ -16,3 +16,26 @@ export function cancelPath(agentName: string): string {
export function cancelNote(acted: boolean): string {
return acted ? "turn cancelled" : "nothing was running";
}
/** What the cancel badge is showing. */
export type CancelStatus = "idle" | "cancelling" | "done" | "error";
/**
* The badge's status once the agent's turn state moves from `prev` to `now`.
* The answer ("turn cancelled") stays up after the turn it stopped ends, and
* clears when the next turn starts, so that turn can be cancelled too. A
* failure clears as soon as its turn ends: with no turn running, there is
* nothing left to retry.
*/
export function statusOnTurnChange(
status: CancelStatus,
prev: string | undefined,
now: string | undefined,
): CancelStatus {
if (status === "cancelling") return status;
const wasIn = canCancel(prev);
const isIn = canCancel(now);
if (isIn && !wasIn) return "idle";
if (!isIn && wasIn && status === "error") return "idle";
return status;
}