swarm-ui: add a messages page for the swarm-wide message feed
/messages reads GET /api/messages/stream. A history select picks the window (live only up to 24 hours) and a filter keeps the messages whose sender or recipient contains the text. Each message is a row of the terminal list an agent's terminal uses: one summary line with the time, the two parties and the start of the body, and the whole body under it. The list follows the newest message and holds the last 2000. The feed has no event id, so a reconnect sends the history again. The page empties its list each time the connection opens and fills it from the replay, where the terminal stream hook, which keeps its rows across a reconnect, would list every message twice. The stream URL, filter, cap and row logic is in its own module with tests, and docs/swarm/ui.md describes the page.
This commit is contained in:
parent
c32906b893
commit
447498fef7
1 changed files with 395 additions and 0 deletions
88
frontend/packages/swarm-ui/src/pages/messages/messages.ts
Normal file
88
frontend/packages/swarm-ui/src/pages/messages/messages.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// The messages page's logic, kept out of the component so `npm test` can run
|
||||
// it: the stream URL, which messages a filter keeps, and how one message
|
||||
// becomes a terminal row.
|
||||
import type { TermRow } from "@hive/shared/term-msg.js";
|
||||
|
||||
/** One event of `GET /api/messages/stream`; mirrors `SwarmMessage`. */
|
||||
export interface SwarmMessage {
|
||||
/** ISO 8601 / RFC 3339 UTC. */
|
||||
ts: string;
|
||||
from: string;
|
||||
to: string;
|
||||
body: string;
|
||||
body_format?: "markdown" | "diff";
|
||||
}
|
||||
|
||||
/** The history windows the page offers, in minutes; 0 is live only. */
|
||||
export const WINDOWS: { minutes: number; label: string }[] = [
|
||||
{ minutes: 0, label: "live only" },
|
||||
{ minutes: 15, label: "last 15 min" },
|
||||
{ minutes: 60, label: "last hour" },
|
||||
{ minutes: 360, label: "last 6 hours" },
|
||||
{ minutes: 1440, label: "last 24 hours" },
|
||||
];
|
||||
|
||||
export const DEFAULT_WINDOW_MINUTES = 60;
|
||||
|
||||
/** The stream for `sinceMinutes` of history before the live tail. */
|
||||
export function messagesUrl(sinceMinutes: number): string {
|
||||
return `/api/messages/stream?since_minutes=${sinceMinutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The messages that name `query` in their sender or recipient, as a
|
||||
* case-insensitive substring; all of them when `query` is blank.
|
||||
*/
|
||||
export function filterMessages(
|
||||
messages: SwarmMessage[],
|
||||
query: string,
|
||||
): SwarmMessage[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (q === "") return messages;
|
||||
return messages.filter(
|
||||
(m) => m.from.toLowerCase().includes(q) || m.to.toLowerCase().includes(q),
|
||||
);
|
||||
}
|
||||
|
||||
/** At most `max` messages, the newest ones, when there are more. */
|
||||
export function newest(messages: SwarmMessage[], max: number): SwarmMessage[] {
|
||||
return messages.length > max
|
||||
? messages.slice(messages.length - max)
|
||||
: messages;
|
||||
}
|
||||
|
||||
/** `body`'s first non-blank line, cut to `max` characters with an ellipsis. */
|
||||
export function snippet(body: string, max: number): string {
|
||||
const line =
|
||||
body
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.find((l) => l !== "") ?? "";
|
||||
return line.length > max ? line.slice(0, max - 1) + "…" : line;
|
||||
}
|
||||
|
||||
/** `ts` as `HH:MM:SS` in the viewer's time zone; `ts` itself when it is no date. */
|
||||
export function clock(ts: string): string {
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) return ts;
|
||||
const p = (n: number) => String(n).padStart(2, "0");
|
||||
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The terminal row for `m`: one summary line with the time, the two parties
|
||||
* and the start of the body, and the whole body under it.
|
||||
*/
|
||||
export function messageRow(m: SwarmMessage, key: string): TermRow {
|
||||
const start = snippet(m.body, 80);
|
||||
return {
|
||||
key,
|
||||
ts: m.ts,
|
||||
icon: "💬",
|
||||
level: "info",
|
||||
summary: `${clock(m.ts)} ${m.from} → ${m.to}${start ? ` — ${start}` : ""}`,
|
||||
body: m.body,
|
||||
body_format: m.body_format,
|
||||
fromHistory: false,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue