// /flow.html entry point (#406 step 2 — flow-specific split from app.js). // // Owns the full-page broker terminal, the operator-inbox derived store // (populated from the broker stream), the inbox pill flyout, and the // @-mention compose box. Pulls shared infrastructure (DOM helpers, side // panel, OS notifications, path linkification) from `./common.js`. // // Does NOT contain the dashboard's tab renderers, mutation-event // dispatchers, or refreshState — that's `./app.js` (the legacy entry // kept until step 3 renames it to `tabs.js`). For now 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 { $, el, Panel, NOTIF, appendLinkified, } from './common.js'; (() => { Panel.bind(); NOTIF.bind(); // ─── operator inbox (derived from the broker message stream) ─────────── // No longer shipped on `/api/state.operator_inbox`. The broker // terminal feeds this via `onAnyEvent` — backfill from // `/dashboard/history` populates on load, live SSE keeps it current. // Newest-first to match the previous behaviour. const INBOX_LIMIT = 50; const operatorInbox = []; function inboxAppendFromEvent(ev) { if (ev.kind !== 'sent' || ev.to !== 'operator') return false; operatorInbox.unshift({ from: ev.from, body: ev.body, at: ev.at, file_refs: ev.file_refs || [], }); if (operatorInbox.length > INBOX_LIMIT) operatorInbox.length = INBOX_LIMIT; return true; } function buildInboxListNode() { if (!operatorInbox.length) return el('p', { class: 'empty' }, 'no messages'); const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19); const ul = el('ul', { class: 'inbox' }); for (const m of operatorInbox) { const li = el('li'); const body = el('span', { class: 'msg-body' }); appendLinkified(body, m.body, m.file_refs); li.append( el('span', { class: 'msg-ts' }, fmt(m.at)), ' ', el('span', { class: 'msg-from' }, m.from), ' ', el('span', { class: 'msg-sep' }, '→ '), body, ); ul.append(li); } return ul; } function renderInbox() { // Flow page surfaces inbox as a pill that opens the side-panel // flyout. Pill is hidden when empty; click handler below opens // the panel with the freshest list. If the panel is already // showing the inbox view, refresh its body in place so live // messages land without a re-open. const pill = $('inbox-pill'); const pillCount = $('inbox-pill-count'); if (pillCount) pillCount.textContent = String(operatorInbox.length); if (pill) pill.hidden = operatorInbox.length === 0; Panel.refresh('inbox', 'inbox · ' + operatorInbox.length, buildInboxListNode()); } // Wire the inbox pill to open the side-panel flyout with the // operator inbox. const inboxPill = $('inbox-pill'); if (inboxPill) { inboxPill.addEventListener('click', () => { Panel.openNamed('inbox', 'inbox · ' + operatorInbox.length, buildInboxListNode()); }); } // ─── local containers cache (for compose autocomplete) ────────────────── // The compose box's @-mention completion suggests known agent names. // /index.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); }).catch(() => { /* graceful: compose just shows `*` and nothing else */ }); // ─── 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, inbox refresh on operator-bound // traffic, OS notifications). (() => { const flow = $('msgflow'); if (!flow) return; flow.innerHTML = ''; const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19); // Pulse the page banner whenever a broker event lands. (Note: // post-#389 the `.banner` lives in the dashboard's