Watch
0
0
Fork
You've already forked hyperhive
0

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:
iris 2026-10-18 18:19:30 +02:00
commit 447498fef7
8 changed files with 395 additions and 0 deletions

View file

@ -0,0 +1,48 @@
// The live feed of `GET /api/messages/stream`, one `SwarmMessage` per event.
// The stream has no event id, so a reconnect sends the history again from the
// start; the list therefore empties each time the connection opens, and the
// replay refills it. Not `useSwarmTermStream`: that one keeps its rows across
// a reconnect, which here would list every message twice.
import { useEffect, useState } from "preact/hooks";
import type { SwarmMessage } from "./messages.js";
import { messagesUrl, newest } from "./messages.js";
export type ConnectionState = "connecting" | "open" | "reconnecting" | "closed";
/** The most messages the page holds; older ones drop off. */
export const MAX_MESSAGES = 2000;
export function useMessageStream(sinceMinutes: number): {
messages: SwarmMessage[];
connection: ConnectionState;
} {
const [messages, setMessages] = useState<SwarmMessage[]>([]);
const [connection, setConnection] = useState<ConnectionState>("connecting");
useEffect(() => {
setMessages([]);
setConnection("connecting");
const es = new EventSource(messagesUrl(sinceMinutes));
es.onopen = () => {
setMessages([]);
setConnection("open");
};
es.onmessage = (e) => {
let m: SwarmMessage;
try {
m = JSON.parse(e.data) as SwarmMessage;
} catch {
return;
}
setMessages((prev) => newest([...prev, m], MAX_MESSAGES));
};
es.onerror = () => {
setConnection(
es.readyState === 0 /* CONNECTING */ ? "reconnecting" : "closed",
);
};
return () => es.close();
}, [sinceMinutes]);
return { messages, connection };
}