swarm-ui: broadcast is a composer on the messages page, not a tab of its own
The messages page gets a broadcast button that opens the agent picker and message box above the stream, so a sent message shows up in the list below it. The broadcast route and nav tab go, with no redirect: the page is now the composer, BroadcastComposer, in the messages page.
This commit is contained in:
parent
711e413901
commit
6cf89755e2
7 changed files with 63 additions and 40 deletions
|
|
@ -11,22 +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/<name>/terminal` | one agent's live terminal |
|
||||
| `/agents/<name>/subagents/<subagent>/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 |
|
||||
| `/broadcast` | send one message to several agents at once, picked from a grid of their icons |
|
||||
| `/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 |
|
||||
| `/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/<name>/terminal` | one agent's live terminal |
|
||||
| `/agents/<name>/subagents/<subagent>/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 |
|
||||
|
||||
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
|
||||
|
|
@ -478,18 +477,21 @@ its newest 100 builds, so a link to an older one shows a note instead.
|
|||
|
||||
### Broadcasting a message
|
||||
|
||||
The **broadcast** page sends one message to several agents at once. Pick the
|
||||
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
|
||||
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.
|
||||
The page skips an agent you ticked and then hid with the filter. Type the
|
||||
The composer skips an agent you ticked and then hid with the filter. Type the
|
||||
message and press **send to N agents**. The text goes exactly as typed, as an
|
||||
inbox message from your authelia username, the same as the box under an agent's
|
||||
terminal. One broadcast may name at most 64 agents, and the button stays off
|
||||
until you untick the extra ones. A broadcast to many agents can take a few
|
||||
seconds to answer, and the button reads **sending…** meanwhile.
|
||||
|
||||
The page then lists what happened to each agent. An agent that wasn't
|
||||
The composer then lists what happened to each agent. An agent that wasn't
|
||||
listening shows as not sent, with the reason, and the others still got the
|
||||
message. **select the N that failed** ticks just those, switching to **all** if the
|
||||
filter hid any, so you can send the same message again. When every agent got the message the box clears; otherwise the
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { AgentsPage } from "./pages/agents/AgentsPage.js";
|
|||
import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js";
|
||||
import { AlertsPage } from "./pages/AlertsPage.js";
|
||||
import { BuildsPage } from "./pages/builds/BuildsPage.js";
|
||||
import { BroadcastPage } from "./pages/broadcast/BroadcastPage.js";
|
||||
import { MessagesPage } from "./pages/messages/MessagesPage.js";
|
||||
import { NEW_FLOW_PATH } from "./pages/notifications/FlowDraft.js";
|
||||
import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js";
|
||||
|
|
@ -46,7 +45,6 @@ export function App() {
|
|||
<Route path="/jobs" component={JobsPage} />
|
||||
<Route path="/schedules" component={SchedulesPage} />
|
||||
<Route path="/builds" component={BuildsPage} />
|
||||
<Route path="/broadcast" component={BroadcastPage} />
|
||||
<Route path="/messages" component={MessagesPage} />
|
||||
<Route path="/notifications" component={NotificationsPage} />
|
||||
<Route path={NEW_FLOW_PATH}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* <BroadcastPage>: the agent picker over the message box, then the per-agent
|
||||
/* <BroadcastComposer>: the agent picker over the message box, then the per-agent
|
||||
result. One column; the grid wraps to however many icons fit. */
|
||||
.ui-broadcast {
|
||||
display: flex;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// <BroadcastPage> — one message to several agents at once. Pick agents from a
|
||||
// <BroadcastComposer> — one message to several agents at once, in a panel on
|
||||
// the messages page above the stream that shows the messages arriving. Pick agents from a
|
||||
// grid of their icons (every agent, or just the ones running), type the
|
||||
// message, send. The controller does the fan-out: `POST /api/agents/broadcast`
|
||||
// answers one outcome per agent, so one that is down does not stop the rest and
|
||||
|
|
@ -16,7 +17,6 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
type RefreshIntervalMs,
|
||||
|
|
@ -38,7 +38,7 @@ import {
|
|||
type BroadcastAgent,
|
||||
type BroadcastResult,
|
||||
} from "./broadcast-types.js";
|
||||
import "./BroadcastPage.css";
|
||||
import "./BroadcastComposer.css";
|
||||
|
||||
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
||||
const FILTER_KEY = "swarm-ui:broadcast:filter";
|
||||
|
|
@ -57,7 +57,7 @@ function storedFilter(): AgentFilter {
|
|||
}
|
||||
}
|
||||
|
||||
export function BroadcastPage() {
|
||||
export function BroadcastComposer() {
|
||||
const [intervalMs, setIntervalMs] =
|
||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
const { data: agents, error: loadError } = usePolled(intervalMs, (signal) =>
|
||||
|
|
@ -126,17 +126,7 @@ export function BroadcastPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="broadcast"
|
||||
icon="📣"
|
||||
actions={
|
||||
<RefreshIntervalPicker
|
||||
id="broadcast-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{loadError && (
|
||||
<ApiErrorPanel context="failed to load agents" problem={loadError} />
|
||||
)}
|
||||
|
|
@ -179,6 +169,11 @@ export function BroadcastPage() {
|
|||
<span class="ui-broadcast-count" role="status">
|
||||
{targets.length} selected
|
||||
</span>
|
||||
<RefreshIntervalPicker
|
||||
id="broadcast-refresh"
|
||||
value={intervalMs}
|
||||
onChange={setIntervalMs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{agents !== null && shown.length === 0 ? (
|
||||
|
|
@ -253,7 +248,7 @@ export function BroadcastPage() {
|
|||
<SendResults results={send.results} onRetry={retryFailed} />
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,16 @@
|
|||
/* <MessagesPage>: 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;
|
||||
}
|
||||
.ui-messages-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// <MessagesPage> — `/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.
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
|
|
@ -11,6 +12,8 @@ import { memo } from "preact/compat";
|
|||
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 { Panel } from "../../ui/panel/Panel.js";
|
||||
import { SelectField } from "../../ui/select-field/SelectField.js";
|
||||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
|
|
@ -50,6 +53,7 @@ const MessageRow = memo(function MessageRow({ m }: { m: ArrivedMessage }) {
|
|||
export function MessagesPage() {
|
||||
const [sinceMinutes, setSinceMinutes] = useState(DEFAULT_WINDOW_MINUTES);
|
||||
const [query, setQuery] = useState("");
|
||||
const [composing, setComposing] = useState(false);
|
||||
const { messages, connection, reconnect } = useMessageStream(sinceMinutes);
|
||||
const shown = filterMessages(messages, query);
|
||||
|
||||
|
|
@ -79,6 +83,12 @@ export function MessagesPage() {
|
|||
icon="💬"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant={composing ? "primary" : "default"}
|
||||
onClick={() => setComposing((on) => !on)}
|
||||
>
|
||||
{composing ? "close broadcast" : "📣 broadcast"}
|
||||
</Button>
|
||||
<Badge
|
||||
tone={CONNECTION_TONE[connection]}
|
||||
value={CONNECTION_LABEL[connection]}
|
||||
|
|
@ -94,6 +104,15 @@ 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">
|
||||
<SelectField
|
||||
id="messages-window"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
|||
{ href: "/alerts", label: "alerts", accent: "var(--red)" },
|
||||
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
||||
{ href: "/schedules", label: "schedules", accent: "var(--cyan)" },
|
||||
{ href: "/broadcast", label: "broadcast", accent: "var(--blue)" },
|
||||
{ href: "/messages", label: "messages", accent: "var(--purple)" },
|
||||
{ href: "/notifications", label: "notifications", accent: "var(--yellow)" },
|
||||
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
|
||||
|
|
|
|||
Loading…
Reference in a new issue