The per-agent page had these and the swarm terminal did not; they need no new backend. A cancel turn button beside send, shown while the agent is in a turn, posts to the controller's existing cancel route and says whether a turn was stopped. The terminal's badges add the effort and how long the agent has been in its turn state. The expand tool output setting moves to the shared package so both pages render the one component, and the swarm menu gets it. Docs follow.
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
// <AgentInputForm> — text for one agent, under its swarm terminal.
|
|
// POSTs `/api/agents/{name}/input`: 202 once the agent's harness holds the
|
|
// message, 503 when it is not listening, problem+json otherwise. The agent
|
|
// sees it as an inbox turn from the logged-in user (the controller takes the
|
|
// sender from the gateway, not from this request).
|
|
import { useState } from "preact/hooks";
|
|
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;
|
|
}) {
|
|
const [body, setBody] = useState("");
|
|
const [result, setResult] = useState<SubmitState>({ status: "idle" });
|
|
|
|
async function submit(e: Event) {
|
|
e.preventDefault();
|
|
setResult({ status: "submitting" });
|
|
try {
|
|
const r = await fetch(
|
|
`/api/agents/${encodeURIComponent(agentName)}/input`,
|
|
{
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ body }),
|
|
},
|
|
);
|
|
if (!r.ok) {
|
|
setResult({ status: "error", problem: await readApiError(r) });
|
|
return;
|
|
}
|
|
setResult({ status: "sent" });
|
|
setBody("");
|
|
} catch (err) {
|
|
setResult({ status: "error", problem: { detail: String(err) } });
|
|
}
|
|
}
|
|
|
|
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}>
|
|
<TextField
|
|
id={`agent-input-${agentName}`}
|
|
label={`message ${agentName}`}
|
|
value={body}
|
|
required
|
|
onInput={setBody}
|
|
/>
|
|
<Button
|
|
variant="primary"
|
|
type="submit"
|
|
disabled={result.status === "submitting"}
|
|
>
|
|
{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} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|