// /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