swarm-ui: move create-agent page off /agents, rename to CreateAgentPage

mara: the route should reflect creating an agent, and stay separate
from a future agent list page. Renamed AgentsPage -> CreateAgentPage
(file, component, css classes) and moved the route from /agents to
/create-agent - flat, not /agents/new, since index.html's relative
asset links only resolve correctly one path segment deep (filed
separately as a real bug, not fixed here). Leaves the bare /agents
path free for a future roster page.
This commit is contained in:
iris 2026-08-16 23:51:45 +02:00 committed by mara
commit 47e89c1c93
4 changed files with 48 additions and 37 deletions

View file

@ -2,8 +2,8 @@
// page component under `./pages/`; this file just maps paths to them. // page component under `./pages/`; this file just maps paths to them.
import { Route, Switch } from 'wouter-preact'; import { Route, Switch } from 'wouter-preact';
import { Shell } from './shell/Shell.js'; import { Shell } from './shell/Shell.js';
import { AgentsPage } from './pages/AgentsPage.js';
import { ComponentsPage } from './pages/ComponentsPage.js'; import { ComponentsPage } from './pages/ComponentsPage.js';
import { CreateAgentPage } from './pages/CreateAgentPage.js';
import { JobsPage } from './pages/JobsPage.js'; import { JobsPage } from './pages/JobsPage.js';
import { OverviewPage } from './pages/OverviewPage.js'; import { OverviewPage } from './pages/OverviewPage.js';
import { Panel } from './ui/panel/Panel.js'; import { Panel } from './ui/panel/Panel.js';
@ -21,7 +21,7 @@ export function App() {
<Shell> <Shell>
<Switch> <Switch>
<Route path="/" component={OverviewPage} /> <Route path="/" component={OverviewPage} />
<Route path="/agents" component={AgentsPage} /> <Route path="/create-agent" component={CreateAgentPage} />
<Route path="/jobs" component={JobsPage} /> <Route path="/jobs" component={JobsPage} />
<Route path="/components" component={ComponentsPage} /> <Route path="/components" component={ComponentsPage} />
<Route component={NotFound} /> <Route component={NotFound} />

View file

@ -1,27 +1,27 @@
/* <AgentsPage> the create-agent form. First form in this package, so /* <CreateAgentPage> the create-agent form. First form in this package,
its input/button chrome lives here rather than in `ui/` see the so its input/button chrome lives here rather than in `ui/` see the
component's own comment for why. Reuses the same base16 slots component's own comment for why. Reuses the same base16 slots
(../theme.css) every other component draws from. */ (../theme.css) every other component draws from. */
.agents-intro { .create-agent-intro {
margin: 0 0 1.5em; margin: 0 0 1.5em;
color: var(--muted); color: var(--muted);
} }
.agents-form { .create-agent-form {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 0.75em; gap: 0.75em;
flex-wrap: wrap; flex-wrap: wrap;
} }
.agents-field { .create-agent-field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.3em; gap: 0.3em;
} }
.agents-label { .create-agent-label {
font-size: 0.85em; font-size: 0.85em;
color: var(--muted); color: var(--muted);
} }
.agents-input { .create-agent-input {
background: var(--bg); background: var(--bg);
color: var(--fg); color: var(--fg);
border: 1px solid var(--purple-dim); border: 1px solid var(--purple-dim);
@ -30,7 +30,7 @@
font: inherit; font: inherit;
min-width: 16em; min-width: 16em;
} }
.agents-submit { .create-agent-submit {
background: var(--purple-dim); background: var(--purple-dim);
color: var(--fg); color: var(--fg);
border: none; border: none;
@ -39,16 +39,16 @@
font: inherit; font: inherit;
cursor: pointer; cursor: pointer;
} }
.agents-submit:disabled { .create-agent-submit:disabled {
opacity: 0.6; opacity: 0.6;
cursor: default; cursor: default;
} }
.agents-result { .create-agent-result {
margin-top: 1em; margin-top: 1em;
} }
.agents-result-ok { .create-agent-result-ok {
color: var(--green); color: var(--green);
} }
.agents-result-error { .create-agent-result-error {
color: var(--red); color: var(--red);
} }

View file

@ -1,21 +1,30 @@
// <AgentsPage> — minimal agent creation form, POSTs to swarm-controller's // <CreateAgentPage> — minimal agent creation form, POSTs to
// `POST /api/agents`. That endpoint only queues a `CreateIdentity` job // swarm-controller's `POST /api/agents`. That endpoint only queues a
// and returns its node id — the identity isn't guaranteed to exist yet // `CreateIdentity` job and returns its node id — the identity isn't
// when this page gets a response — so this page shows the queued // guaranteed to exist yet when this page gets a response — so this
// confirmation and links to the Jobs page to watch it settle, rather // page shows the queued confirmation and links to the Jobs page to
// than duplicating JobqGraph's per-node polling here. Scope matches the // watch it settle, rather than duplicating JobqGraph's per-node
// originating issue exactly: name field only, no forge/deploy options // polling here. Scope matches the originating issue exactly: name
// (those aren't wired server-side yet either). // field only, no forge/deploy options (those aren't wired server-side
// yet either).
//
// Lives at `/create-agent`, not `/agents` — a future agent *roster*
// (listing existing agents) is the natural owner of the bare `/agents`
// path, and this creation form is a distinct action from that list,
// not a variant of it. Flat, not `/agents/new`: `index.html`'s asset
// links are relative (`static/main.js`), which only resolve correctly
// one path segment deep — a real bug, filed separately rather than
// fixed here, but reason enough to avoid a nested route today.
// //
// First real form in this package — no shared form kit exists yet, so // First real form in this package — no shared form kit exists yet, so
// the input/button styling below is scoped to `AgentsPage.css` rather // the input/button styling below is scoped to `CreateAgentPage.css`
// than promoted into `ui/`. Promote the day a second page needs one, // rather than promoted into `ui/`. Promote the day a second page needs
// same "don't build ahead of a second caller" rule `Panel`'s own // one, same "don't build ahead of a second caller" rule `Panel`'s own
// comment states. // comment states.
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import { Link } from 'wouter-preact'; import { Link } from 'wouter-preact';
import { Panel } from '../ui/panel/Panel.js'; import { Panel } from '../ui/panel/Panel.js';
import './AgentsPage.css'; import './CreateAgentPage.css';
interface CreateAgentResponse { interface CreateAgentResponse {
node_id: number; node_id: number;
@ -33,7 +42,7 @@ type SubmitState =
// catching it before a round-trip is a pure UX win, not a new gate. // catching it before a round-trip is a pure UX win, not a new gate.
const NAME_PATTERN = '[a-z0-9-]{1,63}'; const NAME_PATTERN = '[a-z0-9-]{1,63}';
export function AgentsPage() { export function CreateAgentPage() {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [result, setResult] = useState<SubmitState>({ status: 'idle' }); const [result, setResult] = useState<SubmitState>({ status: 'idle' });
@ -56,19 +65,19 @@ export function AgentsPage() {
} }
return ( return (
<Panel title="agents"> <Panel title="create agent">
<p class="agents-intro"> <p class="create-agent-intro">
Create a new agent's swarm-level identity. This only queues the job — check{' '} Create a new agent's swarm-level identity. This only queues the job — check{' '}
<Link href="/jobs">jobs</Link> to watch it settle. <Link href="/jobs">jobs</Link> to watch it settle.
</p> </p>
<form class="agents-form" onSubmit={submit}> <form class="create-agent-form" onSubmit={submit}>
<div class="agents-field"> <div class="create-agent-field">
<label class="agents-label" for="agent-name"> <label class="create-agent-label" for="agent-name">
agent name agent name
</label> </label>
<input <input
id="agent-name" id="agent-name"
class="agents-input" class="create-agent-input"
type="text" type="text"
value={name} value={name}
pattern={NAME_PATTERN} pattern={NAME_PATTERN}
@ -77,17 +86,19 @@ export function AgentsPage() {
onInput={(e) => setName(e.currentTarget.value)} onInput={(e) => setName(e.currentTarget.value)}
/> />
</div> </div>
<button class="agents-submit" type="submit" disabled={result.status === 'submitting'}> <button class="create-agent-submit" type="submit" disabled={result.status === 'submitting'}>
{result.status === 'submitting' ? 'creating…' : 'create'} {result.status === 'submitting' ? 'creating…' : 'create'}
</button> </button>
</form> </form>
{result.status === 'done' && ( {result.status === 'done' && (
<p class="agents-result agents-result-ok"> <p class="create-agent-result create-agent-result-ok">
queued as job node #{result.nodeId} <Link href="/jobs">watch it in jobs</Link> queued as job node #{result.nodeId} <Link href="/jobs">watch it in jobs</Link>
</p> </p>
)} )}
{result.status === 'error' && ( {result.status === 'error' && (
<p class="agents-result agents-result-error">failed to queue: {result.message}</p> <p class="create-agent-result create-agent-result-error">
failed to queue: {result.message}
</p>
)} )}
</Panel> </Panel>
); );

View file

@ -19,7 +19,7 @@ import './Shell.css';
const NAV_ITEMS: { href: string; label: string }[] = [ const NAV_ITEMS: { href: string; label: string }[] = [
{ href: '/', label: 'overview' }, { href: '/', label: 'overview' },
{ href: '/agents', label: 'agents' }, { href: '/create-agent', label: 'new agent' },
{ href: '/jobs', label: 'jobs' }, { href: '/jobs', label: 'jobs' },
{ href: '/components', label: 'components' }, { href: '/components', label: 'components' },
]; ];