// — minimal agent creation form, POSTs to // swarm-controller's `POST /api/agents`. That endpoint only queues a // `CreateIdentity` job and returns its node id — the identity isn't // guaranteed to exist yet when this gets a response — so this shows // the queued confirmation and links to the Jobs page to watch it // settle, rather than duplicating JobqGraph's per-node polling here. // Scope matches the originating issue exactly: name field only, no // forge/deploy options (those aren't wired server-side yet either). // // Rendered inside a `Dialog` from `AgentsPage`'s own "+ agent" button, // not a standalone route — the roster it feeds is the natural home for // the action that populates it, and a distinct top-level nav item next // to it was one click of indirection for no benefit. Kept as its own // component (not inlined into AgentsPage) since the two-panel layout // below is unchanged from its former life as a page: same content, same // review history, just a different mount point. // // `hive` field added because agent creation had no way to record which // hive an agent runs on. `POST /api/agents` now requires it, so the // roster fetched off `GET /api/hives` backs a required select here // rather than a free-text field — a spawn target has to be one of the // swarm's actual hives, same reasoning `Ident.parse` gets client-side // pattern validation for `name`. // // Its name field, hive select, and submit button all come from the // shared `ui/` form kit (`TextField`/`SelectField`/`Button`) rather than // page-scoped input/button chrome — see that kit's own comments for why. import { useEffect, useState } from "preact/hooks"; import { Link } from "wouter-preact"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Panel } from "../ui/panel/Panel.js"; import { TextField } from "../ui/text-field/TextField.js"; import { SelectField, type SelectOption, } from "../ui/select-field/SelectField.js"; import { Button } from "../ui/button/Button.js"; import "./CreateAgentForm.css"; // Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage` // consumes off `/api/hives/status`, but this page hits the plain // `/api/hives` roster (no status/freshness needed, just "what hives // exist to spawn into"). interface HiveEntry { name: string; domain: string; } interface CreateAgentResponse { node_id: number; } type SubmitState = | { status: "idle" } | { status: "submitting" } | { status: "done"; nodeId: number } | { status: "error"; problem: ProblemDetails }; // Client-side mirror of `hive_types::Ident` (1-63 chars of `[a-z0-9-]`) — // a `pattern` hint only, not a substitute for the server's own // validation; a name this rejects would fail server-side anyway, so // catching it before a round-trip is a pure UX win, not a new gate. // // Hyphen escaped (`\-`) rather than left trailing in the class: modern // browsers validate the `pattern` attribute's regex in Unicode-set (`v`) // mode, which is far stricter about hyphen placement than classic mode — // `[a-z0-9-]` throws "Invalid character class" under `v` mode even though // it's valid classic-mode regex (a trailing `-` is unambiguous there). // Reproduced directly: `new RegExp('[a-z0-9-]', 'v')` throws, // `new RegExp('[a-z0-9\\-]', 'v')` doesn't — confirmed via mara's console // exception report on this page. const NAME_PATTERN = "[a-z0-9\\-]{1,63}"; export function CreateAgentForm({ onClose, }: { /** Rendered as the form panel's own close button — this form always * mounts inside a `plain` `Dialog` (see `AgentsPage`), which no longer * floats its own. */ onClose?: () => void; }) { const [name, setName] = useState(""); const [hive, setHive] = useState(""); // `null` = still loading, `[]` = loaded but empty (a real, if unusual, // swarm state) — distinct from "not fetched yet" so the placeholder // option's label can tell the two apart. const [hives, setHives] = useState(null); const [hivesError, setHivesError] = useState(null); const [result, setResult] = useState({ status: "idle" }); // Loaded once, not re-fetched on submit: the roster changes rarely // enough (a nix-level swarm config change) that staleness within one // page visit isn't worth a second round-trip per keystroke/submit. useEffect(() => { (async () => { const r = await fetch("/api/hives"); if (!r.ok) { setHivesError(await readApiError(r)); return; } const data = (await r.json()) as HiveEntry[]; setHives(data); // Single-hive swarms are the common case — default it rather than // making the operator pick the only option. Multi-hive swarms get // no default (the placeholder stays selected), so `required` on // the select below forces an explicit choice per mara's scope. if (data.length === 1) setHive(data[0].name); })().catch((e: unknown) => setHivesError({ detail: String(e) })); }, []); const hiveOptions: SelectOption[] = (hives ?? []).map((h) => ({ value: h.name, label: h.name, })); const hivesReady = hives !== null && hives.length > 0; async function submit(e: Event) { e.preventDefault(); setResult({ status: "submitting" }); try { const r = await fetch("/api/agents", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name, hive }), }); if (!r.ok) { setResult({ status: "error", problem: await readApiError(r) }); return; } const data = (await r.json()) as CreateAgentResponse; setResult({ status: "done", nodeId: data.node_id }); setName(""); } catch (err) { setResult({ status: "error", problem: { detail: String(err) } }); } } return (

Create a new agent's swarm-level identity. This only queues the job — check jobs to watch it settle.

{hivesError && ( )}
{result.status === "done" && (

queued as job node #{result.nodeId} —{" "} watch it in jobs

)} {result.status === "error" && ( )}
{/* Describes the intended end state (a running agent), not today's literal behaviour (deploying the container onto the hive isn't wired up server-side yet — see this file's top comment) — mara's explicit call on this copy: read as finished, not as a running commentary on partial implementation. */}

Submitting this queues everything a new agent needs: a swarm-level identity, a config repo on the forge with the operator added as a collaborator, and a container running the agent on the hive you pick above. Watch it all settle on jobs.

); }