From 4b122b3f17afd0e8a81f1da931afccf4105529da Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 01:42:14 +0000 Subject: [PATCH] fix(harness): wrap isolated graph agents Preserve layered connected components while packing zero-degree agents into a deterministic labeled grid below them. Closes: SAP-2966 --- .changeset/calm-graphs-wrap.md | 5 + packages/harness/web/e2e/project-axis.spec.ts | 3 + packages/harness/web/e2e/smoke.spec.ts | 3 + .../web/src/components/SystemGraphCanvas.tsx | 19 ++ .../web/src/lib/system-graph-layout.test.ts | 116 +++++++++ .../web/src/lib/system-graph-layout.ts | 242 +++++++++++++++++- packages/harness/web/src/styles.css | 13 + 7 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 .changeset/calm-graphs-wrap.md diff --git a/.changeset/calm-graphs-wrap.md b/.changeset/calm-graphs-wrap.md new file mode 100644 index 000000000..cf8338592 --- /dev/null +++ b/.changeset/calm-graphs-wrap.md @@ -0,0 +1,5 @@ +--- +"@sapiom/harness": patch +--- + +Agent Studio now packs agents without detected relationships into a labeled grid below connected graph components, keeping sparse project maps at a usable height. diff --git a/packages/harness/web/e2e/project-axis.spec.ts b/packages/harness/web/e2e/project-axis.spec.ts index 8d793bea1..e9617da95 100644 --- a/packages/harness/web/e2e/project-axis.spec.ts +++ b/packages/harness/web/e2e/project-axis.spec.ts @@ -232,6 +232,9 @@ test.describe("multi-root", () => { await expect( page.getByTestId("system-graph-node-ads-worker"), ).toBeVisible(); + await expect(page.getByTestId("system-graph-isolated-label")).toHaveText( + "2 agents · no detected relationships", + ); await expect(page.getByTestId("system-graph-node-gateway")).toHaveCount(0); }); diff --git a/packages/harness/web/e2e/smoke.spec.ts b/packages/harness/web/e2e/smoke.spec.ts index 6447ff490..6e66a16cd 100644 --- a/packages/harness/web/e2e/smoke.spec.ts +++ b/packages/harness/web/e2e/smoke.spec.ts @@ -528,6 +528,9 @@ test.describe("three-zone IA (rail explorer, tab strip, right pane)", () => { await expect( page.getByTestId("system-graph-node-standalone"), ).toContainText("Standalone"); + await expect(page.getByTestId("system-graph-isolated-label")).toHaveText( + "1 agent · no detected relationships", + ); await expect( page.getByTestId("system-graph-edge-agent:research-agent:growth"), ).toContainText("blocking + async"); diff --git a/packages/harness/web/src/components/SystemGraphCanvas.tsx b/packages/harness/web/src/components/SystemGraphCanvas.tsx index 69f1ec4d8..0b70c00c6 100644 --- a/packages/harness/web/src/components/SystemGraphCanvas.tsx +++ b/packages/harness/web/src/components/SystemGraphCanvas.tsx @@ -397,6 +397,25 @@ export function SystemGraphCanvas({ ))} + {layout.isolatedSections.map((section) => ( +

+ {section.label} +

+ ))} + {layout.nodes.map((placed) => { const graphNode = graphNodes.get(placed.id)!; const navigable = navigableAgentKeys.has(graphNode.agentKey); diff --git a/packages/harness/web/src/lib/system-graph-layout.test.ts b/packages/harness/web/src/lib/system-graph-layout.test.ts index b192fa117..c3ff8b5b8 100644 --- a/packages/harness/web/src/lib/system-graph-layout.test.ts +++ b/packages/harness/web/src/lib/system-graph-layout.test.ts @@ -12,6 +12,7 @@ import { type SystemGraphLayout, type SystemGraphNodeGroup, } from "./system-graph-layout"; +import { fitSystemGraphView } from "./system-graph-viewport"; const node = (id: string) => ({ id, agentKey: id, label: id.toUpperCase() }); const edge = ( @@ -36,6 +37,11 @@ function byId(layout: SystemGraphLayout, id: string) { return placed; } +function onlyIsolatedSection(layout: SystemGraphLayout) { + expect(layout.isolatedSections).toHaveLength(1); + return layout.isolatedSections[0]!; +} + function rectanglesOverlap( left: { x: number; y: number; width: number; height: number }, right: { x: number; y: number; width: number; height: number }, @@ -94,6 +100,7 @@ describe("layoutSystemGraph", () => { expect(byId(layout, "a").x).toBeLessThan(byId(layout, "b").x); expect(byId(layout, "b").x).toBeLessThan(byId(layout, "c").x); + expect(layout.isolatedSections).toEqual([]); }); it("keeps fan-out targets together and fan-in targets after every caller", () => { @@ -142,6 +149,9 @@ describe("layoutSystemGraph", () => { }); it("keeps disconnected components and isolated agents exactly once", () => { + const connected = layoutSystemGraph( + graph(["a", "b", "x", "y"], [edge("a", "b"), edge("x", "y")]), + ); const layout = layoutSystemGraph( graph(["isolated", "a", "b", "x", "y"], [edge("a", "b"), edge("x", "y")]), ); @@ -153,12 +163,107 @@ describe("layoutSystemGraph", () => { "x", "y", ]); + expect( + layout.nodes.filter((candidate) => candidate.id !== "isolated"), + ).toEqual(connected.nodes); + expect(layout.edges).toEqual(connected.edges); expect( new Set(layout.nodes.map((candidate) => candidate.componentId)).size, ).toBe(3); + const isolatedSection = onlyIsolatedSection(layout); + expect(isolatedSection).toMatchObject({ + groupId: null, + count: 1, + label: "1 agent · no detected relationships", + }); + expect(isolatedSection.labelBounds.y).toBeGreaterThan( + Math.max( + byId(layout, "b").y + SYSTEM_GRAPH_NODE_HEIGHT, + byId(layout, "y").y + SYSTEM_GRAPH_NODE_HEIGHT, + ), + ); + expect(byId(layout, "isolated").y).toBeGreaterThan( + isolatedSection.labelBounds.y + isolatedSection.labelBounds.height, + ); + expect( + isolatedSection.labelBounds.x + isolatedSection.labelBounds.width, + ).toBeLessThanOrEqual(layout.bounds.width); + expect( + isolatedSection.labelBounds.y + isolatedSection.labelBounds.height, + ).toBeLessThanOrEqual(layout.bounds.height); expectNodesNotToOverlap(layout); }); + it("keeps a 77-agent, 4-edge graph bounded while preserving its connected layout", () => { + const nodeIds = Array.from( + { length: 77 }, + (_, index) => `agent-${index.toString().padStart(2, "0")}`, + ); + const connectedIds = nodeIds.slice(0, 5); + const edges = connectedIds + .slice(0, -1) + .map((id, index) => edge(id, connectedIds[index + 1]!)); + const connected = layoutSystemGraph(graph(connectedIds, edges)); + const sparse = layoutSystemGraph(graph(nodeIds, edges)); + + expect(sparse.nodes.map((candidate) => candidate.id)).toEqual(nodeIds); + expect(sparse.edges).toHaveLength(4); + expect(onlyIsolatedSection(sparse)).toMatchObject({ + groupId: null, + count: 72, + label: "72 agents · no detected relationships", + }); + expect( + sparse.nodes.filter((candidate) => connectedIds.includes(candidate.id)), + ).toEqual(connected.nodes); + expect(sparse.edges).toEqual(connected.edges); + + const isolated = sparse.nodes.filter( + (candidate) => !connectedIds.includes(candidate.id), + ); + expect( + new Set(isolated.map((candidate) => candidate.x)).size, + ).toBeGreaterThan(1); + expect( + new Set(isolated.map((candidate) => candidate.y)).size, + ).toBeGreaterThan(1); + expect(sparse.bounds.height).toBeLessThanOrEqual(1_200); + expect( + fitSystemGraphView(sparse.bounds, { width: 1_200, height: 800 }, 16).zoom, + ).toBeGreaterThanOrEqual(0.5); + expectNodesNotToOverlap(sparse); + expectEdgesNotToCrossCards(sparse); + + expect( + layoutSystemGraph(graph([...nodeIds].reverse(), [...edges].reverse())), + ).toEqual(sparse); + }); + + it("keeps fallback edge labels above the isolated section without rerouting them", () => { + const connectedIds = Array.from( + { length: 5 }, + (_, index) => `connected-${index}`, + ); + const denseEdges = connectedIds.flatMap((from) => + connectedIds.filter((to) => to !== from).map((to) => edge(from, to)), + ); + const connected = layoutSystemGraph(graph(connectedIds, denseEdges)); + const sparse = layoutSystemGraph( + graph([...connectedIds, "isolated"], denseEdges), + ); + + expect( + sparse.nodes.filter((candidate) => candidate.id !== "isolated"), + ).toEqual(connected.nodes); + expect(sparse.edges).toEqual(connected.edges); + const isolatedSection = onlyIsolatedSection(sparse); + expect( + sparse.edges.some((candidate) => + rectanglesOverlap(candidate.labelBounds, isolatedSection.labelBounds), + ), + ).toBe(false); + }); + it("groups dual-mode records into one connector with stable mode semantics", () => { const layout = layoutSystemGraph( graph( @@ -258,6 +363,7 @@ describe("layoutSystemGraph", () => { routed.labelBounds.y + routed.labelBounds.height, ).toBeLessThanOrEqual(layout.bounds.height); } + expect(layout.isolatedSections).toEqual([]); }); it("is deeply deterministic when node and edge input order changes", () => { @@ -374,6 +480,15 @@ describe("layoutSystemGraph with groups", () => { ).toBe(false); } } + const isolatedSection = onlyIsolatedSection(layout); + expect(isolatedSection).toMatchObject({ + groupId: "group:ungrouped", + count: 1, + label: "1 agent · no detected relationships", + }); + expect( + contains(boxOf(layout, "Ungrouped"), isolatedSection.labelBounds), + ).toBe(true); expectNodesNotToOverlap(layout); }); @@ -467,6 +582,7 @@ describe("layoutSystemGraph with groups", () => { crossesGroup: true, }); expect(layout.edges[0]!.path).not.toContain("NaN"); + expect(layout.isolatedSections).toEqual([]); }); it("wraps a container of unconnected agents instead of stacking them", () => { diff --git a/packages/harness/web/src/lib/system-graph-layout.ts b/packages/harness/web/src/lib/system-graph-layout.ts index 3774a14ce..869896e3f 100644 --- a/packages/harness/web/src/lib/system-graph-layout.ts +++ b/packages/harness/web/src/lib/system-graph-layout.ts @@ -19,6 +19,7 @@ const LAYOUT_PADDING = 32; const PORT_LIMIT = 24; const PORT_STEP = 8; const LABEL_HEIGHT = 16; +const ISOLATED_SECTION_LABEL_GAP = 12; /** * Inside a container, around everything it holds. @@ -142,12 +143,23 @@ export interface SystemGraphLayoutEdge { crossesGroup: boolean; } +export interface SystemGraphIsolatedSection { + /** The group whose isolated cards this label describes, or null when the + * graph was laid out without group information. */ + groupId: string | null; + count: number; + label: string; + labelBounds: SystemGraphLabelBounds; +} + export interface SystemGraphLayout { nodes: SystemGraphLayoutNode[]; edges: SystemGraphLayoutEdge[]; /** Empty when laid out without groups — the map draws no chrome it was not * given a reason to draw. */ groups: SystemGraphLayoutGroup[]; + /** One labelled grid per region that contains globally degree-zero agents. */ + isolatedSections: SystemGraphIsolatedSection[]; bounds: { width: number; height: number }; } @@ -469,24 +481,43 @@ function shelfPack( } /** - * Every component of one region, ranked internally and then shelf-packed. + * Every non-isolated component of one region, ranked internally and then + * shelf-packed, followed by one wrapped grid for the region's isolated nodes. * - * Components are laid out at their own origin first because packing needs each - * box's size up front — the vertical stack this replaces never had to know one. + * Isolation is classified against the WHOLE graph before regions are formed. + * An agent whose only edge crosses a group boundary still has a detected + * relationship and must never be filed under the isolated label. */ function placeRegionNodes( topology: SystemGraphTopology, groupId: string | null, + globallyIsolatedNodeIds: ReadonlySet, ): { nodes: SystemGraphLayoutNode[]; componentBoxes: Map; componentByNode: Map; strongByNode: Map; + isolatedSection: SystemGraphIsolatedSection | null; + isolatedNodeIds: ReadonlySet; } { const componentByNode = new Map(); const strongByNode = new Map(); - - const laid = topology.components.map((component) => { + const isolatedNodes = topology.components + .flatMap((component) => + component.stronglyConnected.flatMap((strong) => + strong.nodeIds + .filter((id) => globallyIsolatedNodeIds.has(id)) + .map((id) => ({ id, component })), + ), + ) + .sort((left, right) => compareIds(left.id, right.id)); + const isolatedNodeIds = new Set(isolatedNodes.map((node) => node.id)); + // A degree-zero node is necessarily its own weak component, so removing its + // component cannot disturb the ranked geometry of any connected component. + const nonIsolatedComponents = topology.components.filter((component) => + component.nodeIds.some((id) => !isolatedNodeIds.has(id)), + ); + const laid = nonIsolatedComponents.map((component) => { const byRank = new Map(); for (const strong of component.stronglyConnected) { const rankNodes = byRank.get(strong.rank) ?? []; @@ -547,8 +578,65 @@ function placeRegionNodes( maxY: at.y + entry.height, }); }); + + let isolatedSection: SystemGraphIsolatedSection | null = null; + if (isolatedNodes.length > 0) { + const columnStride = SYSTEM_GRAPH_NODE_WIDTH + SYSTEM_GRAPH_SLOT_GAP; + const rowStride = SYSTEM_GRAPH_NODE_HEIGHT + SYSTEM_GRAPH_SLOT_GAP; + // Balance physical width and height (cards are much wider than tall) while + // retaining the map's landscape packing target. Keep the decision + // independent of the viewport so the layout is stable. + const columns = Math.min( + isolatedNodes.length, + Math.max( + 2, + Math.ceil( + Math.sqrt( + (isolatedNodes.length * rowStride * SHELF_ASPECT) / columnStride, + ), + ), + ), + ); + const gridWidth = + columns * SYSTEM_GRAPH_NODE_WIDTH + + Math.max(0, columns - 1) * SYSTEM_GRAPH_SLOT_GAP; + const label = isolatedSectionLabel(isolatedNodes.length); + const labelY = packed.height > 0 ? packed.height + COMPONENT_GAP : 0; + const gridY = labelY + LABEL_HEIGHT + ISOLATED_SECTION_LABEL_GAP; + + isolatedNodes.forEach(({ id, component }, index) => { + nodes.push({ + id, + x: (index % columns) * columnStride, + y: Math.floor(index / columns) * rowStride + gridY, + width: SYSTEM_GRAPH_NODE_WIDTH, + height: SYSTEM_GRAPH_NODE_HEIGHT, + componentId: component.id, + groupId, + }); + }); + isolatedSection = { + groupId, + count: isolatedNodes.length, + label, + labelBounds: { + x: 0, + y: labelY, + width: Math.max(gridWidth, labelWidth(label)), + height: LABEL_HEIGHT, + }, + }; + } + nodes.sort((left, right) => compareIds(left.id, right.id)); - return { nodes, componentBoxes, componentByNode, strongByNode }; + return { + nodes, + componentBoxes, + componentByNode, + strongByNode, + isolatedSection, + isolatedNodeIds, + }; } function spreadPortOffsets( @@ -594,6 +682,10 @@ function labelForModes(modes: readonly AgentInvocationMode[]): string { return modes.length === 2 ? "blocking + async" : modes[0]!; } +function isolatedSectionLabel(count: number): string { + return `${count} ${count === 1 ? "agent" : "agents"} · no detected relationships`; +} + function labelWidth(label: string): number { return Math.max(40, label.length * 6.5 + 8); } @@ -975,6 +1067,58 @@ const translateRouted = ( }, }); +const translateIsolatedSection = ( + section: SystemGraphIsolatedSection, + dx: number, + dy: number, +): SystemGraphIsolatedSection => ({ + ...section, + labelBounds: { + ...section.labelBounds, + x: round(section.labelBounds.x + dx), + y: round(section.labelBounds.y + dy), + }, +}); + +/** Keep the explanatory label below every routed primitive without allowing + * provisional isolated cards to influence edge-label placement. */ +function placeIsolatedSectionBelowEdges( + nodes: SystemGraphLayoutNode[], + edges: readonly RoutedEdge[], + isolatedSection: SystemGraphIsolatedSection | null, + isolatedNodeIds: ReadonlySet, +): { + nodes: SystemGraphLayoutNode[]; + isolatedSection: SystemGraphIsolatedSection | null; +} { + if (!isolatedSection || edges.length === 0) { + return { nodes, isolatedSection }; + } + let routedBottom = Number.NEGATIVE_INFINITY; + for (const edge of edges) { + for (const point of edge.points) { + routedBottom = Math.max(routedBottom, point.y); + } + routedBottom = Math.max( + routedBottom, + edge.labelBounds.y + edge.labelBounds.height, + ); + } + const targetY = Math.max( + isolatedSection.labelBounds.y, + routedBottom + COMPONENT_GAP, + ); + const dy = round(targetY - isolatedSection.labelBounds.y); + if (dy === 0) return { nodes, isolatedSection }; + + return { + nodes: nodes.map((node) => + isolatedNodeIds.has(node.id) ? translateNode(node, 0, dy) : node, + ), + isolatedSection: translateIsolatedSection(isolatedSection, 0, dy), + }; +} + interface Region { id: string; /** null for the single implicit region of an ungrouped layout — the one case @@ -1051,6 +1195,7 @@ interface LaidRegion extends Sized { label: string | null; nodes: SystemGraphLayoutNode[]; edges: RoutedEdge[]; + isolatedSection: SystemGraphIsolatedSection | null; insetX: number; insetY: number; } @@ -1064,7 +1209,11 @@ interface LaidRegion extends Sized { * from the cards alone would let a cycle gutter or a displaced label spill into * the neighbouring system. */ -function layoutRegion(graph: SystemGraph, region: Region): LaidRegion { +function layoutRegion( + graph: SystemGraph, + region: Region, + globallyIsolatedNodeIds: ReadonlySet, +): LaidRegion { const members = new Set(region.nodeIds); const subgraph: SystemGraph = { ...graph, @@ -1076,20 +1225,30 @@ function layoutRegion(graph: SystemGraph, region: Region): LaidRegion { const placed = placeRegionNodes( analyzeSystemGraph(subgraph), region.label === null ? null : region.id, + globallyIsolatedNodeIds, ); const labels: SystemGraphLabelBounds[] = []; + const connectedNodes = placed.nodes.filter( + (node) => !placed.isolatedNodeIds.has(node.id), + ); const routed = routeEdges( groupSystemGraphEdges(subgraph.edges), - placed.nodes, + connectedNodes, placed.componentBoxes, placed.componentByNode, placed.strongByNode, labels, ); + const settled = placeIsolatedSectionBelowEdges( + placed.nodes, + routed, + placed.isolatedSection, + placed.isolatedNodeIds, + ); const xs: number[] = []; const ys: number[] = []; - for (const node of placed.nodes) { + for (const node of settled.nodes) { xs.push(node.x, node.x + node.width); ys.push(node.y, node.y + node.height); } @@ -1101,6 +1260,18 @@ function layoutRegion(graph: SystemGraph, region: Region): LaidRegion { xs.push(edge.labelBounds.x, edge.labelBounds.x + edge.labelBounds.width); ys.push(edge.labelBounds.y, edge.labelBounds.y + edge.labelBounds.height); } + if (settled.isolatedSection) { + xs.push( + settled.isolatedSection.labelBounds.x, + settled.isolatedSection.labelBounds.x + + settled.isolatedSection.labelBounds.width, + ); + ys.push( + settled.isolatedSection.labelBounds.y, + settled.isolatedSection.labelBounds.y + + settled.isolatedSection.labelBounds.height, + ); + } const minX = Math.min(...xs); const minY = Math.min(...ys); const contentWidth = round(Math.max(...xs) - minX); @@ -1109,8 +1280,11 @@ function layoutRegion(graph: SystemGraph, region: Region): LaidRegion { return { id: region.id, label: region.label, - nodes: placed.nodes.map((node) => translateNode(node, -minX, -minY)), + nodes: settled.nodes.map((node) => translateNode(node, -minX, -minY)), edges: routed.map((edge) => translateRouted(edge, -minX, -minY)), + isolatedSection: settled.isolatedSection + ? translateIsolatedSection(settled.isolatedSection, -minX, -minY) + : null, insetX: labelled ? GROUP_PADDING : 0, insetY: labelled ? GROUP_PADDING + GROUP_HEADER : 0, width: labelled ? contentWidth + GROUP_PADDING * 2 : contentWidth, @@ -1124,6 +1298,7 @@ function shiftLayout( nodes: SystemGraphLayoutNode[], edges: RoutedEdge[], groups: SystemGraphLayoutGroup[], + isolatedSections: SystemGraphIsolatedSection[], ): SystemGraphLayout { const xs: number[] = []; const ys: number[] = []; @@ -1143,8 +1318,24 @@ function shiftLayout( xs.push(edge.labelBounds.x, edge.labelBounds.x + edge.labelBounds.width); ys.push(edge.labelBounds.y, edge.labelBounds.y + edge.labelBounds.height); } + for (const isolatedSection of isolatedSections) { + xs.push( + isolatedSection.labelBounds.x, + isolatedSection.labelBounds.x + isolatedSection.labelBounds.width, + ); + ys.push( + isolatedSection.labelBounds.y, + isolatedSection.labelBounds.y + isolatedSection.labelBounds.height, + ); + } if (xs.length === 0 || ys.length === 0) { - return { nodes: [], edges: [], groups: [], bounds: { width: 0, height: 0 } }; + return { + nodes: [], + edges: [], + groups: [], + isolatedSections: [], + bounds: { width: 0, height: 0 }, + }; } const minX = Math.min(...xs); const minY = Math.min(...ys); @@ -1178,6 +1369,9 @@ function shiftLayout( nodes: shiftedNodes, edges: shiftedEdges, groups: shiftedGroups, + isolatedSections: isolatedSections.map((section) => + translateIsolatedSection(section, dx, dy), + ), bounds: { width: round(maxX - minX + LAYOUT_PADDING * 2), height: round(maxY - minY + LAYOUT_PADDING * 2), @@ -1191,10 +1385,24 @@ export function layoutSystemGraph( ): SystemGraphLayout { validateGraph(graph); if (graph.nodes.length === 0) { - return { nodes: [], edges: [], groups: [], bounds: { width: 0, height: 0 } }; + return { + nodes: [], + edges: [], + groups: [], + isolatedSections: [], + bounds: { width: 0, height: 0 }, + }; } + const relatedNodeIds = new Set(); + for (const edge of graph.edges) { + relatedNodeIds.add(edge.from); + relatedNodeIds.add(edge.to); + } + const globallyIsolatedNodeIds = new Set( + graph.nodes.map((node) => node.id).filter((id) => !relatedNodeIds.has(id)), + ); const laid = toRegions(graph, groups).map((region) => - layoutRegion(graph, region), + layoutRegion(graph, region, globallyIsolatedNodeIds), ); const packed = shelfPack(laid, GROUP_GAP); @@ -1202,6 +1410,7 @@ export function layoutSystemGraph( const routed: RoutedEdge[] = []; const labels: SystemGraphLabelBounds[] = []; const boxes: SystemGraphLayoutGroup[] = []; + const isolatedSections: SystemGraphIsolatedSection[] = []; laid.forEach((region, index) => { const at = packed.offsets[index]!; const dx = at.x + region.insetX; @@ -1212,6 +1421,11 @@ export function layoutSystemGraph( routed.push(moved); labels.push(moved.labelBounds); } + if (region.isolatedSection) { + const moved = translateIsolatedSection(region.isolatedSection, dx, dy); + isolatedSections.push(moved); + labels.push(moved.labelBounds); + } if (region.label !== null) { boxes.push({ id: region.id, @@ -1232,7 +1446,7 @@ export function layoutSystemGraph( ); routed.push(...routeCrossGroupEdges(crossing, nodes, labels)); - return shiftLayout(nodes, routed, boxes); + return shiftLayout(nodes, routed, boxes, isolatedSections); } export function systemGraphNodeById( diff --git a/packages/harness/web/src/styles.css b/packages/harness/web/src/styles.css index 3b29c0ab1..d8d04f2dd 100644 --- a/packages/harness/web/src/styles.css +++ b/packages/harness/web/src/styles.css @@ -4291,6 +4291,19 @@ button.rail-footer-card:hover { white-space: nowrap; } +.system-graph-isolated-label { + position: absolute; + display: flex; + align-items: center; + box-sizing: border-box; + margin: 0; + color: var(--text-faint); + font-family: var(--font-mono); + font-size: var(--type-meta); + white-space: nowrap; + pointer-events: none; +} + .system-graph-node { position: absolute; display: flex;