// /flow.html entry point. Owns the full-page broker terminal and the // @-mention compose box. Pulls shared infrastructure (DOM helpers, // side panel, OS notifications, path linkification) from `./common.js`. // // The operator inbox lives on the dashboard's Y3R C4LL tab now; FL0W // stays the pure event firehose. // // Does NOT contain the dashboard's tab renderers, mutation-event // dispatchers, or refreshState — that's `./tabs.js`, loaded only by // /dashboard.html. The flow page runs purely on the broker stream + an // initial /api/state fetch (compose autocomplete needs the live // container list). import { create as termCreate } from "@hive/shared/terminal.js"; import { $, NOTIF, appendLinkified, openStream, initServerWarnings, } from "./common.js"; import { el } from "@hive/shared/dom.js"; import { epochSec } from "./util.js"; (() => { NOTIF.bind(); initServerWarnings(); // ─── local containers cache (for compose autocomplete) ────────────────── // The compose box's @-mention completion suggests known agent names. // /dashboard.html (tabs.js) maintains the canonical `containersState` // from /api/state + SSE; here we keep a small local mirror updated // by the same `container_state_changed` / `container_removed` events // the dashboard would handle. const flowContainers = new Map(); fetch("/api/state") .then((r) => (r.ok ? r.json() : null)) .then((s) => { if (!s || !Array.isArray(s.containers)) return; for (const c of s.containers) flowContainers.set(c.name, c); populateAgentFilter(); }) .catch(() => { /* graceful: compose just shows `*` and nothing else */ }); // ─── agent filter ─────────────────────────────────────────────── // A select in the FL0W header narrows the timeline to messages involving // one agent (matched on `from` OR `to`). Each rendered row carries // `data-from` / `data-to`; non-matching rows get `.flow-hidden`. New rows // pick up the active filter at render time (see renderMsg); changing the // filter re-scans existing rows. Selection persists in localStorage so a // reload / tab-switch keeps the view. Uses `$('msgflow')` for row access // so it works regardless of the message-flow IIFE's local scope. let agentFilter = localStorage.getItem("flow-agent-filter") || ""; function rowMatchesFilter(from, to) { return !agentFilter || from === agentFilter || to === agentFilter; } function applyAgentFilter() { const flow = $("msgflow"); if (!flow) return; for (const row of flow.children) { const { from, to } = row.dataset; if (from === undefined && to === undefined) continue; // non-message row row.classList.toggle("flow-hidden", !rowMatchesFilter(from, to)); } } function populateAgentFilter() { const sel = $("flow-agent-filter"); if (!sel) return; const names = [...flowContainers.keys()].sort(); sel.replaceChildren(); sel.append(el("option", { value: "" }, "all agents")); for (const n of names) sel.append(el("option", { value: n }, n)); // Preserve a saved selection even if that agent isn't in the live // container list (yet / anymore) so the filter doesn't silently reset. if (agentFilter && !names.includes(agentFilter)) { sel.append(el("option", { value: agentFilter }, agentFilter)); } sel.value = agentFilter; } { const sel = $("flow-agent-filter"); if (sel) { sel.addEventListener("change", () => { agentFilter = sel.value; if (agentFilter) localStorage.setItem("flow-agent-filter", agentFilter); else localStorage.removeItem("flow-agent-filter"); applyAgentFilter(); }); } } // ─── message flow: shared terminal pane ──────────────────────────────── // Scroll, pill, backfill + SSE plumbing live in @hive/shared/terminal. // What stays here is the broker-message renderer + the page-local // side effects (banner pulse, OS notifications on operator-bound // traffic). (() => { const flow = $("msgflow"); if (!flow) return; flow.replaceChildren(); const tsFmt = (ts) => new Date(ts).toISOString().slice(11, 19); // Pulse the page banner whenever a broker event lands. The // `.banner` element lives in the dashboard's