From c8f9d9bbdd4ebd339f3c0e005c224e58b05dcc89 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 14 Apr 2026 16:24:09 +0300 Subject: [PATCH] refactor: migrate agent graph to feature slice --- docs/FEATURE_ARCHITECTURE_STANDARD.md | 18 +++++ eslint.config.js | 71 +++++++++++++++++++ src/features/README.md | 5 ++ src/features/agent-graph/README.md | 20 ++++++ .../domain}/buildInlineActivityEntries.ts | 2 +- .../core/domain}/collapseOverflowStacks.ts | 0 .../core/domain}/taskGraphSemantics.ts | 0 .../renderer}/adapters/TeamGraphAdapter.ts | 17 +++-- .../hooks}/useGraphCreateTaskDialog.tsx | 0 .../renderer/hooks}/useTeamGraphAdapter.ts | 2 +- src/features/agent-graph/renderer/index.ts | 12 ++++ .../renderer}/ui/GraphActivityHud.tsx | 2 +- .../renderer}/ui/GraphBlockingEdgePopover.tsx | 0 .../renderer}/ui/GraphNodePopover.tsx | 29 +++++--- .../renderer}/ui/GraphProvisioningHud.tsx | 0 .../renderer}/ui/GraphTaskCard.tsx | 11 +-- .../renderer}/ui/TeamGraphOverlay.tsx | 4 +- .../agent-graph/renderer}/ui/TeamGraphTab.tsx | 6 +- .../components/layout/PaneContent.tsx | 2 +- .../components/team/TeamDetailView.tsx | 4 +- .../team/members/MemberDetailDialog.tsx | 2 +- .../team/members/MemberMessagesTab.tsx | 2 +- src/renderer/features/agent-graph/index.ts | 9 --- .../agent-graph/GraphActivityHud.test.ts | 32 +++++---- .../GraphBlockingEdgePopover.test.ts | 2 +- .../agent-graph/GraphNodePopover.test.ts | 4 +- .../agent-graph/GraphProvisioningHud.test.ts | 4 +- .../agent-graph/TeamGraphAdapter.test.ts | 4 +- .../buildInlineActivityEntries.test.ts | 2 +- .../collapseOverflowStacks.test.ts | 2 +- 30 files changed, 203 insertions(+), 65 deletions(-) create mode 100644 src/features/agent-graph/README.md rename src/{renderer/features/agent-graph/utils => features/agent-graph/core/domain}/buildInlineActivityEntries.ts (99%) rename src/{renderer/features/agent-graph/utils => features/agent-graph/core/domain}/collapseOverflowStacks.ts (100%) rename src/{renderer/features/agent-graph/utils => features/agent-graph/core/domain}/taskGraphSemantics.ts (100%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/adapters/TeamGraphAdapter.ts (98%) rename src/{renderer/features/agent-graph/ui => features/agent-graph/renderer/hooks}/useGraphCreateTaskDialog.tsx (100%) rename src/{renderer/features/agent-graph/adapters => features/agent-graph/renderer/hooks}/useTeamGraphAdapter.ts (97%) create mode 100644 src/features/agent-graph/renderer/index.ts rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/GraphActivityHud.tsx (99%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/GraphBlockingEdgePopover.tsx (100%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/GraphNodePopover.tsx (95%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/GraphProvisioningHud.tsx (100%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/GraphTaskCard.tsx (92%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/TeamGraphOverlay.tsx (97%) rename src/{renderer/features/agent-graph => features/agent-graph/renderer}/ui/TeamGraphTab.tsx (96%) delete mode 100644 src/renderer/features/agent-graph/index.ts diff --git a/docs/FEATURE_ARCHITECTURE_STANDARD.md b/docs/FEATURE_ARCHITECTURE_STANDARD.md index 913970ce..483acda2 100644 --- a/docs/FEATURE_ARCHITECTURE_STANDARD.md +++ b/docs/FEATURE_ARCHITECTURE_STANDARD.md @@ -262,6 +262,12 @@ A smaller feature may skip `core/` and `preload/` when it is: - not adding a new use case - not adding a new transport boundary +If the feature still owns meaningful pure semantics or projection rules, keep +`core/` and skip only the process layers you do not need. + +Example: +- `src/features/agent-graph` keeps `core/domain` and `renderer`, but does not add fake `main/` or `preload/` folders because the transport boundary lives elsewhere. + ## Definition Of Done For A Reference Feature A feature is reference-quality when: @@ -295,3 +301,15 @@ Use it as the example for: - renderer dumb UI + hook orchestration - browser-friendly transport direction - feature-level lint guard rails + +## Agent Graph As The Thin-Slice Reference + +`src/features/agent-graph` is the thin-slice example for a renderer integration +feature built on top of a reusable package. + +Use it as the example for: + +- keeping pure graph semantics in `core/domain` +- exposing a renderer-only public entrypoint +- integrating `packages/agent-graph` without inventing fake process layers +- migrating legacy `src/renderer/features/*` code into the canonical feature root diff --git a/eslint.config.js b/eslint.config.js index cbb8fac6..15da01bf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -227,6 +227,77 @@ export default defineConfig([ ], }, }, + { + name: 'feature-agent-graph-public-entrypoints', + files: ['src/**/*.{ts,tsx}'], + ignores: ['src/features/agent-graph/**/*'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: [ + '@features/agent-graph/core/**', + '@features/agent-graph/renderer/**', + ], + message: + 'Import agent-graph only through its public entrypoint: @features/agent-graph/renderer.', + }, + ], + }, + ], + }, + }, + { + name: 'feature-agent-graph-core-domain-guards', + files: ['src/features/agent-graph/core/domain/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: [ + '@features/agent-graph/renderer/**', + '@main/**', + '@renderer/**', + '@preload/**', + 'electron', + 'fastify', + 'child_process', + 'node:child_process', + ], + message: + 'agent-graph core/domain must stay pure and cannot depend on renderer, main, preload, or platform code.', + }, + ], + }, + ], + }, + }, + { + name: 'feature-agent-graph-renderer-boundaries', + files: ['src/features/agent-graph/renderer/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: [ + '@main/**', + '@preload/**', + 'electron', + ], + message: + 'agent-graph renderer may depend on shared, renderer, package, and feature-local modules, but not on main/preload or Electron APIs directly.', + }, + ], + }, + ], + }, + }, // Module boundaries - Enforce Electron three-process architecture { diff --git a/src/features/README.md b/src/features/README.md index c6bf5609..4e9851ff 100644 --- a/src/features/README.md +++ b/src/features/README.md @@ -8,6 +8,7 @@ Before creating or refactoring a feature, read: Reference implementation: - `src/features/recent-projects` +- `src/features/agent-graph` Use `src/features//` by default when the work introduces: - a new use case or business policy @@ -17,3 +18,7 @@ Use `src/features//` by default when the work introduces: Do not duplicate architecture rules in feature folders. Keep the standard centralized in [../../docs/FEATURE_ARCHITECTURE_STANDARD.md](../../docs/FEATURE_ARCHITECTURE_STANDARD.md). + +Rule of thumb: +- `recent-projects` is the full slice example with process-aware outer layers +- `agent-graph` is the thin slice example built around `core/` plus `renderer/` diff --git a/src/features/agent-graph/README.md b/src/features/agent-graph/README.md new file mode 100644 index 00000000..70101e02 --- /dev/null +++ b/src/features/agent-graph/README.md @@ -0,0 +1,20 @@ +# Agent Graph Feature + +This feature is a thin renderer slice over the reusable graph engine in `packages/agent-graph`. + +Read first: +- [Feature Architecture Standard](../../docs/FEATURE_ARCHITECTURE_STANDARD.md) +- [Feature root guidance](../CLAUDE.md) + +Public entrypoint: +- `@features/agent-graph/renderer` + +Responsibilities: +- `packages/agent-graph` owns reusable graph rendering and low-level graph mechanics +- `src/features/agent-graph/core/domain` owns project-specific graph semantics and pure projection helpers +- `src/features/agent-graph/renderer` owns the renderer integration layer, hooks, adapters, and UI + +Use this feature as the thin-slice example when a feature: +- has no dedicated `main` or `preload` transport boundary +- integrates an existing reusable package into the app shell +- still needs its own feature boundary and public entrypoint diff --git a/src/renderer/features/agent-graph/utils/buildInlineActivityEntries.ts b/src/features/agent-graph/core/domain/buildInlineActivityEntries.ts similarity index 99% rename from src/renderer/features/agent-graph/utils/buildInlineActivityEntries.ts rename to src/features/agent-graph/core/domain/buildInlineActivityEntries.ts index 1327fd64..690c6f4f 100644 --- a/src/renderer/features/agent-graph/utils/buildInlineActivityEntries.ts +++ b/src/features/agent-graph/core/domain/buildInlineActivityEntries.ts @@ -160,7 +160,7 @@ export function buildInlineActivityEntries({ for (const [ownerNodeId, entries] of entriesByOwnerNodeId) { entriesByOwnerNodeId.set( ownerNodeId, - entries.sort((a, b) => b.graphItem.timestamp.localeCompare(a.graphItem.timestamp)) + entries.toSorted((a, b) => b.graphItem.timestamp.localeCompare(a.graphItem.timestamp)) ); } diff --git a/src/renderer/features/agent-graph/utils/collapseOverflowStacks.ts b/src/features/agent-graph/core/domain/collapseOverflowStacks.ts similarity index 100% rename from src/renderer/features/agent-graph/utils/collapseOverflowStacks.ts rename to src/features/agent-graph/core/domain/collapseOverflowStacks.ts diff --git a/src/renderer/features/agent-graph/utils/taskGraphSemantics.ts b/src/features/agent-graph/core/domain/taskGraphSemantics.ts similarity index 100% rename from src/renderer/features/agent-graph/utils/taskGraphSemantics.ts rename to src/features/agent-graph/core/domain/taskGraphSemantics.ts diff --git a/src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts similarity index 98% rename from src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts rename to src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts index bb279bbd..a7fd0bf9 100644 --- a/src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts @@ -1,8 +1,9 @@ /** * TeamGraphAdapter — transforms Zustand TeamData → GraphDataPort. * - * This is the ONLY file in this feature that imports from @renderer/store. - * If the project data model changes, ONLY this class needs updating. + * This adapter owns the graph projection from team runtime state into the + * reusable package port model. Renderer hooks may still read store state, but + * projection rules stay here so the mapping logic has one main reason to change. * * Class-based with ES #private fields and DI-ready constructor. */ @@ -26,16 +27,15 @@ import { isLeadMember } from '@shared/utils/leadDetection'; import { buildInlineActivityEntries, getGraphLeadMemberName, -} from '../utils/buildInlineActivityEntries'; -import { collapseOverflowStacksWithMeta } from '../utils/collapseOverflowStacks'; +} from '../../core/domain/buildInlineActivityEntries'; +import { collapseOverflowStacksWithMeta } from '../../core/domain/collapseOverflowStacks'; import { isTaskBlocked, isTaskInReviewCycle, resolveTaskReviewer, -} from '../utils/taskGraphSemantics'; +} from '../../core/domain/taskGraphSemantics'; import type { - GraphActivityItem, GraphDataPort, GraphEdge, GraphNode, @@ -571,7 +571,10 @@ export class TeamGraphAdapter { for (const relatedId of task.related ?? []) { if (!visibleTaskIds.has(relatedId)) continue; - const key = [task.id, relatedId].sort().join(':'); + const key = + task.id.localeCompare(relatedId) <= 0 + ? `${task.id}:${relatedId}` + : `${relatedId}:${task.id}`; if (this.#seenRelated.has(key)) continue; this.#seenRelated.add(key); edges.push({ diff --git a/src/renderer/features/agent-graph/ui/useGraphCreateTaskDialog.tsx b/src/features/agent-graph/renderer/hooks/useGraphCreateTaskDialog.tsx similarity index 100% rename from src/renderer/features/agent-graph/ui/useGraphCreateTaskDialog.tsx rename to src/features/agent-graph/renderer/hooks/useGraphCreateTaskDialog.tsx diff --git a/src/renderer/features/agent-graph/adapters/useTeamGraphAdapter.ts b/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts similarity index 97% rename from src/renderer/features/agent-graph/adapters/useTeamGraphAdapter.ts rename to src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts index 2b160983..346a7105 100644 --- a/src/renderer/features/agent-graph/adapters/useTeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts @@ -13,7 +13,7 @@ import { } from '@renderer/store/slices/teamSlice'; import { useShallow } from 'zustand/react/shallow'; -import { TeamGraphAdapter } from './TeamGraphAdapter'; +import { TeamGraphAdapter } from '../adapters/TeamGraphAdapter'; import type { GraphDataPort } from '@claude-teams/agent-graph'; diff --git a/src/features/agent-graph/renderer/index.ts b/src/features/agent-graph/renderer/index.ts new file mode 100644 index 00000000..fca5e786 --- /dev/null +++ b/src/features/agent-graph/renderer/index.ts @@ -0,0 +1,12 @@ +/** + * agent-graph renderer feature - public API. + * + * Consumers outside the feature should import from here instead of reaching + * into ui/, hooks/, or core/ directly. + */ + +export { buildInlineActivityEntries } from '../core/domain/buildInlineActivityEntries'; +export { TeamGraphAdapter } from './adapters/TeamGraphAdapter'; +export type { TeamGraphOverlayProps } from './ui/TeamGraphOverlay'; +export { TeamGraphOverlay } from './ui/TeamGraphOverlay'; +export { TeamGraphTab } from './ui/TeamGraphTab'; diff --git a/src/renderer/features/agent-graph/ui/GraphActivityHud.tsx b/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx similarity index 99% rename from src/renderer/features/agent-graph/ui/GraphActivityHud.tsx rename to src/features/agent-graph/renderer/ui/GraphActivityHud.tsx index 29ba6fd9..0be0a066 100644 --- a/src/renderer/features/agent-graph/ui/GraphActivityHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx @@ -17,7 +17,7 @@ import { buildInlineActivityEntries, getGraphLeadMemberName, type InlineActivityEntry, -} from '../utils/buildInlineActivityEntries'; +} from '../../core/domain/buildInlineActivityEntries'; import type { GraphNode } from '@claude-teams/agent-graph'; import type { TimelineItem } from '@renderer/components/team/activity/LeadThoughtsGroup'; diff --git a/src/renderer/features/agent-graph/ui/GraphBlockingEdgePopover.tsx b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx similarity index 100% rename from src/renderer/features/agent-graph/ui/GraphBlockingEdgePopover.tsx rename to src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx diff --git a/src/renderer/features/agent-graph/ui/GraphNodePopover.tsx b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx similarity index 95% rename from src/renderer/features/agent-graph/ui/GraphNodePopover.tsx rename to src/features/agent-graph/renderer/ui/GraphNodePopover.tsx index 029b1834..ffb7d100 100644 --- a/src/renderer/features/agent-graph/ui/GraphNodePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx @@ -1,7 +1,7 @@ /** * GraphNodePopover — renders popover for graph nodes using project UI components. - * Lives in features/ (not in package) so it CAN import from @renderer/. - * Reuses agentAvatarUrl, status helpers, and UI primitives from the project. + * This stays in the renderer slice instead of the reusable package because it + * composes project-specific UI, selectors, and presentation helpers. */ import { Badge } from '@renderer/components/ui/badge'; @@ -16,7 +16,7 @@ import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvision import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; -import { isTaskInReviewCycle, resolveTaskReviewer } from '../utils/taskGraphSemantics'; +import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics'; import { GraphTaskCard } from './GraphTaskCard'; @@ -40,11 +40,22 @@ function formatToolPreview(preview: string | undefined): string | undefined { // If it looks like raw JSON object, try to extract a readable field if (preview.startsWith('{') || preview.startsWith('[')) { try { - const obj = JSON.parse(preview.length > 200 ? preview.slice(0, 200) : preview); - // Common readable fields - return ( - obj.subject ?? obj.name ?? obj.label ?? obj.file_path ?? obj.path ?? obj.query ?? undefined - ); + const parsed: unknown = JSON.parse(preview.length > 200 ? preview.slice(0, 200) : preview); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const previewRecord = parsed as Record; + const candidates = [ + previewRecord.subject, + previewRecord.name, + previewRecord.label, + previewRecord.file_path, + previewRecord.path, + previewRecord.query, + ]; + const firstText = candidates.find((value) => typeof value === 'string'); + if (typeof firstText === 'string') { + return firstText; + } + } } catch { // Truncated JSON — extract first quoted value const match = /"(?:subject|name|label|path|query)":\s*"([^"]{1,60})"/.exec(preview); @@ -421,7 +432,7 @@ const MemberPopoverContent = ({ )} - {/* TODO: Context usage disabled — LeadContextUsage.percent unreliable (jumps) */} + {/* Context usage stays hidden for now because LeadContextUsage.percent is unreliable. */} {/* Current task indicator — reuses same pattern as MemberCard */} {node.currentTaskId && node.currentTaskSubject && ( diff --git a/src/renderer/features/agent-graph/ui/GraphProvisioningHud.tsx b/src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx similarity index 100% rename from src/renderer/features/agent-graph/ui/GraphProvisioningHud.tsx rename to src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx diff --git a/src/renderer/features/agent-graph/ui/GraphTaskCard.tsx b/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx similarity index 92% rename from src/renderer/features/agent-graph/ui/GraphTaskCard.tsx rename to src/features/agent-graph/renderer/ui/GraphTaskCard.tsx index ba2c343e..903fdeea 100644 --- a/src/renderer/features/agent-graph/ui/GraphTaskCard.tsx +++ b/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx @@ -1,6 +1,7 @@ /** * GraphTaskCard — wraps the REAL KanbanTaskCard with graph-specific glow/pulse effects. - * Lives in features/ so it CAN import from @renderer/. + * This is a renderer integration component, so it is allowed to compose + * project UI primitives and store-backed selectors. */ import { useMemo } from 'react'; @@ -10,10 +11,10 @@ import { useStore } from '@renderer/store'; import { selectTeamDataForName } from '@renderer/store/slices/teamSlice'; import { useShallow } from 'zustand/react/shallow'; -import { isTaskBlocked, resolveTaskGraphColumn } from '../utils/taskGraphSemantics'; +import { isTaskBlocked, resolveTaskGraphColumn } from '../../core/domain/taskGraphSemantics'; import type { GraphNode } from '@claude-teams/agent-graph'; -import type { KanbanColumnId, TeamTask, TeamTaskWithKanban } from '@shared/types'; +import type { KanbanColumnId, TeamTask } from '@shared/types'; // ─── Types ────────────────────────────────────────────────────────────────── @@ -119,8 +120,8 @@ export const GraphTaskCard = ({ const columnId = resolveColumn(task); const taskWithKanban = task; - const closeAct = (fn?: (id: string) => void) => (taskId: string) => { - fn?.(taskId); + const closeAct = (fn?: (id: string) => void) => (nextTaskId: string) => { + fn?.(nextTaskId); onClose(); }; diff --git a/src/renderer/features/agent-graph/ui/TeamGraphOverlay.tsx b/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx similarity index 97% rename from src/renderer/features/agent-graph/ui/TeamGraphOverlay.tsx rename to src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx index 2a12a7d3..d71db2ab 100644 --- a/src/renderer/features/agent-graph/ui/TeamGraphOverlay.tsx +++ b/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx @@ -9,13 +9,13 @@ import { GraphView } from '@claude-teams/agent-graph'; import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost'; import { useStore } from '@renderer/store'; -import { useTeamGraphAdapter } from '../adapters/useTeamGraphAdapter'; +import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog'; +import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter'; import { GraphActivityHud } from './GraphActivityHud'; import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover'; import { GraphNodePopover } from './GraphNodePopover'; import { GraphProvisioningHud } from './GraphProvisioningHud'; -import { useGraphCreateTaskDialog } from './useGraphCreateTaskDialog'; import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph'; import type { diff --git a/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx b/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx similarity index 96% rename from src/renderer/features/agent-graph/ui/TeamGraphTab.tsx rename to src/features/agent-graph/renderer/ui/TeamGraphTab.tsx index 940b62dd..76025c59 100644 --- a/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx +++ b/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx @@ -9,15 +9,15 @@ import { GraphView } from '@claude-teams/agent-graph'; import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost'; import { useStore } from '@renderer/store'; -import { useTeamGraphAdapter } from '../adapters/useTeamGraphAdapter'; +import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog'; +import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter'; import { GraphActivityHud } from './GraphActivityHud'; import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover'; import { GraphNodePopover } from './GraphNodePopover'; import { GraphProvisioningHud } from './GraphProvisioningHud'; -import { useGraphCreateTaskDialog } from './useGraphCreateTaskDialog'; -import type { GraphDomainRef, GraphEventPort, GraphNode } from '@claude-teams/agent-graph'; +import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph'; import type { MemberActivityFilter, MemberDetailTab, diff --git a/src/renderer/components/layout/PaneContent.tsx b/src/renderer/components/layout/PaneContent.tsx index a7449d29..e65e0a9e 100644 --- a/src/renderer/components/layout/PaneContent.tsx +++ b/src/renderer/components/layout/PaneContent.tsx @@ -3,8 +3,8 @@ * Uses CSS display-toggle to keep all tabs mounted (preserving state). */ +import { TeamGraphTab } from '@features/agent-graph/renderer'; import { TabUIProvider } from '@renderer/contexts/TabUIContext'; -import { TeamGraphTab } from '@renderer/features/agent-graph/ui/TeamGraphTab'; import { DashboardView } from '../dashboard/DashboardView'; import { ExtensionStoreView } from '../extensions/ExtensionStoreView'; diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index fe8eed43..c53f03c4 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -72,15 +72,15 @@ import { TrashDialog } from './kanban/TrashDialog'; import { MemberDetailDialog } from './members/MemberDetailDialog'; import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes'; -import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode'; import type { AddMemberEntry } from './dialogs/AddMemberDialog'; +import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode'; import type { ComponentProps, CSSProperties } from 'react'; const ProjectEditorOverlay = lazy(() => import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay })) ); const TeamGraphOverlay = lazy(() => - import('@renderer/features/agent-graph/ui/TeamGraphOverlay').then((m) => ({ + import('@features/agent-graph/renderer').then((m) => ({ default: m.TeamGraphOverlay, })) ); diff --git a/src/renderer/components/team/members/MemberDetailDialog.tsx b/src/renderer/components/team/members/MemberDetailDialog.tsx index d4cdce7c..3db3e9d2 100644 --- a/src/renderer/components/team/members/MemberDetailDialog.tsx +++ b/src/renderer/components/team/members/MemberDetailDialog.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; +import { buildInlineActivityEntries } from '@features/agent-graph/renderer'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; -import { buildInlineActivityEntries } from '@renderer/features/agent-graph/utils/buildInlineActivityEntries'; import { useMemberStats } from '@renderer/hooks/useMemberStats'; import { isLeadMember } from '@shared/utils/leadDetection'; import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react'; diff --git a/src/renderer/components/team/members/MemberMessagesTab.tsx b/src/renderer/components/team/members/MemberMessagesTab.tsx index d273268c..b2eda169 100644 --- a/src/renderer/components/team/members/MemberMessagesTab.tsx +++ b/src/renderer/components/team/members/MemberMessagesTab.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { buildInlineActivityEntries } from '@features/agent-graph/renderer'; import { api } from '@renderer/api'; import { ActivityItem } from '@renderer/components/team/activity/ActivityItem'; import { @@ -8,7 +9,6 @@ import { } from '@renderer/components/team/activity/activityMessageContext'; import { MessageExpandDialog } from '@renderer/components/team/activity/MessageExpandDialog'; import { Button } from '@renderer/components/ui/button'; -import { buildInlineActivityEntries } from '@renderer/features/agent-graph/utils/buildInlineActivityEntries'; import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead'; import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages'; import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering'; diff --git a/src/renderer/features/agent-graph/index.ts b/src/renderer/features/agent-graph/index.ts deleted file mode 100644 index 1c15bd65..00000000 --- a/src/renderer/features/agent-graph/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * agent-graph feature — public API. - * Only exports UI components and adapter types. - */ - -export { TeamGraphAdapter } from './adapters/TeamGraphAdapter'; -export type { TeamGraphOverlayProps } from './ui/TeamGraphOverlay'; -export { TeamGraphOverlay } from './ui/TeamGraphOverlay'; -export { TeamGraphTab } from './ui/TeamGraphTab'; diff --git a/test/renderer/features/agent-graph/GraphActivityHud.test.ts b/test/renderer/features/agent-graph/GraphActivityHud.test.ts index 1f772787..55c9648d 100644 --- a/test/renderer/features/agent-graph/GraphActivityHud.test.ts +++ b/test/renderer/features/agent-graph/GraphActivityHud.test.ts @@ -2,7 +2,7 @@ import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { GraphActivityHud } from '@renderer/features/agent-graph/ui/GraphActivityHud'; +import { GraphActivityHud } from '@features/agent-graph/renderer/ui/GraphActivityHud'; import type { GraphNode } from '@claude-teams/agent-graph'; import type { InboxMessage } from '@shared/types/team'; @@ -17,16 +17,22 @@ const teamState = { tasks: [], messages: [], }, - teamDataCacheByName: { - 'demo-team': { - members: [ - { name: 'team-lead', agentType: 'team-lead' }, - { name: 'jack', agentType: 'developer' }, - ], - tasks: [], - messages: [], - }, - } as Record>; tasks: unknown[]; messages: unknown[] }>, + teamDataCacheByName: new Map< + string, + { members: Record[]; tasks: unknown[]; messages: unknown[] } + >([ + [ + 'demo-team', + { + members: [ + { name: 'team-lead', agentType: 'team-lead' }, + { name: 'jack', agentType: 'developer' }, + ], + tasks: [], + messages: [], + }, + ], + ]), teams: [], }; @@ -38,7 +44,7 @@ vi.mock('@renderer/store', () => ({ vi.mock('@renderer/store/slices/teamSlice', () => ({ selectTeamDataForName: (_state: typeof teamState, teamName: string) => - teamState.teamDataCacheByName[teamName] ?? + teamState.teamDataCacheByName.get(teamName) ?? (teamState.selectedTeamName === teamName ? teamState.selectedTeamData : null), })); @@ -79,7 +85,7 @@ vi.mock('@renderer/components/team/activity/activityMessageContext', () => ({ resolveMessageRenderProps: () => ({}), })); -vi.mock('@renderer/features/agent-graph/utils/buildInlineActivityEntries', () => ({ +vi.mock('@features/agent-graph/core/domain/buildInlineActivityEntries', () => ({ buildInlineActivityEntries: (...args: unknown[]) => buildInlineActivityEntries(...args), getGraphLeadMemberName: () => 'team-lead', })); diff --git a/test/renderer/features/agent-graph/GraphBlockingEdgePopover.test.ts b/test/renderer/features/agent-graph/GraphBlockingEdgePopover.test.ts index 8740ecb6..15d30a81 100644 --- a/test/renderer/features/agent-graph/GraphBlockingEdgePopover.test.ts +++ b/test/renderer/features/agent-graph/GraphBlockingEdgePopover.test.ts @@ -2,8 +2,8 @@ import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { GraphBlockingEdgePopover } from '@features/agent-graph/renderer/ui/GraphBlockingEdgePopover'; import { useStore } from '@renderer/store'; -import { GraphBlockingEdgePopover } from '@renderer/features/agent-graph/ui/GraphBlockingEdgePopover'; import type { GraphEdge, GraphNode } from '@claude-teams/agent-graph'; diff --git a/test/renderer/features/agent-graph/GraphNodePopover.test.ts b/test/renderer/features/agent-graph/GraphNodePopover.test.ts index 9a85ee15..7eaa65b5 100644 --- a/test/renderer/features/agent-graph/GraphNodePopover.test.ts +++ b/test/renderer/features/agent-graph/GraphNodePopover.test.ts @@ -13,11 +13,11 @@ vi.mock('@renderer/components/ui/button', () => ({ React.createElement('button', { type: 'button' }, children), })); -vi.mock('@renderer/features/agent-graph/ui/GraphTaskCard', () => ({ +vi.mock('@features/agent-graph/renderer/ui/GraphTaskCard', () => ({ GraphTaskCard: () => React.createElement('div', null, 'task-card'), })); -import { GraphNodePopover } from '@renderer/features/agent-graph/ui/GraphNodePopover'; +import { GraphNodePopover } from '@features/agent-graph/renderer/ui/GraphNodePopover'; import type { GraphNode } from '@claude-teams/agent-graph'; diff --git a/test/renderer/features/agent-graph/GraphProvisioningHud.test.ts b/test/renderer/features/agent-graph/GraphProvisioningHud.test.ts index e9f830a0..49a29299 100644 --- a/test/renderer/features/agent-graph/GraphProvisioningHud.test.ts +++ b/test/renderer/features/agent-graph/GraphProvisioningHud.test.ts @@ -2,6 +2,8 @@ import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { GraphProvisioningHud } from '@features/agent-graph/renderer/ui/GraphProvisioningHud'; + const hookState = { presentation: null as | { @@ -44,8 +46,6 @@ vi.mock('@renderer/components/team/TeamProvisioningPanel', () => ({ ), })); -import { GraphProvisioningHud } from '@renderer/features/agent-graph/ui/GraphProvisioningHud'; - const placement = { x: 120, y: 80, scale: 1, visible: true }; describe('GraphProvisioningHud', () => { diff --git a/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts b/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts index 10f1771e..08fab22a 100644 --- a/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts +++ b/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { TeamGraphAdapter } from '@renderer/features/agent-graph/adapters/TeamGraphAdapter'; +import { TeamGraphAdapter } from '@features/agent-graph/renderer/adapters/TeamGraphAdapter'; import type { InboxMessage, TeamData, TeamTaskWithKanban } from '@shared/types/team'; import type { GraphDataPort } from '@claude-teams/agent-graph'; @@ -459,7 +459,7 @@ describe('TeamGraphAdapter particles', () => { const graph = adapter.adapt(next, 'my-team'); expect(graph.particles).toHaveLength(2); - expect(graph.particles.map((particle) => particle.kind).sort()).toEqual([ + expect(graph.particles.map((particle) => particle.kind).toSorted((a, b) => a.localeCompare(b))).toEqual([ 'inbox_message', 'task_comment', ]); diff --git a/test/renderer/features/agent-graph/buildInlineActivityEntries.test.ts b/test/renderer/features/agent-graph/buildInlineActivityEntries.test.ts index 9c8c56b8..495da788 100644 --- a/test/renderer/features/agent-graph/buildInlineActivityEntries.test.ts +++ b/test/renderer/features/agent-graph/buildInlineActivityEntries.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { buildInlineActivityEntries, getGraphLeadMemberName, -} from '@renderer/features/agent-graph/utils/buildInlineActivityEntries'; +} from '@features/agent-graph/core/domain/buildInlineActivityEntries'; import type { InboxMessage, TeamData, TeamTaskWithKanban } from '@shared/types/team'; diff --git a/test/renderer/features/agent-graph/collapseOverflowStacks.test.ts b/test/renderer/features/agent-graph/collapseOverflowStacks.test.ts index a3e637b5..d712659f 100644 --- a/test/renderer/features/agent-graph/collapseOverflowStacks.test.ts +++ b/test/renderer/features/agent-graph/collapseOverflowStacks.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { collapseOverflowStacks, collapseOverflowStacksWithMeta, -} from '@renderer/features/agent-graph/utils/collapseOverflowStacks'; +} from '@features/agent-graph/core/domain/collapseOverflowStacks'; import type { GraphNode } from '@claude-teams/agent-graph';