diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx index 65d47afe..b97634a3 100644 --- a/frontend/packages/swarm-ui/src/App.tsx +++ b/frontend/packages/swarm-ui/src/App.tsx @@ -4,6 +4,7 @@ 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 { CreateAgentPage } from './pages/CreateAgentPage.js'; import { JobsPage } from './pages/JobsPage.js'; import { HivesPage } from './pages/HivesPage.js'; import { Panel } from './ui/panel/Panel.js'; @@ -22,6 +23,7 @@ export function App() { + diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index fe312ec1..08fee821 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -11,18 +11,9 @@ // client-side into one row per agent rather than N per-agent config-PR // calls β€” exactly why the bulk endpoint exists instead of looping the // single-agent one. -// -// Owns the "+ agent" trigger too: creation used to be its own -// `/create-agent` route + nav item, but the roster this populates is -// the natural home for the action that populates it β€” a separate top- -// level nav entry was one click of indirection for no benefit. The form -// itself (`CreateAgentForm`) is unchanged from its page days, just -// mounted inside a `Dialog` instead of a route. 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 { Button } from '../ui/button/Button.js'; -import { Dialog } from '../ui/dialog/Dialog.js'; import { Panel } from '../ui/panel/Panel.js'; import { RefreshIntervalPicker, @@ -31,7 +22,6 @@ import { } from '../ui/refresh-interval/RefreshInterval.js'; import { StatusChip } from '../ui/status-chip/StatusChip.js'; import { Table, type TableColumn } from '../ui/table/Table.js'; -import { CreateAgentForm } from './CreateAgentForm.js'; interface ConfigPrStatus { pr_number: number; @@ -77,7 +67,6 @@ export function AgentsPage() { const [rows, setRows] = useState(null); const [error, setError] = useState(null); const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); - const [createOpen, setCreateOpen] = useState(false); useRefreshInterval(intervalMs, () => { (async () => { @@ -104,21 +93,11 @@ export function AgentsPage() { - - - - } + actions={} > {error ? : null} {!error && rows === null ?

loading…

: null} {rows ? a.name} /> : null} - setCreateOpen(false)} label="create agent"> - - ); } diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css similarity index 86% rename from frontend/packages/swarm-ui/src/pages/CreateAgentForm.css rename to frontend/packages/swarm-ui/src/pages/CreateAgentPage.css index 1f59c51e..7c5d1810 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css @@ -1,8 +1,6 @@ -/* β€” the create-agent form, rendered inside a `Dialog` - from `AgentsPage` (see that component's file-top comment for why). - Input/button chrome comes from the shared `ui/` form kit - (`TextField`/`SelectField`/`Button`); this file only owns this - component's own layout + copy. Column, +/* β€” the create-agent form. Input/button chrome now + comes from the shared `ui/` form kit (`TextField`/`SelectField`/ + `Button`); this file only owns the page's own layout + copy. Column, not row inside the form itself: with a second field (hive) added, a row layout put fields of different natural widths on one baseline and looked misaligned β€” mara: "make it a col". Reuses the same base16 diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx similarity index 88% rename from frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx rename to frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx index b13aaa7c..82cc7cb8 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx @@ -1,19 +1,20 @@ -// β€” minimal agent creation form, POSTs to +// β€” 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). +// 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). // -// 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. +// 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. // // `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 @@ -33,7 +34,7 @@ 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'; +import './CreateAgentPage.css'; // Mirrors swarm-controller's `HiveEntry` β€” same shape `HivesPage` // consumes off `/api/hives/status`, but this page hits the plain @@ -69,7 +70,7 @@ type SubmitState = // exception report on this page. const NAME_PATTERN = '[a-z0-9\\-]{1,63}'; -export function CreateAgentForm() { +export function CreateAgentPage() { const [name, setName] = useState(''); const [hive, setHive] = useState(''); // `null` = still loading, `[]` = loaded but empty (a real, if unusual, diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index 539471d2..39b0cf79 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -39,6 +39,7 @@ import './Shell.css'; const NAV_ITEMS: { href: string; label: string; accent: string }[] = [ { href: '/', label: 'hives', accent: 'var(--purple)' }, { href: '/agents', label: 'agents', accent: 'var(--green)' }, + { href: '/create-agent', label: 'new agent', accent: 'var(--cyan)' }, { href: '/jobs', label: 'jobs', accent: 'var(--pink)' }, { href: '/components', label: 'components', accent: 'var(--blue)' }, ]; diff --git a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css b/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css deleted file mode 100644 index ee4d4b96..00000000 --- a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css +++ /dev/null @@ -1,53 +0,0 @@ -/* β€” native `` styling. Sized to comfortably hold the - create-agent form's own two-panel layout (which wraps to single- - column below its existing 16em-per-column threshold β€” see - CreateAgentForm.css β€” so this doesn't need to special-case that). */ -.ui-dialog { - /* No sitewide `box-sizing: border-box` reset exists β€” without this, - the padding + border below add ON TOP of `width: 90vw` rather than - being carved out of it, overflowing a real phone-width viewport - (measured: 401px rendered against a 390px viewport). Caught via a - real screenshot at 390px, not assumed. */ - box-sizing: border-box; - position: relative; - width: 90vw; - max-width: 44em; - max-height: 85vh; - overflow: auto; - padding: 1.5em; - border: 1px solid var(--border); - border-radius: 0.6em; - background: var(--bg-elev); - color: var(--fg); -} -.ui-dialog::backdrop { - background: rgba(0, 0, 0, 0.5); -} -.ui-dialog-close { - position: absolute; - top: 0.75em; - right: 0.75em; - display: flex; - align-items: center; - justify-content: center; - width: 2.2em; - height: 2.2em; - border: 1px solid transparent; - border-radius: 0.4em; - background: none; - color: var(--fg); - font-size: 1em; - line-height: 1; - cursor: pointer; -} -.ui-dialog-close:hover { - border-color: var(--border); - background: var(--bg); -} -.ui-dialog-body { - /* Clears the close button's own box (0.75em inset + 2.2em size) with - room to spare β€” verified against a real screenshot; a smaller value - let wide content (e.g. the create-agent form's two-panel layout) - render directly under the button instead of beside it. */ - padding-right: 3.25em; -} diff --git a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx b/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx deleted file mode 100644 index e485bad3..00000000 --- a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// β€” a generic modal overlay, native `` element rather -// than a hand-rolled focus-trap + backdrop: `showModal()` gives focus -// trapping, Escape-to-close, and a real `::backdrop` for free, and -// `@hive/shared`'s own shadow-DOM `hive-dialog` custom element isn't an -// option here β€” swarm-ui's esbuild config can't consume those custom -// elements at all (a separate, already-tracked gap). -// No opinion on content beyond a close button; the caller's own markup -// supplies whatever heading/layout it needs (its first real caller, -// the create-agent form, already has an established two-panel layout -// from its former life as a standalone route β€” reusing that unchanged -// is simpler and lower-risk than re-deriving a modal-specific layout -// for content mara has already reviewed). -import { useEffect, useRef } from 'preact/hooks'; -import type { ComponentChildren } from 'preact'; -import './Dialog.css'; - -export function Dialog({ - open, - onClose, - label, - children, -}: { - open: boolean; - onClose: () => void; - // `aria-label` only, no visible title row β€” see file-top comment on - // why content owns its own heading. - label: string; - children: ComponentChildren; -}) { - const ref = useRef(null); - - // Drives the native open/closed state from the `open` prop rather - // than mounting/unmounting the element β€” `showModal()`/`close()` are - // imperative, there's no declarative `` equivalent that - // also gets you the backdrop + focus trap. - useEffect(() => { - const el = ref.current; - if (!el) return; - if (open && !el.open) el.showModal(); - else if (!open && el.open) el.close(); - }, [open]); - - // The dialog's own `close` event fires on Escape (and would fire on - // a native
submit, unused here) β€” syncing it - // back to the caller's state keeps `open` truthful even when nothing - // in this component's own JS drove the close. - useEffect(() => { - const el = ref.current; - if (!el) return; - function handleClose() { - onClose(); - } - el.addEventListener('close', handleClose); - return () => el.removeEventListener('close', handleClose); - }, [onClose]); - - return ( - ` element itself (not any child) - // exactly when it's outside the rendered content box β€” inside - // the box, the click target is always some descendant. Standard - // "click the backdrop to close" trick for native ``. - onClick={(e) => { - if (e.target === ref.current) onClose(); - }} - > - -
{children}
-
- ); -} diff --git a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css index 94906f12..ed4f1393 100644 --- a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css +++ b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css @@ -3,7 +3,7 @@ from (one class, so the two never drift). `width: 100%` fills whichever container the caller gives it β€” the kit itself has no opinion on a maximum width; a page that wants one narrower than its - own layout caps it at the layout level (`CreateAgentForm.css`'s + own layout caps it at the layout level (`CreateAgentPage.css`'s `.create-agent-form-col` is the existing example), same reasoning `Panel` has no width opinion of its own either. `min-height` is a touch-target floor (44px at the default 16px root font β€” WCAG @@ -15,13 +15,13 @@ The field wrapper repeats `.ui-form-control`'s own `width: 100%` rather than leaving the wrapper unconstrained: inside a shrink-to-fit - flex column (`CreateAgentForm`'s form is one), an unconstrained + flex column (`CreateAgentPage`'s form is one), an unconstrained wrapper sizes to its own content β€” and a `width: 100%` *control* inside an auto-width wrapper resolves against that shrunk width, not the container the page actually gave it, so two fields with differently-long labels ("agent name" vs "hive") ended up with differently-wide inputs β€” the misalignment mara reported on the - create-agent form. Matching the two declarations here means every + create-agent page. Matching the two declarations here means every field's control width is driven by the same container width regardless of its label's length or its siblings'. */ .ui-form-field { diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx index d74bcdb3..01265adf 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx @@ -15,7 +15,7 @@ // `icon` is a small header glyph, left of the title β€” the swarm-ui // design guide's own whimsy reference (before this, the guide only // pointed at the dashboard's matrix-rain background). Grew out of -// CreateAgentForm's one-off πŸͺͺ dropped straight into a panel's body +// CreateAgentPage's one-off πŸͺͺ dropped straight into a panel's body // copy: mara's call on review was that a single ad-hoc emoji isn't // whimsy in the guide's sense (small, delightful, *consistent*), it // should be a real theme every panel can opt into the same way. Plain diff --git a/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx b/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx index e0cba5ef..c759e6f6 100644 --- a/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx +++ b/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx @@ -1,5 +1,5 @@ // β€” a labelled single-line text input, the shared control -// every form (`CreateAgentForm` today) reaches for instead +// every page-level form (`CreateAgentPage` today) reaches for instead // of hand-rolling its own label/input pair. No textarea/multi-line // mode β€” promote that the day a real caller needs one, same "don't // build ahead of a caller" rule the rest of `ui/` follows.