From a2fe3f2e0172b95966c7844f4ea4c39f9de9a3d1 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 6 Jun 2026 11:16:44 +0200 Subject: [PATCH] feat(flow): collapse back-to-back sent+delivered into one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a message directly wakes its recipient, the broker emits `sent` then `delivered` for the same row id ~instantly, rendering two near-identical lines on the FL0W timeline. Track each recent `sent` row and, when its `delivered` lands within COLLAPSE_SECS (3s), upgrade that row in place (arrow → green ✓, title "sent + delivered") instead of adding a second line. A delivery that arrives later (recipient was busy) still renders as its own row, so genuine delivery latency stays visible. `Sent` and `Delivered` share the same broker row id (dashboard_events.rs), so matching is exact. The recentSent map is bounded to 256 entries. Reply threading is unaffected (msgRowMap still resolves the surviving row). Closes #1472. --- frontend/packages/dashboard/src/flow.js | 43 +++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/frontend/packages/dashboard/src/flow.js b/frontend/packages/dashboard/src/flow.js index 5c5cfdf9..2ad891a0 100644 --- a/frontend/packages/dashboard/src/flow.js +++ b/frontend/packages/dashboard/src/flow.js @@ -170,6 +170,41 @@ import { } // Register this row so future replies can reference it. if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row); + return row; + } + + // Sent→Delivered collapse (#1472). When a message directly wakes its + // recipient the broker emits `sent` then `delivered` for the same row + // id back-to-back, rendering two near-identical lines. We track each + // recent `sent` row and, when its `delivered` lands within + // COLLAPSE_SECS, upgrade that row in place (✓) instead of adding a + // second line. A delivery that arrives *later* (recipient was busy) + // stays a separate row so the latency remains visible. + const COLLAPSE_SECS = 3; + const recentSent = new Map(); // broker row id → { row, at } + function rememberSent(ev, row) { + if (ev.id == null || ev.id <= 0) return; + recentSent.set(ev.id, { row, at: ev.at }); + // Bound the map — drop the oldest entries once it grows past a + // page of un-collapsed sends (insertion order = oldest first). + while (recentSent.size > 256) { + recentSent.delete(recentSent.keys().next().value); + } + } + // Returns true if the delivered event was folded into its sent row. + function collapseDelivered(ev) { + if (ev.id == null || ev.id <= 0) return false; + const s = recentSent.get(ev.id); + if (!s || ev.at - s.at > COLLAPSE_SECS) return false; + const arrow = s.row.querySelector('.msg-arrow'); + if (arrow) arrow.textContent = '✓'; + // Re-style the row as delivered (green ✓) — the collapsed line now + // represents the delivered state; it was sent + delivered as one. + s.row.classList.remove('sent'); + s.row.classList.add('delivered'); + s.row.title = 'sent + delivered'; + recentSent.delete(ev.id); + return true; } // Anchor the `↓ N new` pill in `.flow-main` rather than the // default `.terminal-wrap` parent — see docs/web-ui.md::Per-agent @@ -195,8 +230,12 @@ import { // and won't accidentally share with tabs.js's wider subscribe. streamFactory: openStream, renderers: { - sent: (ev, api) => renderMsg(ev, api, '→'), - delivered: (ev, api) => renderMsg(ev, api, '✓'), + sent: (ev, api) => rememberSent(ev, renderMsg(ev, api, '→')), + delivered: (ev, api) => { + // Fold into the matching sent row when it just happened; + // otherwise render the delivery as its own line. + if (!collapseDelivered(ev)) renderMsg(ev, api, '✓'); + }, // Maintain the local containers cache from the same stream // (compose autocomplete reads from `flowContainers`). The // dashboard's tab renderers aren't on this page, so we don't