diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 82780f75..26d77b00 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -11,21 +11,21 @@ The signed-in user's avatar sits at the right end of the header on every page. It opens a menu with the user's name and links to authelia's settings and sign-out pages; the hive dashboard and the per-agent page have the same menu. -| route | what | -| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/` | the start page: the swarm's name over a digital-rain backdrop, plus a paperclip offering to set up an agent while no `ruth` exists (it opens the create form with `ruth` filled in); the swarm name in the header links here | -| `/hives` | the hive directory, each hive with its last reported status | -| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts | -| `/agents//terminal` | one agent's live terminal | -| `/agents//subagents//terminal` | one of that agent's subagents' live terminal | -| `/alerts` | the alerts the swarm's Grafana holds now: the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token | -| `/jobs` | the controller's job graph — where agent creation and credential mints show progress | -| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove | -| `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; its **broadcast** button opens a composer that sends one message to several agents at once | -| `/notifications` | the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows | -| `/issues` | a cross-repo issue report | -| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings | -| `/builds` | the swarm builder's recent builds of agent config PRs, with what nix printed when one failed | +| route | what | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/` | the start page: the swarm's name over a digital-rain backdrop, plus a paperclip offering to set up an agent while no `ruth` exists (it opens the create form with `ruth` filled in); the swarm name in the header links here | +| `/hives` | the hive directory, each hive with its last reported status | +| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts | +| `/agents//terminal` | one agent's live terminal | +| `/agents//subagents//terminal` | one of that agent's subagents' live terminal | +| `/alerts` | the alerts the swarm's Grafana holds now: the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token | +| `/jobs` | the controller's job graph — where agent creation and credential mints show progress | +| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove | +| `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; a folded **broadcast** card above the feed sends one message to several agents at once | +| `/notifications` | the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows | +| `/issues` | a cross-repo issue report | +| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings | +| `/builds` | the swarm builder's recent builds of agent config PRs, with what nix printed when one failed | Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md). Pages that refresh on a timer share one polling rule: the page sends no new @@ -477,10 +477,10 @@ its newest 100 builds, so a link to an older one shows a note instead. ### Broadcasting a message -The **broadcast** button on the messages page (`/messages`) opens a composer above the -message list that sends one message to several agents at once, and the messages -show up in the list below it as they go. The button reads **close broadcast** -while the composer is open; closing it keeps what you typed and ticked. Pick the +The messages page (`/messages`) has a **broadcast** card above the message list. It +starts folded; select its header to open it. The card holds a composer that sends one message +to several agents at once, and the messages show up in the list below as they +go. Folding it again keeps what you typed and ticked. Pick the agents from a grid of their icons. **running** shows the agents that report themselves running and **all** shows every agent; the browser keeps the choice. **select all** ticks every agent the filter shows, and **clear** unticks them. diff --git a/frontend/packages/swarm-ui/src/pages/broadcast/broadcast-types.ts b/frontend/packages/swarm-ui/src/pages/broadcast/broadcast-types.ts index 99b083b7..6634ba37 100644 --- a/frontend/packages/swarm-ui/src/pages/broadcast/broadcast-types.ts +++ b/frontend/packages/swarm-ui/src/pages/broadcast/broadcast-types.ts @@ -1,6 +1,6 @@ -// What the broadcast page decides without a DOM: which agents the filter +// What the broadcast composer decides without a DOM: which agents the filter // shows, which of the ticked ones a send really goes to, and how the -// controller's per-agent answer is read. Kept out of `BroadcastPage.tsx` so it +// controller's per-agent answer is read. Kept out of `BroadcastComposer.tsx` so it // is plain functions of its arguments and can be tested. /** The part of an `/api/agents/status` row the page reads. */ diff --git a/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.css b/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.css index c2ce449b..bcb6fe8a 100644 --- a/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.css +++ b/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.css @@ -1,15 +1,11 @@ /* : the filter bar over the terminal-style message list. */ @import "@hive/shared/terminal.css"; -/* The broadcast composer above the list, set apart from it by a rule. The - composer's own rules are BroadcastComposer.css's. */ -.ui-messages-broadcast { - margin-block-end: 1em; - padding-block-end: 1em; - border-bottom: 1px solid var(--border); -} -.ui-messages-broadcast[hidden] { - display: none; +/* The broadcast card, folded by default, over the messages card. */ +.ui-messages-page { + display: flex; + flex-direction: column; + gap: 1em; } .ui-messages-bar { display: flex; diff --git a/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.tsx b/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.tsx index d5540745..e8322c02 100644 --- a/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/messages/MessagesPage.tsx @@ -1,7 +1,8 @@ // — `/messages`: every message between agents, and from the // operator, reminders and compaction to an agent, as the swarm's controller -// streams them (`GET /api/messages/stream`), across all hives. The **broadcast** -// button opens a composer above the list to send one message to several agents. +// streams them (`GET /api/messages/stream`), across all hives. Above the +// list, a folded **broadcast** card holds the composer that sends one message +// to several agents. // // The stream sends the chosen window of history first, then the live tail. A // message an agent drained with an in-turn `recv` is missing, because it starts @@ -13,7 +14,7 @@ import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { Badge, type BadgeTone } from "@hive/shared/badge.js"; import { Row } from "@hive/shared/term-row.js"; import { BroadcastComposer } from "../broadcast/BroadcastComposer.js"; -import { Button } from "../../ui/button/Button.js"; +import { Disclosure } from "../../ui/disclosure/Disclosure.js"; import { Panel } from "../../ui/panel/Panel.js"; import { SelectField } from "../../ui/select-field/SelectField.js"; import { TextField } from "../../ui/text-field/TextField.js"; @@ -54,6 +55,10 @@ export function MessagesPage() { const [sinceMinutes, setSinceMinutes] = useState(DEFAULT_WINDOW_MINUTES); const [query, setQuery] = useState(""); const [composing, setComposing] = useState(false); + // The composer is built on the first open and kept after, so what was typed + // and ticked stays through a fold, and a page where it is never opened does + // not poll the agent list. + const [composerBuilt, setComposerBuilt] = useState(false); const { messages, connection, reconnect } = useMessageStream(sinceMinutes); const shown = filterMessages(messages, query); @@ -78,87 +83,86 @@ export function MessagesPage() { }, [newestId, shown.length, stickToBottom]); return ( - - - - {connection === "closed" ? ( - - ) : null} - - } - > - {/* Kept mounted while closed, so what was typed and picked stays and the - agent list is already loaded when it opens. */} - -
- setSinceMinutes(Number(v))} - options={WINDOWS.map((w) => ({ - value: String(w.minutes), - label: w.label, - }))} - /> - - - {query.trim() === "" - ? `${messages.length} messages` - : `${shown.length} of ${messages.length} messages`} - -
-
-
- {shown.length === 0 ? ( -
- - {connection === "open" - ? messages.length === 0 - ? "(connected — no messages yet)" - : "(no message matches the filter)" - : CONNECTION_LABEL[connection]} - -
- ) : ( - shown.map((m) => ) - )} + {composerBuilt ? : null} + + + + {connection === "closed" ? ( + + ) : null} + + } + > +
+ setSinceMinutes(Number(v))} + options={WINDOWS.map((w) => ({ + value: String(w.minutes), + label: w.label, + }))} + /> + + + {query.trim() === "" + ? `${messages.length} messages` + : `${shown.length} of ${messages.length} messages`} +
-
-

- Shows what the agents sent each other. A message an agent drained with - an in-turn recv is missing. The history is at most 100 - recent messages per agent. -

- +
+
+ {shown.length === 0 ? ( +
+ + {connection === "open" + ? messages.length === 0 + ? "(connected — no messages yet)" + : "(no message matches the filter)" + : CONNECTION_LABEL[connection]} + +
+ ) : ( + shown.map((m) => ) + )} +
+
+

+ Shows what the agents sent each other. A message an agent drained with + an in-turn recv is missing. The history is at most 100 + recent messages per agent. +

+ +
); } diff --git a/frontend/packages/swarm-ui/test/broadcast-types.test.mjs b/frontend/packages/swarm-ui/test/broadcast-types.test.mjs index c2a93cca..fd96e151 100644 --- a/frontend/packages/swarm-ui/test/broadcast-types.test.mjs +++ b/frontend/packages/swarm-ui/test/broadcast-types.test.mjs @@ -1,4 +1,4 @@ -// The broadcast page's selection and answer rules. Run by `npm test`, which +// The broadcast composer's selection and answer rules. Run by `npm test`, which // Node runs with its built-in test runner and type stripping, so the `.ts` // module is imported as it is. import assert from "node:assert/strict";