swarm-ui: the cancel badge clears when the next turn starts
This commit is contained in:
parent
2f2b4aa9e2
commit
8060e91939
3 changed files with 65 additions and 6 deletions
|
|
@ -1,12 +1,19 @@
|
||||||
// <CancelTurnBadge> — "cancel turn" among the state badges over the terminal.
|
// <CancelTurnBadge> — "cancel turn" among the state badges over the terminal.
|
||||||
// Shown only while the agent is in a turn (`canCancel`). POSTs
|
// Shown only while the agent is in a turn (`canCancel`). POSTs
|
||||||
// `/api/agents/{name}/cancel`; the answer says whether a turn was running, and
|
// `/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
|
// the badge shows that (or the failure) in place of its label, so a cancel is
|
||||||
// click, so a cancel is never silently a no-op.
|
// never silently a no-op. The answer clears when the next turn starts
|
||||||
import { useState } from "preact/hooks";
|
// (`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 { Badge } from "@hive/shared/badge.js";
|
||||||
import { readApiError } from "@hive/shared/api-error.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 =
|
type CancelState =
|
||||||
| { status: "idle" }
|
| { status: "idle" }
|
||||||
|
|
@ -22,6 +29,13 @@ export function CancelTurnBadge({
|
||||||
turnState: string;
|
turnState: string;
|
||||||
}) {
|
}) {
|
||||||
const [state, setState] = useState<CancelState>({ status: "idle" });
|
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() {
|
async function cancel() {
|
||||||
setState({ status: "cancelling" });
|
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") {
|
if (state.status === "done") {
|
||||||
return <Badge variant="quiet" value={cancelNote(state.acted)} />;
|
return <Badge variant="quiet" value={cancelNote(state.acted)} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,26 @@ export function cancelPath(agentName: string): string {
|
||||||
export function cancelNote(acted: boolean): string {
|
export function cancelNote(acted: boolean): string {
|
||||||
return acted ? "turn cancelled" : "nothing was running";
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
canCancel,
|
canCancel,
|
||||||
cancelNote,
|
cancelNote,
|
||||||
cancelPath,
|
cancelPath,
|
||||||
|
statusOnTurnChange,
|
||||||
} from "../src/agent/terminal/cancel.ts";
|
} from "../src/agent/terminal/cancel.ts";
|
||||||
|
|
||||||
test("cancel is offered while the agent is in a turn, and only then", () => {
|
test("cancel is offered while the agent is in a turn, and only then", () => {
|
||||||
|
|
@ -23,3 +24,26 @@ test("the note says whether a turn was stopped", () => {
|
||||||
assert.equal(cancelNote(true), "turn cancelled");
|
assert.equal(cancelNote(true), "turn cancelled");
|
||||||
assert.equal(cancelNote(false), "nothing was running");
|
assert.equal(cancelNote(false), "nothing was running");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the answer stays after its turn ends and clears when the next starts", () => {
|
||||||
|
assert.equal(statusOnTurnChange("done", "thinking", "idle"), "done");
|
||||||
|
assert.equal(statusOnTurnChange("done", "idle", "idle"), "done");
|
||||||
|
assert.equal(statusOnTurnChange("done", "idle", "thinking"), "idle");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a failure clears when its turn ends, or a new one starts", () => {
|
||||||
|
assert.equal(statusOnTurnChange("error", "thinking", "thinking"), "error");
|
||||||
|
assert.equal(statusOnTurnChange("error", "thinking", "idle"), "idle");
|
||||||
|
assert.equal(statusOnTurnChange("error", "idle", "thinking"), "idle");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a cancel in flight is left alone", () => {
|
||||||
|
assert.equal(
|
||||||
|
statusOnTurnChange("cancelling", "thinking", "idle"),
|
||||||
|
"cancelling",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
statusOnTurnChange("cancelling", "idle", "thinking"),
|
||||||
|
"cancelling",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue