diff --git a/packages/dashboard/app/components/AgentsView.css b/packages/dashboard/app/components/AgentsView.css
index ea27d08216..eb879bfa11 100644
--- a/packages/dashboard/app/components/AgentsView.css
+++ b/packages/dashboard/app/components/AgentsView.css
@@ -1047,9 +1047,15 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
transition: none;
}
+/*
+FNXC:AgentsOrgChart 2026-06-22-03:05:
+AgentsView uses the measured SVG overlay as the single connector system so parent-child lines remain non-zero and aligned across horizontal/vertical layouts, desktop/mobile sizing, and pan/zoom transforms. Explicitly size the absolute SVG to fill the chart canvas instead of relying on the browser's default 300×150 SVG viewport.
+*/
.agent-org-chart-connectors {
position: absolute;
inset: 0;
+ width: 100%;
+ height: 100%;
overflow: visible;
pointer-events: none;
}
@@ -1192,41 +1198,6 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c
gap: var(--org-chart-sibling-gap);
padding-top: var(--org-chart-children-offset);
margin-top: var(--org-chart-connector-gap);
- --org-chart-first-child-center-offset: 50%;
- --org-chart-last-child-center-offset: 50%;
-}
-
-.org-chart-children::before {
- content: "";
- position: absolute;
- top: 0;
- height: var(--org-chart-children-offset, var(--space-md));
- left: var(--org-chart-first-child-center-offset);
- right: var(--org-chart-last-child-center-offset);
- border-top: 1px solid var(--border);
- pointer-events: none;
-}
-
-.org-chart-children > .org-chart-node::before {
- content: "";
- position: absolute;
- top: calc(-1 * var(--org-chart-children-offset));
- left: 50%;
- width: 1px;
- height: var(--org-chart-children-offset);
- border-left: 1px solid var(--border);
- pointer-events: none;
-}
-
-.agent-org-chart--vertical .org-chart-children::before {
- top: 0;
- left: var(--space-sm);
- right: auto;
- bottom: 0;
- width: 1px;
- height: auto;
- border-top: none;
- border-left: 1px solid var(--border);
}
.agent-org-chart--vertical {
diff --git a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx
index 4031a41bf7..fab0c503f7 100644
--- a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx
+++ b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx
@@ -1,3 +1,5 @@
+import { readFileSync } from "node:fs";
+import { join, resolve } from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { AgentsView } from "../AgentsView";
@@ -26,6 +28,17 @@ vi.mock("../../api", async (importOriginal) => {
const mockFetchOrgTree = vi.mocked((apiModule as any).fetchOrgTree);
const mockFetchAgents = vi.mocked((apiModule as any).fetchAgents);
+const COMPONENTS_DIR = resolve(__dirname, "..");
+const AGENTS_VIEW_CSS = join(COMPONENTS_DIR, "AgentsView.css");
+
+function extractRuleBlock(css: string, selector: string): string {
+ const ruleStart = css.indexOf(`${selector} {`);
+ expect(ruleStart, `Expected ${selector} to exist in AgentsView.css`).toBeGreaterThanOrEqual(0);
+ const bodyStart = css.indexOf("{", ruleStart);
+ const bodyEnd = css.indexOf("\n}", bodyStart);
+ expect(bodyEnd, `Expected ${selector} rule to have a closing brace`).toBeGreaterThan(bodyStart);
+ return css.slice(bodyStart + 1, bodyEnd);
+}
const orgTree = [{ agent: { id: "ceo", name: "CEO", role: "scheduler", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [
{ agent: { id: "cto", name: "CTO", role: "engineer", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [
@@ -61,6 +74,18 @@ describe("AgentsView org chart interactions", () => {
mockRects();
});
+ it("keeps SVG connectors explicitly sized and removes the broken CSS connector bus", () => {
+ const css = readFileSync(AGENTS_VIEW_CSS, "utf8");
+ const connectorBlock = extractRuleBlock(css, ".agent-org-chart-connectors");
+ expect(connectorBlock).toMatch(/width\s*:\s*100%\s*;/);
+ expect(connectorBlock).toMatch(/height\s*:\s*100%\s*;/);
+ expect(connectorBlock).toMatch(/overflow\s*:\s*visible\s*;/);
+ expect(css).not.toContain("--org-chart-first-child-center-offset");
+ expect(css).not.toContain("--org-chart-last-child-center-offset");
+ expect(css).not.toContain(".org-chart-children::before");
+ expect(css).not.toContain(".org-chart-children > .org-chart-node::before");
+ });
+
it("renders controls and supports transform interactions", async () => {
render();
fireEvent.click(await screen.findByLabelText("Org Chart view"));
@@ -112,11 +137,28 @@ describe("AgentsView org chart interactions", () => {
fireEvent.click(screen.getByLabelText("Vertical layout"));
await waitFor(() => {
- const firstPath = document.querySelector(".agent-org-chart-connectors path")?.getAttribute("d") ?? "";
+ const paths = document.querySelectorAll(".agent-org-chart-connectors path");
+ expect(paths.length).toBe(4);
+ const firstPath = paths[0]?.getAttribute("d") ?? "";
expect(firstPath).toMatch(/^M\s\d+\s\d+\sL\s\d+\s\d+/);
});
});
+ it("does not render connector paths for empty or single-root org chart data states", async () => {
+ mockFetchOrgTree.mockResolvedValueOnce([]);
+ const empty = render();
+ fireEvent.click(await screen.findByLabelText("Org Chart view"));
+ await screen.findByText("No agents found");
+ expect(document.querySelectorAll(".agent-org-chart-connectors path")).toHaveLength(0);
+ empty.unmount();
+
+ mockFetchOrgTree.mockResolvedValueOnce([{ agent: { id: "solo", name: "Solo", role: "executor", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [] }]);
+ render();
+ fireEvent.click(await screen.findByLabelText("Org Chart view"));
+ await screen.findByText("Solo");
+ await waitFor(() => expect(document.querySelectorAll(".agent-org-chart-connectors path")).toHaveLength(0));
+ });
+
it("renders mobile controls", async () => {
mockViewportMode.mockReturnValue("mobile");
render();