From 45cdd62116e562bf8eca80bb05061c6404664889 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 15 Jul 2026 01:18:15 +0200 Subject: [PATCH] fix(#2465): render multi-agent DAG nodes as per-agent subgraph lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderQueueEntry flattened entry.nodes into one arrow-joined chain regardless of which agent each node belongs to. A multi-agent DAG (e.g. hivectl restart --graceful with several agents) runs independent per-agent subgraphs concurrently, no cross-agent deps, so joining them all into one sequential-looking chain misrepresented the actual DAG shape (mara's report: 'expected one dag that forks after the start node into the per agent sub dags'). Group nodes by n.agent (stable, first-seen order) and render one .rqe-nodes line per agent, with a small agent-label chip when the DAG spans more than one. Single-agent DAGs (the common case) collapse back to exactly the prior one-line render — no visible change there. --- frontend/packages/dashboard/src/builds.js | 61 +++++++++++++------ .../dashboard/src/system-sections.css | 4 ++ 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/frontend/packages/dashboard/src/builds.js b/frontend/packages/dashboard/src/builds.js index 80314b4d..075236e3 100644 --- a/frontend/packages/dashboard/src/builds.js +++ b/frontend/packages/dashboard/src/builds.js @@ -307,27 +307,52 @@ function renderQueueEntry(entry, _byId, isChild) { // state glyph, live step label, and build-log link. This is the // node-aware render that makes queue jumps / partial progress // visible (e.g. reconcile running while swap failed). + // + // A DAG can span multiple agents (e.g. a hive-wide restart) as + // independent per-agent subgraphs with no cross-agent deps — they run + // concurrently, not sequentially. Joining every node in array order + // with a single `→` chain misrepresents that as one long sequential + // pipeline. Group by `n.agent` (stable, first-seen order) and render + // each agent's subgraph on its own line instead; single-agent DAGs + // (the common case) collapse back to exactly the old one-line render. const nodes = entry.nodes || []; if (nodes.length) { - const chain = el('div', { class: 'rqe-nodes' }); - nodes.forEach((n, i) => { - if (i > 0) chain.append(el('span', { class: 'rqe-node-arrow' }, ' → ')); - const chip = el('span', { - class: 'rqe-node rqe-node-' + n.state, - title: n.kind + ' · ' + n.state + (n.error ? ' — ' + n.error : ''), - }, - (QUEUE_STATE_GLYPH[n.state] || '?') + ' ' + (NODE_KIND_LABEL[n.kind] || n.kind)); - chain.append(chip); - if (n.build_log_id != null) { - chain.append(el('a', { - class: 'rqe-log-link rqe-node-log', - href: '/builds.html?id=' + n.build_log_id + '#buildlogs', - target: '_blank', - title: 'view build log #' + n.build_log_id, - }, '⎙')); + const groups = []; + const groupByAgent = new Map(); + for (const n of nodes) { + let g = groupByAgent.get(n.agent); + if (!g) { + g = []; + groupByAgent.set(n.agent, g); + groups.push([n.agent, g]); } - }); - li.append(chain); + g.push(n); + } + const multiAgent = groups.length > 1; + for (const [agent, groupNodes] of groups) { + const chain = el('div', { class: 'rqe-nodes' }); + if (multiAgent) { + chain.append(el('code', { class: 'rqe-node-agent-label' }, agent)); + } + groupNodes.forEach((n, i) => { + if (i > 0) chain.append(el('span', { class: 'rqe-node-arrow' }, ' → ')); + const chip = el('span', { + class: 'rqe-node rqe-node-' + n.state, + title: n.kind + ' · ' + n.state + (n.error ? ' — ' + n.error : ''), + }, + (QUEUE_STATE_GLYPH[n.state] || '?') + ' ' + (NODE_KIND_LABEL[n.kind] || n.kind)); + chain.append(chip); + if (n.build_log_id != null) { + chain.append(el('a', { + class: 'rqe-log-link rqe-node-log', + href: '/builds.html?id=' + n.build_log_id + '#buildlogs', + target: '_blank', + title: 'view build log #' + n.build_log_id, + }, '⎙')); + } + }); + li.append(chain); + } } const running = runningNode(entry); if (running && running.step) { diff --git a/frontend/packages/dashboard/src/system-sections.css b/frontend/packages/dashboard/src/system-sections.css index 019b9216..306eb0d1 100644 --- a/frontend/packages/dashboard/src/system-sections.css +++ b/frontend/packages/dashboard/src/system-sections.css @@ -161,6 +161,10 @@ .rqe-node-cancelled { opacity: 0.55; text-decoration: line-through; } .rqe-node-arrow { color: var(--muted); } .rqe-node-log { margin-left: 0.1em; text-decoration: none; } +/* Per-agent subgraph label, shown only when a DAG's nodes span more than + one agent (independent concurrent subgraphs, one `.rqe-nodes` line each — + see renderQueueEntry in builds.js). */ +.rqe-node-agent-label { color: var(--amber); margin-right: 0.3em; } .rqe-step { flex-basis: 100%; margin: 0.1em 0 0 1.8em;