refactor(flow): drop operator-inbox UI, now lives on Y3R C4LL (#1483)

The operator inbox moved to the dashboard's Y3R C4LL tab in #1469
(◆ 1NB0X ◆ section, with per-message + mark-all read). FL0W goes back
to being the pure event firehose, so remove its now-redundant inbox UI:

- flow.js: drop the operatorInbox store, inboxAppendFromEvent,
  buildInboxListNode, renderInbox, the inbox-pill click wiring, and the
  onAnyEvent hook that fed them. The side panel was only used for the
  inbox flyout on this page, so drop Panel.bind() + the Panel import too.
- flow.html: remove the inbox pill, the offscreen inbox-section div, and
  the side-panel markup.
- flow.css: remove the .flow-pill* and .flow-inbox-headless rules.
- home.html: Flow tile desc → "live all-agents message firehose".
- docs/web-ui/dashboard.md: drop the FL0W 0PER4T0R 1NB0X section, point
  at Y3R C4LL, and fix the count-pill + /op-send descriptions.

The agent filter, sent→delivered collapse, compose box, and OS
notifications on operator-bound traffic are unaffected.
This commit is contained in:
iris 2026-06-07 19:42:23 +02:00 committed by mara
commit e949129e75
5 changed files with 27 additions and 183 deletions

View file

@ -1,8 +1,9 @@
// /flow.html entry point. 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`.
// /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
@ -13,74 +14,14 @@
import { create as termCreate } from '@hive/shared/terminal.js';
import {
$, el,
Panel, NOTIF,
NOTIF,
appendLinkified,
openStream,
} 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`
@ -144,8 +85,8 @@ import {
// ─── 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).
// side effects (banner pulse, OS notifications on operator-bound
// traffic).
(() => {
const flow = $('msgflow');
if (!flow) return;
@ -310,19 +251,10 @@ import {
// tabs handle these on /index.html via tabs.js.
_default: () => {},
},
// Both history backfill and live frames flow through here, so the
// inbox section ends up populated correctly on first paint and
// updated thereafter — no /api/state refetch needed for inbox
// freshness.
onAnyEvent: (ev /* , { fromHistory } */) => {
if (inboxAppendFromEvent(ev)) renderInbox();
},
// Re-sync the local containers cache on every SSE (re)connect.
// Live mutation events that fired during a disconnect window
// are never replayed, so without this the compose autocomplete
// could drift stale. We don't try to recover missed broker rows
// here — operator inbox briefly stales on reconnect; the
// history-replay covers the next page load.
// could drift stale.
onStreamOpen: () => {
fetch('/api/state').then((r) => r.ok ? r.json() : null).then((s) => {
if (!s || !Array.isArray(s.containers)) return;
@ -448,8 +380,8 @@ import {
input.disabled = true;
try {
// /op-send returns 200. The SSE channel carries the resulting
// MessageEvent → the terminal renders the sent row + the
// inbox updates on its own; no /api/state refetch needed.
// MessageEvent → the terminal renders the sent row on its own;
// no /api/state refetch needed.
const resp = await fetch('/op-send', {
method: 'POST',
body: new URLSearchParams(fd),