swarm-ui: cancel turn is a badge over the terminal, not a button by send
This commit is contained in:
parent
214d7000d3
commit
2f2b4aa9e2
4 changed files with 84 additions and 44 deletions
|
|
@ -360,9 +360,11 @@ box. **send** hands the text to the agent's harness, and the agent takes it as
|
|||
an inbox message from your authelia username. **sent** means the harness was
|
||||
listening and holds the message. If the harness isn't running, or isn't
|
||||
connected to the swarm queue, the box shows an error and the agent gets nothing.
|
||||
While the agent is in a turn, a **cancel turn** button sits beside **send**. It
|
||||
asks the agent's harness to stop the turn and says whether one was running
|
||||
("turn cancelled" or "nothing was running"). Subagents have no message box.
|
||||
While the agent is in a turn, a **cancel turn** badge appears among the state
|
||||
badges over the terminal, in its own tab. It asks the agent's harness to stop
|
||||
the turn, then shows whether one was running ("turn cancelled" or "nothing was
|
||||
running"); a failure shows "cancel failed" with the reason on hover, and a click
|
||||
tries again. Subagents have no message box and no cancel badge.
|
||||
|
||||
The message doesn't go through the agent's hive, so the hive dashboard's
|
||||
message history doesn't list it. The agent's answer shows in its terminal; an
|
||||
|
|
|
|||
|
|
@ -8,25 +8,15 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { canCancel, cancelNote, cancelPath } from "./cancel.js";
|
||||
import "./AgentInputForm.css";
|
||||
|
||||
type SubmitState =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "sent" }
|
||||
| { status: "cancelled"; acted: boolean }
|
||||
| { status: "error"; problem: ProblemDetails };
|
||||
|
||||
export function AgentInputForm({
|
||||
agentName,
|
||||
turnState,
|
||||
}: {
|
||||
agentName: string;
|
||||
// The agent's turn state when the page has it; the cancel button shows only
|
||||
// while the agent is in a turn.
|
||||
turnState?: string;
|
||||
}) {
|
||||
export function AgentInputForm({ agentName }: { agentName: string }) {
|
||||
const [body, setBody] = useState("");
|
||||
const [result, setResult] = useState<SubmitState>({ status: "idle" });
|
||||
|
||||
|
|
@ -53,21 +43,6 @@ export function AgentInputForm({
|
|||
}
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
setResult({ status: "submitting" });
|
||||
try {
|
||||
const r = await fetch(cancelPath(agentName), { method: "POST" });
|
||||
if (!r.ok) {
|
||||
setResult({ status: "error", problem: await readApiError(r) });
|
||||
return;
|
||||
}
|
||||
const { acted } = (await r.json()) as { acted: boolean };
|
||||
setResult({ status: "cancelled", acted });
|
||||
} catch (err) {
|
||||
setResult({ status: "error", problem: { detail: String(err) } });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="ui-agent-input">
|
||||
<form class="ui-agent-input-form" onSubmit={submit}>
|
||||
|
|
@ -85,20 +60,8 @@ export function AgentInputForm({
|
|||
>
|
||||
{result.status === "submitting" ? "sending…" : "send"}
|
||||
</Button>
|
||||
{canCancel(turnState) && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={cancel}
|
||||
disabled={result.status === "submitting"}
|
||||
>
|
||||
cancel turn
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
{result.status === "sent" && <p class="ui-agent-input-sent">sent</p>}
|
||||
{result.status === "cancelled" && (
|
||||
<p class="ui-agent-input-sent">{cancelNote(result.acted)}</p>
|
||||
)}
|
||||
{result.status === "error" && (
|
||||
<ApiErrorPanel context="failed to send" problem={result.problem} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
} from "./useSwarmAgentStateStream.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { AgentInputForm } from "./AgentInputForm.js";
|
||||
import { CancelTurnBadge } from "./CancelTurnBadge.js";
|
||||
import "./AgentTermPreview.css";
|
||||
|
||||
const NEAR_BOTTOM_PX = 48;
|
||||
|
|
@ -277,6 +278,12 @@ export function AgentTermPreview({
|
|||
/>
|
||||
)}
|
||||
{header && <StateBadges header={header} ctx={ctx} cost={cost} />}
|
||||
{header && subagent === undefined && (
|
||||
<CancelTurnBadge
|
||||
agentName={agentName}
|
||||
turnState={header.turn_state}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!fullHeight && (
|
||||
<Badge
|
||||
|
|
@ -310,9 +317,7 @@ export function AgentTermPreview({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{subagent === undefined && (
|
||||
<AgentInputForm agentName={agentName} turnState={header?.turn_state} />
|
||||
)}
|
||||
{subagent === undefined && <AgentInputForm agentName={agentName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
// <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";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { readApiError } from "@hive/shared/api-error.js";
|
||||
import { canCancel, cancelNote, cancelPath } from "./cancel.js";
|
||||
|
||||
type CancelState =
|
||||
| { status: "idle" }
|
||||
| { status: "cancelling" }
|
||||
| { status: "done"; acted: boolean }
|
||||
| { status: "error"; detail: string };
|
||||
|
||||
export function CancelTurnBadge({
|
||||
agentName,
|
||||
turnState,
|
||||
}: {
|
||||
agentName: string;
|
||||
turnState: string;
|
||||
}) {
|
||||
const [state, setState] = useState<CancelState>({ status: "idle" });
|
||||
|
||||
async function cancel() {
|
||||
setState({ status: "cancelling" });
|
||||
try {
|
||||
const r = await fetch(cancelPath(agentName), { method: "POST" });
|
||||
if (!r.ok) {
|
||||
const p = await readApiError(r);
|
||||
setState({
|
||||
status: "error",
|
||||
detail: p.detail ?? p.title ?? `status ${p.status ?? r.status}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { acted } = (await r.json()) as { acted: boolean };
|
||||
setState({ status: "done", acted });
|
||||
} catch (err) {
|
||||
setState({ status: "error", detail: String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
// 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)} />;
|
||||
}
|
||||
if (state.status === "error") {
|
||||
return (
|
||||
<Badge
|
||||
tone="negative"
|
||||
value="cancel failed"
|
||||
title={state.detail}
|
||||
onClick={cancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!canCancel(turnState)) return null;
|
||||
return (
|
||||
<Badge
|
||||
variant="quiet"
|
||||
value={state.status === "cancelling" ? "cancelling…" : "cancel turn"}
|
||||
title="stop the agent's current turn"
|
||||
onClick={state.status === "cancelling" ? undefined : cancel}
|
||||
disabled={state.status === "cancelling"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue