Watch
0
0
Fork
You've already forked hyperhive
0

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:
iris 2026-10-25 16:07:01 +01:00 • committed by mara
commit 6cf89755e2
7 changed files with 63 additions and 40 deletions

View file

@ -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}>

View file

@ -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;

View file

@ -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>
</>
);
}

View file

@ -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;

View file

@ -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"

View file

@ -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)" },