diff --git a/packages/agent-graph/src/ports/GraphConfigPort.ts b/packages/agent-graph/src/ports/GraphConfigPort.ts index 6065bfe2..8b5ae57b 100644 --- a/packages/agent-graph/src/ports/GraphConfigPort.ts +++ b/packages/agent-graph/src/ports/GraphConfigPort.ts @@ -32,6 +32,7 @@ export interface GraphConfigPort { }; // ─── Filters (show/hide node kinds) ──────────────────────────────────── + showActivity?: boolean; showTasks?: boolean; showProcesses?: boolean; showCompletedTasks?: boolean; diff --git a/packages/agent-graph/src/ui/GraphControls.tsx b/packages/agent-graph/src/ui/GraphControls.tsx index 801b4a8b..54e632cf 100644 --- a/packages/agent-graph/src/ui/GraphControls.tsx +++ b/packages/agent-graph/src/ui/GraphControls.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import * as Tooltip from '@radix-ui/react-tooltip'; import { + Activity, Columns3, Expand, Settings2, @@ -26,6 +27,7 @@ import { } from 'lucide-react'; export interface GraphFilterState { + showActivity: boolean; showTasks: boolean; showProcesses: boolean; showEdges: boolean; @@ -219,6 +221,13 @@ export function GraphControls({ border: '1px solid rgba(100, 200, 255, 0.12)', }} > + toggle('showActivity')} + icon={} + label="Activity" + block + /> toggle('showTasks')} diff --git a/packages/agent-graph/src/ui/GraphView.tsx b/packages/agent-graph/src/ui/GraphView.tsx index 4f26e365..3e1c75db 100644 --- a/packages/agent-graph/src/ui/GraphView.tsx +++ b/packages/agent-graph/src/ui/GraphView.tsx @@ -70,6 +70,7 @@ export interface GraphViewProps { onSelectNode: (nodeId: string) => void; }) => React.ReactNode; renderHud?: (props: { + filters: GraphFilterState; getLaunchAnchorScreenPlacement: ( leadNodeId: string, ) => { x: number; y: number; scale: number; visible: boolean } | null; @@ -112,6 +113,7 @@ export function GraphView({ const [selectedEdgeId, setSelectedEdgeId] = useState(null); const [interactionLocked, setInteractionLocked] = useState(false); const [filters, setFilters] = useState({ + showActivity: config?.showActivity ?? true, showTasks: config?.showTasks ?? true, showProcesses: config?.showProcesses ?? true, showEdges: true, @@ -1016,6 +1018,7 @@ export function GraphView({ {renderHud ? (
{renderHud({ + filters, getLaunchAnchorScreenPlacement, getActivityWorldRect, getTransientHandoffSnapshot, diff --git a/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx b/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx index 69e3bc65..ea4163f3 100644 --- a/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx +++ b/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx @@ -152,7 +152,7 @@ export const TeamGraphOverlay = ({ getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null; focusEdgeIds?: ReadonlySet | null; }; - const { getViewportSize, focusNodeIds } = extraHudProps; + const { getViewportSize, focusNodeIds, filters } = extraHudProps; return ( <> @@ -174,6 +174,7 @@ export const TeamGraphOverlay = ({ getNodeWorldPosition={extraHudProps.getNodeWorldPosition} getViewportSize={getViewportSize} focusNodeIds={focusNodeIds} + enabled={filters?.showActivity ?? true} onOpenTaskDetail={onOpenTaskDetail} onOpenMemberProfile={onOpenMemberProfile} /> diff --git a/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx b/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx index f4374d32..b27a84d0 100644 --- a/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx +++ b/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx @@ -176,7 +176,7 @@ export const TeamGraphTab = ({ getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null; focusEdgeIds?: ReadonlySet | null; }; - const { getViewportSize, focusNodeIds } = extraHudProps; + const { getViewportSize, focusNodeIds, filters } = extraHudProps; return ( <> @@ -199,7 +199,7 @@ export const TeamGraphTab = ({ getNodeWorldPosition={extraHudProps.getNodeWorldPosition} getViewportSize={getViewportSize} focusNodeIds={focusNodeIds} - enabled={isActive} + enabled={isActive && (filters?.showActivity ?? true)} onOpenTaskDetail={dispatchOpenTask} onOpenMemberProfile={dispatchOpenProfile} /> diff --git a/test/renderer/features/agent-graph/GraphControls.test.ts b/test/renderer/features/agent-graph/GraphControls.test.ts index 72cf8cd3..17f98ea6 100644 --- a/test/renderer/features/agent-graph/GraphControls.test.ts +++ b/test/renderer/features/agent-graph/GraphControls.test.ts @@ -36,6 +36,7 @@ describe('GraphControls', () => { root.render( React.createElement(GraphControls, { filters: { + showActivity: true, showTasks: true, showProcesses: true, showEdges: true, @@ -88,6 +89,7 @@ describe('GraphControls', () => { root.render( React.createElement(GraphControls, { filters: { + showActivity: true, showTasks: true, showProcesses: true, showEdges: true, @@ -112,4 +114,62 @@ describe('GraphControls', () => { await Promise.resolve(); }); }); + + it('toggles activity visibility from graph settings', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const onFiltersChange = vi.fn(); + + await act(async () => { + root.render( + React.createElement(GraphControls, { + filters: { + showActivity: true, + showTasks: true, + showProcesses: true, + showEdges: true, + paused: false, + }, + onFiltersChange, + onZoomIn: vi.fn(), + onZoomOut: vi.fn(), + onZoomToFit: vi.fn(), + teamName: 'demo-team', + }) + ); + await Promise.resolve(); + }); + + const settingsButton = host.querySelector('button[aria-label="Graph settings"]'); + expect(settingsButton).not.toBeNull(); + + await act(async () => { + settingsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + + const activityButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Activity') + ); + expect(activityButton).not.toBeUndefined(); + + await act(async () => { + activityButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + + expect(onFiltersChange).toHaveBeenCalledWith({ + showActivity: false, + showTasks: true, + showProcesses: true, + showEdges: true, + paused: false, + }); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); }); diff --git a/test/renderer/features/agent-graph/GraphView.test.ts b/test/renderer/features/agent-graph/GraphView.test.ts index e2f1908c..ddfff5bd 100644 --- a/test/renderer/features/agent-graph/GraphView.test.ts +++ b/test/renderer/features/agent-graph/GraphView.test.ts @@ -31,6 +31,7 @@ const hoisted = vi.hoisted(() => ({ time: 0, }, clearTransientOwnerPositions: vi.fn(), + graphControlsProps: null as null | Record, })); vi.mock('../../../../packages/agent-graph/src/hooks/useGraphCamera', () => ({ @@ -69,7 +70,10 @@ vi.mock('../../../../packages/agent-graph/src/hooks/useGraphSimulation', () => ( })); vi.mock('../../../../packages/agent-graph/src/ui/GraphControls', () => ({ - GraphControls: () => null, + GraphControls: (props: Record) => { + hoisted.graphControlsProps = props; + return null; + }, })); vi.mock('../../../../packages/agent-graph/src/ui/GraphOverlay', () => ({ @@ -95,6 +99,7 @@ describe('GraphView pan interactions', () => { hoisted.interaction.isDragging.current = false; hoisted.simulationState.nodes = []; hoisted.simulationState.edges = []; + hoisted.graphControlsProps = null; vi.stubGlobal( 'ResizeObserver', class { @@ -397,4 +402,71 @@ describe('GraphView pan interactions', () => { expect(hoisted.interaction.handleMouseUp).toHaveBeenCalledTimes(1); expect(hoisted.clearTransientOwnerPositions).toHaveBeenCalledTimes(1); }); + + it('passes activity filter state to renderHud and updates it through graph controls', async () => { + const renderHud = vi.fn(() => null); + + await act(async () => { + root.render( + React.createElement(GraphView, { + data: { + teamName: 'demo-team', + nodes: [], + edges: [], + particles: [], + }, + config: { + animationEnabled: false, + showActivity: false, + }, + renderHud, + }) + ); + await Promise.resolve(); + }); + + expect(renderHud).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: expect.objectContaining({ + showActivity: false, + }), + }) + ); + + const controlsProps = hoisted.graphControlsProps as + | { + filters: { + showActivity: boolean; + showTasks: boolean; + showProcesses: boolean; + showEdges: boolean; + paused: boolean; + }; + onFiltersChange: (filters: { + showActivity: boolean; + showTasks: boolean; + showProcesses: boolean; + showEdges: boolean; + paused: boolean; + }) => void; + } + | null; + expect(controlsProps).not.toBeNull(); + + await act(async () => { + controlsProps?.onFiltersChange({ + ...controlsProps!.filters, + showActivity: true, + }); + await Promise.resolve(); + }); + + expect(renderHud).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: expect.objectContaining({ + showActivity: true, + }), + }) + ); + }); });