From d3c19558668517c4b7a302abfe2ba3eff9233b5f Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 26 Jul 2026 19:18:53 -0700 Subject: [PATCH] FN-8625: remove heartbeat controls from agent org charts Keep org charts read-only for heartbeat configuration while preserving controls on board and list surfaces. - Remove heartbeat toggle props, markup, and styling from org chart nodes - Cover both org chart layouts and board heartbeat availability with regression tests - Add a patch changeset for the dashboard behavior fix Files changed: .../fn-8625-remove-org-chart-heartbeat-toggle.md | 7 +++++++ packages/dashboard/app/components/AgentsView.css | 7 ------- packages/dashboard/app/components/AgentsView.tsx | 14 +++++--------- .../__tests__/AgentsView.orgchart.test.tsx | 20 ++++++++++++++++++++ .../app/components/__tests__/AgentsView.test.tsx | 20 +++++++++++--------- 5 files changed, 43 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-8625 Fusion-Task-Lineage: 454c05e6-fe31-486b-975a-80ba5e8c518c Co-authored-by: Fusion (runfusion.ai) --- ...-8625-remove-org-chart-heartbeat-toggle.md | 7 +++++++ .../dashboard/app/components/AgentsView.css | 7 ------- .../dashboard/app/components/AgentsView.tsx | 14 +++++-------- .../__tests__/AgentsView.orgchart.test.tsx | 20 +++++++++++++++++++ .../components/__tests__/AgentsView.test.tsx | 20 ++++++++++--------- 5 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 .changeset/fn-8625-remove-org-chart-heartbeat-toggle.md diff --git a/.changeset/fn-8625-remove-org-chart-heartbeat-toggle.md b/.changeset/fn-8625-remove-org-chart-heartbeat-toggle.md new file mode 100644 index 0000000000..dbcd2e592a --- /dev/null +++ b/.changeset/fn-8625-remove-org-chart-heartbeat-toggle.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Remove the heartbeat enable/disable button from agent org chart nodes. +category: fix +dev: HeartbeatToggle remains in board view; OrgChartNode no longer receives heartbeat props. diff --git a/packages/dashboard/app/components/AgentsView.css b/packages/dashboard/app/components/AgentsView.css index 447d9d11d7..badb6196a6 100644 --- a/packages/dashboard/app/components/AgentsView.css +++ b/packages/dashboard/app/components/AgentsView.css @@ -1235,13 +1235,6 @@ AgentsView uses the measured SVG overlay as the single connector system so paren font-size: var(--space-md); } -/* FNXC:AgentHeartbeatControls 2026-07-23-13:15: Keep the org action outside the card's button-like detail target so keyboard users never encounter nested interactive controls. */ -.org-chart-node__actions { - display: flex; - justify-content: center; - margin-top: var(--space-xs); -} - .agent-heartbeat-toggle { min-height: var(--touch-target-min-size); } diff --git a/packages/dashboard/app/components/AgentsView.tsx b/packages/dashboard/app/components/AgentsView.tsx index e59e0f094b..0d2bc61283 100644 --- a/packages/dashboard/app/components/AgentsView.tsx +++ b/packages/dashboard/app/components/AgentsView.tsx @@ -180,7 +180,10 @@ type OrgChartTransform = { scale: number; x: number; y: number }; /* FNXC:AgentHeartbeatControls 2026-07-23-13:10: -List, board, and org-chart cards use one explicit heartbeat action. It changes only runtimeConfig.enabled through the preserved payload helper; lifecycle pause/resume remains a separate control. +List and board cards use one explicit heartbeat action. It changes only runtimeConfig.enabled through the preserved payload helper; lifecycle pause/resume remains a separate control. + +FNXC:AgentHeartbeatControls 2026-07-26-19:07: +FN-8625 makes the org chart intentionally read-only for heartbeat configuration. Enable and disable actions remain available through board, list, and bulk-control surfaces. */ function HeartbeatToggle({ agent, pending, onToggle }: { agent: Agent; pending: boolean; onToggle: (agent: Agent) => void }) { const { t } = useTranslation("app"); @@ -210,15 +213,13 @@ function HeartbeatToggle({ agent, pending, onToggle }: { agent: Agent; pending: type OrgChartNodeProps = { node: OrgTreeNode; onSelect: (id: string) => void; - onToggleHeartbeat: (agent: Agent) => void; - isHeartbeatPending: (agentId: string) => boolean; getHealthStatus: (agent: Agent) => AgentHealthStatus; selectedAgentId: string | null; registerNodeElement: (id: string, element: HTMLDivElement | null) => void; linksRef: MutableRefObject; }; -function OrgChartNode({ node, onSelect, onToggleHeartbeat, isHeartbeatPending, getHealthStatus, selectedAgentId, registerNodeElement, linksRef }: OrgChartNodeProps) { +function OrgChartNode({ node, onSelect, getHealthStatus, selectedAgentId, registerNodeElement, linksRef }: OrgChartNodeProps) { const { t } = useTranslation("app"); const { agent, children } = node; const health = getHealthStatus(agent); @@ -258,7 +259,6 @@ function OrgChartNode({ node, onSelect, onToggleHeartbeat, isHeartbeatPending, g -
{children.length > 0 && (
{children.map((child) => { @@ -268,8 +268,6 @@ function OrgChartNode({ node, onSelect, onToggleHeartbeat, isHeartbeatPending, g key={child.agent.id} node={child} onSelect={onSelect} - onToggleHeartbeat={onToggleHeartbeat} - isHeartbeatPending={isHeartbeatPending} getHealthStatus={getHealthStatus} selectedAgentId={selectedAgentId} registerNodeElement={registerNodeElement} @@ -1874,8 +1872,6 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin key={node.agent.id} node={node} onSelect={handleOrgChartNodeSelect} - onToggleHeartbeat={(agent) => void handleHeartbeatEnabledChange(agent, !isAgentHeartbeatEnabled(agent))} - isHeartbeatPending={(agentId) => isBulkHeartbeatMutationRunning || heartbeatMutationAgentIds.has(agentId)} getHealthStatus={getHealthStatus} selectedAgentId={selectedOrgChartAgentId} registerNodeElement={registerOrgChartNodeElement} diff --git a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx index e9e5442f41..d9f90c022a 100644 --- a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx @@ -147,6 +147,26 @@ describe("AgentsView org chart interactions", () => { }); }); + it("renders heartbeat controls nowhere in either org-chart layout", async () => { + const { container } = render(); + fireEvent.click(await screen.findByLabelText("Org Chart view")); + + const assertHeartbeatControlsAreAbsent = () => { + const chart = screen.getByTestId("agent-org-chart"); + expect(container.querySelectorAll(".org-chart-node__actions")).toHaveLength(0); + expect(Array.from(chart.querySelectorAll("button")).filter((button) => /Disable heartbeat|Enable heartbeat/i.test(button.getAttribute("aria-label") ?? button.textContent ?? ""))).toHaveLength(0); + expect(chart.querySelectorAll(".org-chart-node button")).toHaveLength(0); + }; + + await screen.findByText("Eng C"); + assertHeartbeatControlsAreAbsent(); + + const layoutToggle = screen.getByTestId("agent-org-chart-layout-toggle"); + fireEvent.click(layoutToggle.querySelector('[data-layout-value="vertical"]')!); + await waitFor(() => expect(screen.getByTestId("agent-org-chart")).toHaveAttribute("data-layout-mode", "vertical")); + assertHeartbeatControlsAreAbsent(); + }); + it("does not render connector paths for empty or single-root org chart data states", async () => { mockFetchOrgTree.mockResolvedValueOnce([]); const empty = render(); diff --git a/packages/dashboard/app/components/__tests__/AgentsView.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.test.tsx index 556a7172f8..4de0604cba 100644 --- a/packages/dashboard/app/components/__tests__/AgentsView.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentsView.test.tsx @@ -841,6 +841,13 @@ describe("AgentsView", () => { }); }); + it("keeps heartbeat controls available on board cards", async () => { + renderView(); + + fireEvent.click(screen.getByRole("button", { name: "Board view" })); + expect(await screen.findByRole("button", { name: /Disable heartbeat for Test Agent 1/i })).toBeInTheDocument(); + }); + it("displays agent task with column context when enriched", async () => { mockFetchAgents.mockResolvedValue([ { ...mockAgents[0], id: "agent-triage", name: "Triage Agent", taskId: "FN-TRIAGE", taskColumn: "triage", state: "active" as AgentState }, @@ -2971,19 +2978,14 @@ describe("AgentsView", () => { expect(boardToggle).toHaveAttribute("aria-pressed", "true"); }); - it("renders recursive org controls and prevents a pending toggle from issuing duplicate PATCH requests", async () => { - let resolveUpdate: ((agent: Agent) => void) | undefined; + it("keeps heartbeat mutations out of org chart nodes", async () => { mockFetchOrgTree.mockResolvedValue([{ agent: { ...mockAgents[1], runtimeConfig: { enabled: false, heartbeatIntervalMs: 900_000 } }, children: [] }]); - mockUpdateAgent.mockImplementation(() => new Promise((resolve) => { resolveUpdate = resolve; })); renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); - const toggle = await screen.findByRole("button", { name: "Enable heartbeat for Test Agent 2" }); - fireEvent.click(toggle); - fireEvent.click(toggle); - expect(mockUpdateAgent).toHaveBeenCalledTimes(1); - expect(toggle).toBeDisabled(); - resolveUpdate?.(mockAgents[1]); + await screen.findByText("Test Agent 2"); + expect(screen.queryByRole("button", { name: /heartbeat for Test Agent 2/i })).toBeNull(); + expect(mockUpdateAgent).not.toHaveBeenCalled(); }); it("updates every eligible current-project durable agent through bulk controls despite filtered display", async () => {