From d044281040f2c238e3ebaa6fda0baffefece9e37 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 16 Aug 2026 23:44:16 +0200 Subject: [PATCH] swarm-ui: minimal agent creation page Adds /agents: a name field that POSTs to swarm-controller's POST /api/agents (from the CreateIdentity work), shows the queued job node id, and links to /jobs to watch it settle. Scope matches the issue exactly - no forge/deploy options, those aren't wired server-side yet. --- frontend/packages/swarm-ui/src/App.tsx | 2 + .../swarm-ui/src/pages/AgentsPage.css | 54 +++++++++++ .../swarm-ui/src/pages/AgentsPage.tsx | 94 +++++++++++++++++++ .../packages/swarm-ui/src/shell/Shell.tsx | 1 + 4 files changed, 151 insertions(+) create mode 100644 frontend/packages/swarm-ui/src/pages/AgentsPage.css create mode 100644 frontend/packages/swarm-ui/src/pages/AgentsPage.tsx diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx index 3ef432e3..2a33b3d7 100644 --- a/frontend/packages/swarm-ui/src/App.tsx +++ b/frontend/packages/swarm-ui/src/App.tsx @@ -2,6 +2,7 @@ // page component under `./pages/`; this file just maps paths to them. import { Route, Switch } from 'wouter-preact'; import { Shell } from './shell/Shell.js'; +import { AgentsPage } from './pages/AgentsPage.js'; import { ComponentsPage } from './pages/ComponentsPage.js'; import { JobsPage } from './pages/JobsPage.js'; import { OverviewPage } from './pages/OverviewPage.js'; @@ -20,6 +21,7 @@ export function App() { + diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.css b/frontend/packages/swarm-ui/src/pages/AgentsPage.css new file mode 100644 index 00000000..cbb36827 --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.css @@ -0,0 +1,54 @@ +/* — the create-agent form. First form in this package, so + its input/button chrome lives here rather than in `ui/` — see the + component's own comment for why. Reuses the same base16 slots + (../theme.css) every other component draws from. */ +.agents-intro { + margin: 0 0 1.5em; + color: var(--muted); +} +.agents-form { + display: flex; + align-items: flex-end; + gap: 0.75em; + flex-wrap: wrap; +} +.agents-field { + display: flex; + flex-direction: column; + gap: 0.3em; +} +.agents-label { + font-size: 0.85em; + color: var(--muted); +} +.agents-input { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--purple-dim); + border-radius: 0.3em; + padding: 0.4em 0.6em; + font: inherit; + min-width: 16em; +} +.agents-submit { + background: var(--purple-dim); + color: var(--fg); + border: none; + border-radius: 0.3em; + padding: 0.5em 1.2em; + font: inherit; + cursor: pointer; +} +.agents-submit:disabled { + opacity: 0.6; + cursor: default; +} +.agents-result { + margin-top: 1em; +} +.agents-result-ok { + color: var(--green); +} +.agents-result-error { + color: var(--red); +} diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx new file mode 100644 index 00000000..c391025c --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -0,0 +1,94 @@ +// — 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 page gets a response — so this page 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). +// +// First real form in this package — no shared form kit exists yet, so +// the input/button styling below is scoped to `AgentsPage.css` rather +// than promoted into `ui/`. Promote the day a second page needs one, +// same "don't build ahead of a second caller" rule `Panel`'s own +// comment states. +import { useState } from 'preact/hooks'; +import { Link } from 'wouter-preact'; +import { Panel } from '../ui/panel/Panel.js'; +import './AgentsPage.css'; + +interface CreateAgentResponse { + node_id: number; +} + +type SubmitState = + | { status: 'idle' } + | { status: 'submitting' } + | { status: 'done'; nodeId: number } + | { status: 'error'; message: string }; + +// 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. +const NAME_PATTERN = '[a-z0-9-]{1,63}'; + +export function AgentsPage() { + const [name, setName] = useState(''); + const [result, setResult] = useState({ status: 'idle' }); + + 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 }), + }); + if (!r.ok) throw new Error((await r.text()) || `http ${r.status}`); + const data = (await r.json()) as CreateAgentResponse; + setResult({ status: 'done', nodeId: data.node_id }); + setName(''); + } catch (err) { + setResult({ status: 'error', message: String(err) }); + } + } + + return ( + +

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

+
+
+ + setName(e.currentTarget.value)} + /> +
+ +
+ {result.status === 'done' && ( +

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

+ )} + {result.status === 'error' && ( +

failed to queue: {result.message}

+ )} +
+ ); +} diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index 7ba5e5a1..9136b04b 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -19,6 +19,7 @@ import './Shell.css'; const NAV_ITEMS: { href: string; label: string }[] = [ { href: '/', label: 'overview' }, + { href: '/agents', label: 'agents' }, { href: '/jobs', label: 'jobs' }, { href: '/components', label: 'components' }, ];