feat(flow): filter the timeline by agent
Adds an agent filter to the FL0W header (#1473): a select listing the live agents narrows the timeline to messages involving the chosen agent (matched on `from` OR `to`). Each message row now carries `data-from`/`data-to`; non-matching rows get `.flow-hidden`. New rows pick up the active filter at render time; changing the filter re-scans existing rows. The selection persists in localStorage so a reload or tab-switch keeps the view. The dropdown is populated from the live container list (and stays current on container add/remove); a saved selection survives even if that agent isn't currently listed. Pure frontend; composes with the sent+delivered collapse (the surviving collapsed row keeps its `data-from`/`data-to`). Closes #1473.
This commit is contained in:
parent
d9c7b7fcc1
commit
4c77eefc51
3 changed files with 77 additions and 1 deletions
|
|
@ -163,3 +163,19 @@ body.flow-shell .flow-main-slim {
|
|||
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. */
|
||||
.flow-filter {
|
||||
font-family: inherit;
|
||||
font-size: 0.85em;
|
||||
background: var(--bg-elev);
|
||||
color: var(--amber);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.25em 0.5em;
|
||||
margin-left: auto;
|
||||
}
|
||||
.flow-filter:focus { outline: 1px solid var(--purple); }
|
||||
.msgrow.flow-hidden { display: none; }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
<header class="logs-header">
|
||||
<a class="logs-back" href="/">← dashboard</a>
|
||||
<span class="logs-title">FL0W</span>
|
||||
<select id="flow-agent-filter" class="flow-filter" title="filter timeline by agent" aria-label="filter timeline by agent">
|
||||
<option value="">all agents</option>
|
||||
</select>
|
||||
</header>
|
||||
|
||||
<!-- Operator inbox flyout trigger — count + click → side panel
|
||||
|
|
|
|||
|
|
@ -91,8 +91,56 @@ import {
|
|||
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 (#1473) ───────────────────────────────────────────────
|
||||
// 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
|
||||
|
|
@ -168,6 +216,11 @@ import {
|
|||
} else {
|
||||
row.append(ts, ' ', arrow, ' ', from, ' ', sep, ' ', to, ' ', body);
|
||||
}
|
||||
// Tag with the participants so the agent filter (#1473) can match
|
||||
// on `from`/`to`, and hide the row up-front if a filter is active.
|
||||
row.dataset.from = ev.from;
|
||||
row.dataset.to = ev.to;
|
||||
if (!rowMatchesFilter(ev.from, ev.to)) row.classList.add('flow-hidden');
|
||||
// Register this row so future replies can reference it.
|
||||
if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row);
|
||||
return row;
|
||||
|
|
@ -244,9 +297,13 @@ import {
|
|||
container_state_changed: (ev) => {
|
||||
if (ev.container && ev.container.name) {
|
||||
flowContainers.set(ev.container.name, ev.container);
|
||||
populateAgentFilter(); // keep the filter dropdown current
|
||||
}
|
||||
},
|
||||
container_removed: (ev) => { flowContainers.delete(ev.name); },
|
||||
container_removed: (ev) => {
|
||||
flowContainers.delete(ev.name);
|
||||
populateAgentFilter();
|
||||
},
|
||||
// Drop every other mutation kind silently — without this
|
||||
// they'd fall through to the terminal module's default
|
||||
// renderer and clutter the log with JSON dumps. The dashboard
|
||||
|
|
|
|||
Loading…
Reference in a new issue