// — 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() { 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. The 🪪 glyph that used to sit here as body copy now lives on `Panel`'s own `icon` prop instead — it's what prompted that prop to exist at all (see Panel.tsx's doc comment) */}

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.

); }