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
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
/* <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;
|
||||
/* 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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// <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. 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 (
|
||||
<Panel
|
||||
title="messages"
|
||||
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]}
|
||||
/>
|
||||
{connection === "closed" ? (
|
||||
<Badge
|
||||
variant="quiet"
|
||||
value="reconnect"
|
||||
title="open the message stream again"
|
||||
onClick={reconnect}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* 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}
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
<BroadcastComposer />
|
||||
</section>
|
||||
<div class="ui-messages-bar">
|
||||
<SelectField
|
||||
id="messages-window"
|
||||
label="history"
|
||||
value={String(sinceMinutes)}
|
||||
onChange={(v) => setSinceMinutes(Number(v))}
|
||||
options={WINDOWS.map((w) => ({
|
||||
value: String(w.minutes),
|
||||
label: w.label,
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
id="messages-filter"
|
||||
label="agent"
|
||||
value={query}
|
||||
placeholder="sender or recipient"
|
||||
onInput={setQuery}
|
||||
/>
|
||||
<span class="ui-messages-count">
|
||||
{query.trim() === ""
|
||||
? `${messages.length} messages`
|
||||
: `${shown.length} of ${messages.length} messages`}
|
||||
</span>
|
||||
</div>
|
||||
<div class="terminal-wrap ui-messages-wrap">
|
||||
<div class="live terminal" ref={logRef} onScroll={handleScroll}>
|
||||
{shown.length === 0 ? (
|
||||
<div class="row note">
|
||||
<span class="row-content">
|
||||
{connection === "open"
|
||||
? messages.length === 0
|
||||
? "(connected — no messages yet)"
|
||||
: "(no message matches the filter)"
|
||||
: CONNECTION_LABEL[connection]}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
shown.map((m) => <MessageRow key={m.id} m={m} />)
|
||||
)}
|
||||
{composerBuilt ? <BroadcastComposer /> : null}
|
||||
</Disclosure>
|
||||
<Panel
|
||||
title="messages"
|
||||
icon="💬"
|
||||
actions={
|
||||
<>
|
||||
<Badge
|
||||
tone={CONNECTION_TONE[connection]}
|
||||
value={CONNECTION_LABEL[connection]}
|
||||
/>
|
||||
{connection === "closed" ? (
|
||||
<Badge
|
||||
variant="quiet"
|
||||
value="reconnect"
|
||||
title="open the message stream again"
|
||||
onClick={reconnect}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="ui-messages-bar">
|
||||
<SelectField
|
||||
id="messages-window"
|
||||
label="history"
|
||||
value={String(sinceMinutes)}
|
||||
onChange={(v) => setSinceMinutes(Number(v))}
|
||||
options={WINDOWS.map((w) => ({
|
||||
value: String(w.minutes),
|
||||
label: w.label,
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
id="messages-filter"
|
||||
label="agent"
|
||||
value={query}
|
||||
placeholder="sender or recipient"
|
||||
onInput={setQuery}
|
||||
/>
|
||||
<span class="ui-messages-count">
|
||||
{query.trim() === ""
|
||||
? `${messages.length} messages`
|
||||
: `${shown.length} of ${messages.length} messages`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="ui-messages-note">
|
||||
Shows what the agents sent each other. A message an agent drained with
|
||||
an in-turn <code>recv</code> is missing. The history is at most 100
|
||||
recent messages per agent.
|
||||
</p>
|
||||
</Panel>
|
||||
<div class="terminal-wrap ui-messages-wrap">
|
||||
<div class="live terminal" ref={logRef} onScroll={handleScroll}>
|
||||
{shown.length === 0 ? (
|
||||
<div class="row note">
|
||||
<span class="row-content">
|
||||
{connection === "open"
|
||||
? messages.length === 0
|
||||
? "(connected — no messages yet)"
|
||||
: "(no message matches the filter)"
|
||||
: CONNECTION_LABEL[connection]}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
shown.map((m) => <MessageRow key={m.id} m={m} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p class="ui-messages-note">
|
||||
Shows what the agents sent each other. A message an agent drained with
|
||||
an in-turn <code>recv</code> is missing. The history is at most 100
|
||||
recent messages per agent.
|
||||
</p>
|
||||
</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`
|
||||
// module is imported as it is.
|
||||
import assert from "node:assert/strict";
|
||||
|
|
|
|||
Loading…
Reference in a new issue