diff --git a/frontend/packages/dashboard/src/builds.js b/frontend/packages/dashboard/src/builds.js index 8b0768bb..80314b4d 100644 --- a/frontend/packages/dashboard/src/builds.js +++ b/frontend/packages/dashboard/src/builds.js @@ -178,76 +178,6 @@ function entryAgents(entry) { return seen.join(','); } -// Split a DAG's nodes into its actual weakly-connected subgraphs, using the -// `deps` edges the backend provides — not an inferred heuristic like -// grouping by `n.agent`. A DAG with independent subgraphs (e.g. a -// multi-agent restart, no cross-agent deps) naturally splits into one -// component per subgraph; a single connected DAG stays one component. Each -// component's nodes come back topo-sorted (Kahn's algorithm, falling back to -// original array order for ties) so a chain renders in actual dependency -// order rather than raw array order. -function nodeComponents(nodes) { - if (!nodes.length) return []; - const byId = new Map(nodes.map((n) => [n.id, n])); - const adj = new Map(nodes.map((n) => [n.id, new Set()])); // undirected, for component split - for (const n of nodes) { - for (const d of n.deps || []) { - if (!byId.has(d)) continue; // dep outside this node set (shouldn't happen) - adj.get(n.id).add(d); - adj.get(d).add(n.id); - } - } - const seen = new Set(); - const components = []; - for (const n of nodes) { - if (seen.has(n.id)) continue; - const compIds = []; - const stack = [n.id]; - seen.add(n.id); - while (stack.length) { - const id = stack.pop(); - compIds.push(id); - for (const nb of adj.get(id)) { - if (!seen.has(nb)) { - seen.add(nb); - stack.push(nb); - } - } - } - const compSet = new Set(compIds); - const compNodes = nodes.filter((cn) => compSet.has(cn.id)); - // Topo-sort within the component via its actual `deps` edges (directed). - const indeg = new Map(compNodes.map((cn) => [cn.id, 0])); - for (const cn of compNodes) { - for (const d of cn.deps || []) { - if (compSet.has(d)) indeg.set(cn.id, indeg.get(cn.id) + 1); - } - } - const ordered = []; - const ready = compNodes.filter((cn) => indeg.get(cn.id) === 0); - const remaining = new Map(compNodes.map((cn) => [cn.id, cn])); - while (ready.length) { - const cn = ready.shift(); - if (!remaining.has(cn.id)) continue; - remaining.delete(cn.id); - ordered.push(cn); - for (const other of compNodes) { - if ((other.deps || []).includes(cn.id) && remaining.has(other.id)) { - indeg.set(other.id, indeg.get(other.id) - 1); - if (indeg.get(other.id) === 0) ready.push(other); - } - } - } - // Any leftover (cycle, or a dep outside the node set) — append in - // original order rather than dropping nodes from the display. - for (const cn of compNodes) { - if (remaining.has(cn.id)) ordered.push(cn); - } - components.push(ordered); - } - return components; -} - function rebuildQueueEntryFingerprint(entry, isChild) { return JSON.stringify({ state: entry.state, @@ -377,48 +307,27 @@ 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's actual shape is its `deps` graph, not an incidental property - // like `n.agent` — render *that* structure (via nodeComponents, which - // splits on `deps` and topo-sorts each piece), not a heuristic grouping. - // A DAG made of independent subgraphs (e.g. a multi-agent restart, no - // cross-agent deps) naturally comes back as multiple components and - // gets one line each; a single connected DAG (the common case) stays - // one component and renders exactly as the old one-line chain did. const nodes = entry.nodes || []; if (nodes.length) { - const components = nodeComponents(nodes); - const multi = components.length > 1; - for (const compNodes of components) { - const chain = el('div', { class: 'rqe-nodes' }); - if (multi) { - // Label from the component's own nodes (informational only — the - // split itself came from `deps`, not from agent). - const agents = []; - for (const n of compNodes) { - if (n.agent && !agents.includes(n.agent)) agents.push(n.agent); - } - chain.append(el('code', { class: 'rqe-node-agent-label' }, agents.join(','))); + 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, + }, '⎙')); } - compNodes.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); - } + }); + 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 306eb0d1..019b9216 100644 --- a/frontend/packages/dashboard/src/system-sections.css +++ b/frontend/packages/dashboard/src/system-sections.css @@ -161,10 +161,6 @@ .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;