diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md
index 3baa5a59..1d19c2c3 100644
--- a/docs/web-ui/dashboard.md
+++ b/docs/web-ui/dashboard.md
@@ -18,9 +18,9 @@ swap.
`◆ P33RS ◆` (hidden when `swarm.peers` is empty),
`◆ M4TR1X ◆ →` (optional page link, see below), `◆ FL0W ◆ →`
(page link), and `◆ S3TT1NGS ◆`. Count pills on SW4RM (container
- count), Y3R C4LL (pending approvals + questions), and SCH3DUL3S
- (active schedules); FL0W pill mirrors the operator inbox length
- (hidden when zero); P33RS and S3TT1NGS have no count. The M4TR1X →
+ count), Y3R C4LL (pending approvals + questions + unread operator
+ messages), and SCH3DUL3S (active schedules); P33RS and S3TT1NGS
+ have no count. The M4TR1X →
entry is hidden when `services.hyperhive.matrix.gui.enable` is off
(defaults to `matrix.enable`) so operators without the matrix GUI
on don't see a dead link — tabs.js gates the `hidden` attribute on
@@ -368,7 +368,7 @@ Operator-local preferences. State lives in the browser's
`localStorage` — preferences do NOT sync between devices and
do NOT survive a profile wipe. Today the tab holds one section
(browser notifications); future preferences (theme, density,
-inbox-pill threshold, etc.) land here as sibling `
` blocks
+etc.) land here as sibling `
` blocks
under the same ``.
**◇ browser notifications** — `🔔 enable notifications` button when
@@ -415,11 +415,9 @@ as the dashboard so the tab strip remains visible; SW4RM / Y3R
C4LL / SYST3M / SCH3DUL3S are cross-page links back to `/#`,
and the FL0W entry is marked active (`aria-current="page"`).
-**0PER4T0R 1NB0X** — recent messages addressed to `operator`,
-derived client-side from the dashboard event stream. Cold load
-seeds from `/dashboard/history`'s 200-message backfill; subsequent
-`sent` events with `to == "operator"` are appended live. Cap 50,
-newest-first.
+The operator inbox is **not** on this page — it lives on the
+dashboard's Y3R C4LL tab (◆ 1NB0X ◆ section, with per-message and
+mark-all read). FL0W stays the pure event firehose.
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
Cold load backfills the last ~200 messages from `/dashboard/history`;
@@ -437,8 +435,7 @@ terminal-style compose box: `@name` picks the recipient (sticky via
localStorage; auto-complete from the live container list, Tab/Enter
to confirm; `@*` broadcasts). `POST /op-send` drops
`{from:"operator", to, body}` into the broker; the resulting SSE
-frame re-renders both the terminal row and the inbox section.
-Manager is addressed as `@root`.
+frame re-renders the terminal row. Manager is addressed as `@root`.
## L0GS page (`/logs.html`)
@@ -805,9 +802,8 @@ that's a browser-level decision, not ours.
- `POST /op-send` (`to=`, `body=`) — drop an
operator-authored message into ``'s inbox. `to=*` fans
out to every registered agent. Returns 200; the broker
- `Sent` event re-renders both the message-flow terminal and
- the operator inbox without a snapshot refetch. Used by the
- compose textbox under MESS4GE FL0W.
+ `Sent` event re-renders the message-flow terminal without a
+ snapshot refetch. Used by the compose textbox under MESS4GE FL0W.
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
headers for one agent, newest first. Returns
`Vec` (JSON): `id`, `agent`, `kind`, `cmdline`,
diff --git a/frontend/packages/dashboard/src/flow.css b/frontend/packages/dashboard/src/flow.css
index 7818c530..954691b9 100644
--- a/frontend/packages/dashboard/src/flow.css
+++ b/frontend/packages/dashboard/src/flow.css
@@ -1,8 +1,7 @@
/* ─── /flow.html — full-page chat ─────────────────────────────────
- See docs/web-ui.md::FL0W page for the page-vs-pane rationale
- and the inbox-flyout-as-pill ergonomics. Shape mirrors the
- per-agent live page (frosted-glass header + composer, full-
- viewport terminal). */
+ See docs/web-ui.md::FL0W page for the page-vs-pane rationale.
+ Shape mirrors the per-agent live page (frosted-glass header +
+ composer, full-viewport terminal). */
:root {
/* Approximate height of the flow chrome (tabbar + dashboard-chrome
@@ -59,47 +58,6 @@ body.flow-shell .tabbar .tab.active.tab-link {
border-color: var(--purple-dim);
box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4);
}
-/* Inbox pill — operator inbox flyout trigger. Sits right under the
- header so it stays in the operator's gaze without crowding the
- chat. Same shape as the agent page's header pills. */
-.flow-pill {
- position: fixed;
- top: calc(var(--flow-header-h) + 0.8em);
- right: 1em;
- z-index: 25;
- background: var(--bg-elev);
- border: 1px solid var(--purple-dim);
- color: var(--fg);
- font-family: inherit;
- font-size: 0.85em;
- letter-spacing: 0.04em;
- border-radius: 999px;
- padding: 0.3em 0.8em;
- display: inline-flex;
- align-items: center;
- gap: 0.5em;
- cursor: pointer;
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
- transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
-}
-.flow-pill:hover {
- border-color: var(--purple);
- color: var(--purple);
- box-shadow: 0 0 12px -2px var(--purple);
-}
-.flow-pill-icon { font-size: 1.05em; line-height: 1; }
-.flow-pill-label { color: var(--muted); }
-.flow-pill-count {
- background: rgba(250, 179, 135, 0.18);
- color: var(--amber);
- border-radius: 999px;
- padding: 0 0.5em;
- min-width: 1.6em;
- text-align: center;
- font-weight: bold;
- font-variant-numeric: tabular-nums;
-}
-
.flow-main {
position: absolute;
top: 0;
@@ -159,11 +117,6 @@ body.flow-shell .flow-main-slim {
padding: 0.4em 1em;
}
-/* Hidden inbox section on the flow page — the renderInbox path
- wants `#inbox-section` in the DOM (legacy contract), but we
- surface the messages via the pill/flyout instead. */
-.flow-inbox-headless { display: none !important; }
-
/* Agent filter (#1473): a select in the FL0W header narrows the
timeline to messages involving one agent. Non-matching rows get
`.flow-hidden`; the select reuses the journal-unit control look. */
diff --git a/frontend/packages/dashboard/src/flow.html b/frontend/packages/dashboard/src/flow.html
index c98f3960..8535b569 100644
--- a/frontend/packages/dashboard/src/flow.html
+++ b/frontend/packages/dashboard/src/flow.html
@@ -23,16 +23,6 @@
-
-
-
@@ -55,34 +45,7 @@
-
-
-
loading…
-
-
-
-
-
-
-
-
-
diff --git a/frontend/packages/dashboard/src/flow.js b/frontend/packages/dashboard/src/flow.js
index 969972cf..cdb46664 100644
--- a/frontend/packages/dashboard/src/flow.js
+++ b/frontend/packages/dashboard/src/flow.js
@@ -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),
diff --git a/frontend/packages/dashboard/src/home.html b/frontend/packages/dashboard/src/home.html
index 10fe7695..ace213d4 100644
--- a/frontend/packages/dashboard/src/home.html
+++ b/frontend/packages/dashboard/src/home.html
@@ -31,7 +31,7 @@
Flow
- all-agents chat + operator inbox
+ live all-agents message firehose