swarm-ui: the broadcast composer is its own folded card above the messages card
Replaces the broadcast button in the messages card's header: a Disclosure titled broadcast, folded by default, over the messages card. The composer is built on the first open and kept, so it polls the agent list only once used. The broadcast page wording in broadcast-types.ts and its test now says composer.
This commit is contained in:
parent
6cf89755e2
commit
31f8a521e0
5 changed files with 112 additions and 112 deletions
|
|
@ -12,7 +12,7 @@ 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.
|
sign-out pages; the hive dashboard and the per-agent page have the same menu.
|
||||||
|
|
||||||
| route | what |
|
| 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 |
|
| `/` | 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 |
|
| `/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` | 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 |
|
||||||
|
|
@ -21,7 +21,7 @@ sign-out pages; the hive dashboard and the per-agent page have the same menu.
|
||||||
| `/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 |
|
| `/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 |
|
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||||
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
| `/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 |
|
| `/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 |
|
| `/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 |
|
| `/issues` | a cross-repo issue report |
|
||||||
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
||||||
|
|
@ -477,10 +477,10 @@ its newest 100 builds, so a link to an older one shows a note instead.
|
||||||
|
|
||||||
### Broadcasting a message
|
### Broadcasting a message
|
||||||
|
|
||||||
The **broadcast** button on the messages page (`/messages`) opens a composer above the
|
The messages page (`/messages`) has a **broadcast** card above the message list. It
|
||||||
message list that sends one message to several agents at once, and the messages
|
starts folded; select its header to open it. The card holds a composer that sends one message
|
||||||
show up in the list below it as they go. The button reads **close broadcast**
|
to several agents at once, and the messages show up in the list below as they
|
||||||
while the composer is open; closing it keeps what you typed and ticked. Pick the
|
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
|
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.
|
themselves running and **all** shows every agent; the browser keeps the choice.
|
||||||
**select all** ticks every agent the filter shows, and **clear** unticks them.
|
**select all** ticks every agent the filter shows, and **clear** unticks them.
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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.
|
// is plain functions of its arguments and can be tested.
|
||||||
|
|
||||||
/** The part of an `/api/agents/status` row the page reads. */
|
/** The part of an `/api/agents/status` row the page reads. */
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
/* <MessagesPage>: the filter bar over the terminal-style message list. */
|
/* <MessagesPage>: the filter bar over the terminal-style message list. */
|
||||||
@import "@hive/shared/terminal.css";
|
@import "@hive/shared/terminal.css";
|
||||||
|
|
||||||
/* The broadcast composer above the list, set apart from it by a rule. The
|
/* The broadcast card, folded by default, over the messages card. */
|
||||||
composer's own rules are BroadcastComposer.css's. */
|
.ui-messages-page {
|
||||||
.ui-messages-broadcast {
|
display: flex;
|
||||||
margin-block-end: 1em;
|
flex-direction: column;
|
||||||
padding-block-end: 1em;
|
gap: 1em;
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.ui-messages-broadcast[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
.ui-messages-bar {
|
.ui-messages-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// <MessagesPage> — `/messages`: every message between agents, and from the
|
// <MessagesPage> — `/messages`: every message between agents, and from the
|
||||||
// operator, reminders and compaction to an agent, as the swarm's controller
|
// operator, reminders and compaction to an agent, as the swarm's controller
|
||||||
// streams them (`GET /api/messages/stream`), across all hives. The **broadcast**
|
// streams them (`GET /api/messages/stream`), across all hives. Above the
|
||||||
// button opens a composer above the list to send one message to several agents.
|
// 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
|
// 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
|
// 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 { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||||
import { Row } from "@hive/shared/term-row.js";
|
import { Row } from "@hive/shared/term-row.js";
|
||||||
import { BroadcastComposer } from "../broadcast/BroadcastComposer.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 { Panel } from "../../ui/panel/Panel.js";
|
||||||
import { SelectField } from "../../ui/select-field/SelectField.js";
|
import { SelectField } from "../../ui/select-field/SelectField.js";
|
||||||
import { TextField } from "../../ui/text-field/TextField.js";
|
import { TextField } from "../../ui/text-field/TextField.js";
|
||||||
|
|
@ -54,6 +55,10 @@ export function MessagesPage() {
|
||||||
const [sinceMinutes, setSinceMinutes] = useState(DEFAULT_WINDOW_MINUTES);
|
const [sinceMinutes, setSinceMinutes] = useState(DEFAULT_WINDOW_MINUTES);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [composing, setComposing] = useState(false);
|
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 { messages, connection, reconnect } = useMessageStream(sinceMinutes);
|
||||||
const shown = filterMessages(messages, query);
|
const shown = filterMessages(messages, query);
|
||||||
|
|
||||||
|
|
@ -78,17 +83,24 @@ export function MessagesPage() {
|
||||||
}, [newestId, shown.length, stickToBottom]);
|
}, [newestId, shown.length, stickToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div class="ui-messages-page">
|
||||||
|
<Disclosure
|
||||||
|
title="broadcast"
|
||||||
|
icon="📣"
|
||||||
|
hint="send one message to several agents"
|
||||||
|
open={composing}
|
||||||
|
onToggle={(open) => {
|
||||||
|
setComposing(open);
|
||||||
|
if (open) setComposerBuilt(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{composerBuilt ? <BroadcastComposer /> : null}
|
||||||
|
</Disclosure>
|
||||||
<Panel
|
<Panel
|
||||||
title="messages"
|
title="messages"
|
||||||
icon="💬"
|
icon="💬"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button
|
|
||||||
variant={composing ? "primary" : "default"}
|
|
||||||
onClick={() => setComposing((on) => !on)}
|
|
||||||
>
|
|
||||||
{composing ? "close broadcast" : "📣 broadcast"}
|
|
||||||
</Button>
|
|
||||||
<Badge
|
<Badge
|
||||||
tone={CONNECTION_TONE[connection]}
|
tone={CONNECTION_TONE[connection]}
|
||||||
value={CONNECTION_LABEL[connection]}
|
value={CONNECTION_LABEL[connection]}
|
||||||
|
|
@ -104,15 +116,6 @@ export function MessagesPage() {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Kept mounted while closed, so what was typed and picked stays and the
|
|
||||||
agent list is already loaded when it opens. */}
|
|
||||||
<section
|
|
||||||
class="ui-messages-broadcast"
|
|
||||||
aria-label="broadcast a message"
|
|
||||||
hidden={!composing}
|
|
||||||
>
|
|
||||||
<BroadcastComposer />
|
|
||||||
</section>
|
|
||||||
<div class="ui-messages-bar">
|
<div class="ui-messages-bar">
|
||||||
<SelectField
|
<SelectField
|
||||||
id="messages-window"
|
id="messages-window"
|
||||||
|
|
@ -160,5 +163,6 @@ export function MessagesPage() {
|
||||||
recent messages per agent.
|
recent messages per agent.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`
|
// Node runs with its built-in test runner and type stripping, so the `.ts`
|
||||||
// module is imported as it is.
|
// module is imported as it is.
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue