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;