refactor: migrate agent graph to feature slice
This commit is contained in:
parent
3fddb4eafd
commit
c8f9d9bbdd
30 changed files with 203 additions and 65 deletions
|
|
@ -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 use case
|
||||||
- not adding a new transport boundary
|
- 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
|
## Definition Of Done For A Reference Feature
|
||||||
|
|
||||||
A feature is reference-quality when:
|
A feature is reference-quality when:
|
||||||
|
|
@ -295,3 +301,15 @@ Use it as the example for:
|
||||||
- renderer dumb UI + hook orchestration
|
- renderer dumb UI + hook orchestration
|
||||||
- browser-friendly transport direction
|
- browser-friendly transport direction
|
||||||
- feature-level lint guard rails
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Module boundaries - Enforce Electron three-process architecture
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ Before creating or refactoring a feature, read:
|
||||||
|
|
||||||
Reference implementation:
|
Reference implementation:
|
||||||
- `src/features/recent-projects`
|
- `src/features/recent-projects`
|
||||||
|
- `src/features/agent-graph`
|
||||||
|
|
||||||
Use `src/features/<feature-name>/` by default when the work introduces:
|
Use `src/features/<feature-name>/` by default when the work introduces:
|
||||||
- a new use case or business policy
|
- a new use case or business policy
|
||||||
|
|
@ -17,3 +18,7 @@ Use `src/features/<feature-name>/` by default when the work introduces:
|
||||||
|
|
||||||
Do not duplicate architecture rules in feature folders.
|
Do not duplicate architecture rules in feature folders.
|
||||||
Keep the standard centralized in [../../docs/FEATURE_ARCHITECTURE_STANDARD.md](../../docs/FEATURE_ARCHITECTURE_STANDARD.md).
|
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/`
|
||||||
|
|
|
||||||
20
src/features/agent-graph/README.md
Normal file
20
src/features/agent-graph/README.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -160,7 +160,7 @@ export function buildInlineActivityEntries({
|
||||||
for (const [ownerNodeId, entries] of entriesByOwnerNodeId) {
|
for (const [ownerNodeId, entries] of entriesByOwnerNodeId) {
|
||||||
entriesByOwnerNodeId.set(
|
entriesByOwnerNodeId.set(
|
||||||
ownerNodeId,
|
ownerNodeId,
|
||||||
entries.sort((a, b) => b.graphItem.timestamp.localeCompare(a.graphItem.timestamp))
|
entries.toSorted((a, b) => b.graphItem.timestamp.localeCompare(a.graphItem.timestamp))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* TeamGraphAdapter — transforms Zustand TeamData → GraphDataPort.
|
* TeamGraphAdapter — transforms Zustand TeamData → GraphDataPort.
|
||||||
*
|
*
|
||||||
* This is the ONLY file in this feature that imports from @renderer/store.
|
* This adapter owns the graph projection from team runtime state into the
|
||||||
* If the project data model changes, ONLY this class needs updating.
|
* 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.
|
* Class-based with ES #private fields and DI-ready constructor.
|
||||||
*/
|
*/
|
||||||
|
|
@ -26,16 +27,15 @@ import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import {
|
import {
|
||||||
buildInlineActivityEntries,
|
buildInlineActivityEntries,
|
||||||
getGraphLeadMemberName,
|
getGraphLeadMemberName,
|
||||||
} from '../utils/buildInlineActivityEntries';
|
} from '../../core/domain/buildInlineActivityEntries';
|
||||||
import { collapseOverflowStacksWithMeta } from '../utils/collapseOverflowStacks';
|
import { collapseOverflowStacksWithMeta } from '../../core/domain/collapseOverflowStacks';
|
||||||
import {
|
import {
|
||||||
isTaskBlocked,
|
isTaskBlocked,
|
||||||
isTaskInReviewCycle,
|
isTaskInReviewCycle,
|
||||||
resolveTaskReviewer,
|
resolveTaskReviewer,
|
||||||
} from '../utils/taskGraphSemantics';
|
} from '../../core/domain/taskGraphSemantics';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GraphActivityItem,
|
|
||||||
GraphDataPort,
|
GraphDataPort,
|
||||||
GraphEdge,
|
GraphEdge,
|
||||||
GraphNode,
|
GraphNode,
|
||||||
|
|
@ -571,7 +571,10 @@ export class TeamGraphAdapter {
|
||||||
|
|
||||||
for (const relatedId of task.related ?? []) {
|
for (const relatedId of task.related ?? []) {
|
||||||
if (!visibleTaskIds.has(relatedId)) continue;
|
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;
|
if (this.#seenRelated.has(key)) continue;
|
||||||
this.#seenRelated.add(key);
|
this.#seenRelated.add(key);
|
||||||
edges.push({
|
edges.push({
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from '@renderer/store/slices/teamSlice';
|
} from '@renderer/store/slices/teamSlice';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { TeamGraphAdapter } from './TeamGraphAdapter';
|
import { TeamGraphAdapter } from '../adapters/TeamGraphAdapter';
|
||||||
|
|
||||||
import type { GraphDataPort } from '@claude-teams/agent-graph';
|
import type { GraphDataPort } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
12
src/features/agent-graph/renderer/index.ts
Normal file
12
src/features/agent-graph/renderer/index.ts
Normal file
|
|
@ -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';
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
buildInlineActivityEntries,
|
buildInlineActivityEntries,
|
||||||
getGraphLeadMemberName,
|
getGraphLeadMemberName,
|
||||||
type InlineActivityEntry,
|
type InlineActivityEntry,
|
||||||
} from '../utils/buildInlineActivityEntries';
|
} from '../../core/domain/buildInlineActivityEntries';
|
||||||
|
|
||||||
import type { GraphNode } from '@claude-teams/agent-graph';
|
import type { GraphNode } from '@claude-teams/agent-graph';
|
||||||
import type { TimelineItem } from '@renderer/components/team/activity/LeadThoughtsGroup';
|
import type { TimelineItem } from '@renderer/components/team/activity/LeadThoughtsGroup';
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* GraphNodePopover — renders popover for graph nodes using project UI components.
|
* GraphNodePopover — renders popover for graph nodes using project UI components.
|
||||||
* Lives in features/ (not in package) so it CAN import from @renderer/.
|
* This stays in the renderer slice instead of the reusable package because it
|
||||||
* Reuses agentAvatarUrl, status helpers, and UI primitives from the project.
|
* composes project-specific UI, selectors, and presentation helpers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Badge } from '@renderer/components/ui/badge';
|
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 { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { isTaskInReviewCycle, resolveTaskReviewer } from '../utils/taskGraphSemantics';
|
import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics';
|
||||||
|
|
||||||
import { GraphTaskCard } from './GraphTaskCard';
|
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 it looks like raw JSON object, try to extract a readable field
|
||||||
if (preview.startsWith('{') || preview.startsWith('[')) {
|
if (preview.startsWith('{') || preview.startsWith('[')) {
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(preview.length > 200 ? preview.slice(0, 200) : preview);
|
const parsed: unknown = JSON.parse(preview.length > 200 ? preview.slice(0, 200) : preview);
|
||||||
// Common readable fields
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
return (
|
const previewRecord = parsed as Record<string, unknown>;
|
||||||
obj.subject ?? obj.name ?? obj.label ?? obj.file_path ?? obj.path ?? obj.query ?? undefined
|
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 {
|
} catch {
|
||||||
// Truncated JSON — extract first quoted value
|
// Truncated JSON — extract first quoted value
|
||||||
const match = /"(?:subject|name|label|path|query)":\s*"([^"]{1,60})"/.exec(preview);
|
const match = /"(?:subject|name|label|path|query)":\s*"([^"]{1,60})"/.exec(preview);
|
||||||
|
|
@ -421,7 +432,7 @@ const MemberPopoverContent = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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 */}
|
{/* Current task indicator — reuses same pattern as MemberCard */}
|
||||||
{node.currentTaskId && node.currentTaskSubject && (
|
{node.currentTaskId && node.currentTaskSubject && (
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* GraphTaskCard — wraps the REAL KanbanTaskCard with graph-specific glow/pulse effects.
|
* 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';
|
import { useMemo } from 'react';
|
||||||
|
|
@ -10,10 +11,10 @@ import { useStore } from '@renderer/store';
|
||||||
import { selectTeamDataForName } from '@renderer/store/slices/teamSlice';
|
import { selectTeamDataForName } from '@renderer/store/slices/teamSlice';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
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 { GraphNode } from '@claude-teams/agent-graph';
|
||||||
import type { KanbanColumnId, TeamTask, TeamTaskWithKanban } from '@shared/types';
|
import type { KanbanColumnId, TeamTask } from '@shared/types';
|
||||||
|
|
||||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -119,8 +120,8 @@ export const GraphTaskCard = ({
|
||||||
const columnId = resolveColumn(task);
|
const columnId = resolveColumn(task);
|
||||||
const taskWithKanban = task;
|
const taskWithKanban = task;
|
||||||
|
|
||||||
const closeAct = (fn?: (id: string) => void) => (taskId: string) => {
|
const closeAct = (fn?: (id: string) => void) => (nextTaskId: string) => {
|
||||||
fn?.(taskId);
|
fn?.(nextTaskId);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -9,13 +9,13 @@ import { GraphView } from '@claude-teams/agent-graph';
|
||||||
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
|
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
|
||||||
import { useStore } from '@renderer/store';
|
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 { GraphActivityHud } from './GraphActivityHud';
|
||||||
import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover';
|
import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover';
|
||||||
import { GraphNodePopover } from './GraphNodePopover';
|
import { GraphNodePopover } from './GraphNodePopover';
|
||||||
import { GraphProvisioningHud } from './GraphProvisioningHud';
|
import { GraphProvisioningHud } from './GraphProvisioningHud';
|
||||||
import { useGraphCreateTaskDialog } from './useGraphCreateTaskDialog';
|
|
||||||
|
|
||||||
import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph';
|
import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph';
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -9,15 +9,15 @@ import { GraphView } from '@claude-teams/agent-graph';
|
||||||
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
|
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
|
||||||
import { useStore } from '@renderer/store';
|
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 { GraphActivityHud } from './GraphActivityHud';
|
||||||
import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover';
|
import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover';
|
||||||
import { GraphNodePopover } from './GraphNodePopover';
|
import { GraphNodePopover } from './GraphNodePopover';
|
||||||
import { GraphProvisioningHud } from './GraphProvisioningHud';
|
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 {
|
import type {
|
||||||
MemberActivityFilter,
|
MemberActivityFilter,
|
||||||
MemberDetailTab,
|
MemberDetailTab,
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
* Uses CSS display-toggle to keep all tabs mounted (preserving state).
|
* 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 { TabUIProvider } from '@renderer/contexts/TabUIContext';
|
||||||
import { TeamGraphTab } from '@renderer/features/agent-graph/ui/TeamGraphTab';
|
|
||||||
|
|
||||||
import { DashboardView } from '../dashboard/DashboardView';
|
import { DashboardView } from '../dashboard/DashboardView';
|
||||||
import { ExtensionStoreView } from '../extensions/ExtensionStoreView';
|
import { ExtensionStoreView } from '../extensions/ExtensionStoreView';
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,15 @@ import { TrashDialog } from './kanban/TrashDialog';
|
||||||
import { MemberDetailDialog } from './members/MemberDetailDialog';
|
import { MemberDetailDialog } from './members/MemberDetailDialog';
|
||||||
import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes';
|
import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes';
|
||||||
|
|
||||||
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
|
|
||||||
import type { AddMemberEntry } from './dialogs/AddMemberDialog';
|
import type { AddMemberEntry } from './dialogs/AddMemberDialog';
|
||||||
|
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
|
||||||
import type { ComponentProps, CSSProperties } from 'react';
|
import type { ComponentProps, CSSProperties } from 'react';
|
||||||
|
|
||||||
const ProjectEditorOverlay = lazy(() =>
|
const ProjectEditorOverlay = lazy(() =>
|
||||||
import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay }))
|
import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay }))
|
||||||
);
|
);
|
||||||
const TeamGraphOverlay = lazy(() =>
|
const TeamGraphOverlay = lazy(() =>
|
||||||
import('@renderer/features/agent-graph/ui/TeamGraphOverlay').then((m) => ({
|
import('@features/agent-graph/renderer').then((m) => ({
|
||||||
default: m.TeamGraphOverlay,
|
default: m.TeamGraphOverlay,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { buildInlineActivityEntries } from '@features/agent-graph/renderer';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
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 { useMemberStats } from '@renderer/hooks/useMemberStats';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
|
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { buildInlineActivityEntries } from '@features/agent-graph/renderer';
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
import { ActivityItem } from '@renderer/components/team/activity/ActivityItem';
|
import { ActivityItem } from '@renderer/components/team/activity/ActivityItem';
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,7 +9,6 @@ import {
|
||||||
} from '@renderer/components/team/activity/activityMessageContext';
|
} from '@renderer/components/team/activity/activityMessageContext';
|
||||||
import { MessageExpandDialog } from '@renderer/components/team/activity/MessageExpandDialog';
|
import { MessageExpandDialog } from '@renderer/components/team/activity/MessageExpandDialog';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { buildInlineActivityEntries } from '@renderer/features/agent-graph/utils/buildInlineActivityEntries';
|
|
||||||
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
||||||
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
|
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
|
||||||
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { act } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
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 { GraphNode } from '@claude-teams/agent-graph';
|
||||||
import type { InboxMessage } from '@shared/types/team';
|
import type { InboxMessage } from '@shared/types/team';
|
||||||
|
|
@ -17,16 +17,22 @@ const teamState = {
|
||||||
tasks: [],
|
tasks: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
},
|
},
|
||||||
teamDataCacheByName: {
|
teamDataCacheByName: new Map<
|
||||||
'demo-team': {
|
string,
|
||||||
members: [
|
{ members: Record<string, unknown>[]; tasks: unknown[]; messages: unknown[] }
|
||||||
{ name: 'team-lead', agentType: 'team-lead' },
|
>([
|
||||||
{ name: 'jack', agentType: 'developer' },
|
[
|
||||||
],
|
'demo-team',
|
||||||
tasks: [],
|
{
|
||||||
messages: [],
|
members: [
|
||||||
},
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
} as Record<string, { members: Array<Record<string, unknown>>; tasks: unknown[]; messages: unknown[] }>,
|
{ name: 'jack', agentType: 'developer' },
|
||||||
|
],
|
||||||
|
tasks: [],
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
teams: [],
|
teams: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -38,7 +44,7 @@ vi.mock('@renderer/store', () => ({
|
||||||
|
|
||||||
vi.mock('@renderer/store/slices/teamSlice', () => ({
|
vi.mock('@renderer/store/slices/teamSlice', () => ({
|
||||||
selectTeamDataForName: (_state: typeof teamState, teamName: string) =>
|
selectTeamDataForName: (_state: typeof teamState, teamName: string) =>
|
||||||
teamState.teamDataCacheByName[teamName] ??
|
teamState.teamDataCacheByName.get(teamName) ??
|
||||||
(teamState.selectedTeamName === teamName ? teamState.selectedTeamData : null),
|
(teamState.selectedTeamName === teamName ? teamState.selectedTeamData : null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -79,7 +85,7 @@ vi.mock('@renderer/components/team/activity/activityMessageContext', () => ({
|
||||||
resolveMessageRenderProps: () => ({}),
|
resolveMessageRenderProps: () => ({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/features/agent-graph/utils/buildInlineActivityEntries', () => ({
|
vi.mock('@features/agent-graph/core/domain/buildInlineActivityEntries', () => ({
|
||||||
buildInlineActivityEntries: (...args: unknown[]) => buildInlineActivityEntries(...args),
|
buildInlineActivityEntries: (...args: unknown[]) => buildInlineActivityEntries(...args),
|
||||||
getGraphLeadMemberName: () => 'team-lead',
|
getGraphLeadMemberName: () => 'team-lead',
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import React, { act } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { GraphBlockingEdgePopover } from '@features/agent-graph/renderer/ui/GraphBlockingEdgePopover';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { GraphBlockingEdgePopover } from '@renderer/features/agent-graph/ui/GraphBlockingEdgePopover';
|
|
||||||
|
|
||||||
import type { GraphEdge, GraphNode } from '@claude-teams/agent-graph';
|
import type { GraphEdge, GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ vi.mock('@renderer/components/ui/button', () => ({
|
||||||
React.createElement('button', { type: 'button' }, children),
|
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'),
|
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';
|
import type { GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { act } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { GraphProvisioningHud } from '@features/agent-graph/renderer/ui/GraphProvisioningHud';
|
||||||
|
|
||||||
const hookState = {
|
const hookState = {
|
||||||
presentation: null as
|
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 };
|
const placement = { x: 120, y: 80, scale: 1, visible: true };
|
||||||
|
|
||||||
describe('GraphProvisioningHud', () => {
|
describe('GraphProvisioningHud', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
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 { InboxMessage, TeamData, TeamTaskWithKanban } from '@shared/types/team';
|
||||||
import type { GraphDataPort } from '@claude-teams/agent-graph';
|
import type { GraphDataPort } from '@claude-teams/agent-graph';
|
||||||
|
|
@ -459,7 +459,7 @@ describe('TeamGraphAdapter particles', () => {
|
||||||
const graph = adapter.adapt(next, 'my-team');
|
const graph = adapter.adapt(next, 'my-team');
|
||||||
|
|
||||||
expect(graph.particles).toHaveLength(2);
|
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',
|
'inbox_message',
|
||||||
'task_comment',
|
'task_comment',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
buildInlineActivityEntries,
|
buildInlineActivityEntries,
|
||||||
getGraphLeadMemberName,
|
getGraphLeadMemberName,
|
||||||
} from '@renderer/features/agent-graph/utils/buildInlineActivityEntries';
|
} from '@features/agent-graph/core/domain/buildInlineActivityEntries';
|
||||||
|
|
||||||
import type { InboxMessage, TeamData, TeamTaskWithKanban } from '@shared/types/team';
|
import type { InboxMessage, TeamData, TeamTaskWithKanban } from '@shared/types/team';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
collapseOverflowStacks,
|
collapseOverflowStacks,
|
||||||
collapseOverflowStacksWithMeta,
|
collapseOverflowStacksWithMeta,
|
||||||
} from '@renderer/features/agent-graph/utils/collapseOverflowStacks';
|
} from '@features/agent-graph/core/domain/collapseOverflowStacks';
|
||||||
|
|
||||||
import type { GraphNode } from '@claude-teams/agent-graph';
|
import type { GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue