refactor(agent-graph): replace store usage with context hooks for team data retrieval

- Updated components in the agent-graph renderer to utilize context hooks instead of the store for accessing team data.
- Introduced `useGraphActivityContext` and `useGraphMemberPopoverContext` hooks to streamline data management.
- Refactored `GraphBlockingEdgePopover`, `GraphNodePopover`, and `GraphTaskCard` components for improved performance and readability.
- Enhanced imports in `MemberDetailDialog` for better organization.
This commit is contained in:
777genius 2026-04-15 16:42:05 +03:00
parent aed08113e6
commit 03dda6b486
17 changed files with 73 additions and 64 deletions

View file

@ -0,0 +1,19 @@
import { useStore } from '@renderer/store';
import {
getCurrentProvisioningProgressForTeam,
selectTeamDataForName,
} from '@renderer/store/slices/teamSlice';
import { useShallow } from 'zustand/react/shallow';
export function useGraphMemberPopoverContext(teamName: string, memberName: string) {
return useStore(
useShallow((state) => ({
teamData: teamName ? selectTeamDataForName(state, teamName) : null,
spawnEntry: teamName ? state.memberSpawnStatusesByTeam[teamName]?.[memberName] : undefined,
leadActivity: teamName ? state.leadActivityByTeam[teamName] : undefined,
progress: teamName ? getCurrentProvisioningProgressForTeam(state, teamName) : null,
memberSpawnSnapshot: teamName ? state.memberSpawnSnapshotsByTeam[teamName] : undefined,
memberSpawnStatuses: teamName ? state.memberSpawnStatusesByTeam[teamName] : undefined,
}))
);
}

View file

@ -6,6 +6,7 @@
*/
export { buildInlineActivityEntries } from '../core/domain/buildInlineActivityEntries';
export { buildGraphMemberNodeIdForMember } from '../core/domain/graphOwnerIdentity';
export { TeamGraphAdapter } from './adapters/TeamGraphAdapter';
export type { TeamGraphOverlayProps } from './ui/TeamGraphOverlay';
export { TeamGraphOverlay } from './ui/TeamGraphOverlay';

View file

@ -2,8 +2,8 @@ import { useMemo } from 'react';
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { useStore } from '@renderer/store';
import { selectTeamDataForName } from '@renderer/store/slices/teamSlice';
import { useGraphActivityContext } from '../hooks/useGraphActivityContext';
import type { GraphEdge, GraphNode } from '@claude-teams/agent-graph';
import type { TeamTaskWithKanban } from '@shared/types';
@ -63,7 +63,7 @@ export const GraphBlockingEdgePopover = ({
onSelectNode,
onOpenTaskDetail,
}: GraphBlockingEdgePopoverProps): React.JSX.Element => {
const teamData = useStore((state) => selectTeamDataForName(state, teamName));
const { teamData } = useGraphActivityContext(teamName);
const tasksById = useMemo(
() => new Map((teamData?.tasks ?? []).map((task) => [task.id, task] as const)),
[teamData?.tasks]

View file

@ -6,17 +6,13 @@
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { useStore } from '@renderer/store';
import {
getCurrentProvisioningProgressForTeam,
selectTeamDataForName,
} from '@renderer/store/slices/teamSlice';
import { agentAvatarUrl, buildMemberLaunchPresentation } from '@renderer/utils/memberHelpers';
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics';
import { useGraphActivityContext } from '../hooks/useGraphActivityContext';
import { useGraphMemberPopoverContext } from '../hooks/useGraphMemberPopoverContext';
import { GraphTaskCard } from './GraphTaskCard';
@ -213,7 +209,7 @@ const OverflowPopoverContent = ({
onClose: () => void;
onOpenTaskDetail?: (taskId: string) => void;
}): React.JSX.Element => {
const teamData = useStore((state) => selectTeamDataForName(state, teamName));
const { teamData } = useGraphActivityContext(teamName);
const tasksById = new Map((teamData?.tasks ?? []).map((task) => [task.id, task]));
const hiddenTasks = (node.overflowTaskIds ?? [])
.map((taskId) => tasksById.get(taskId) ?? null)
@ -297,16 +293,7 @@ const MemberPopoverContent = ({
: '';
const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64);
const { teamData, spawnEntry, leadActivity, progress, memberSpawnSnapshot, memberSpawnStatuses } =
useStore(
useShallow((state) => ({
teamData: teamName ? selectTeamDataForName(state, teamName) : null,
spawnEntry: teamName ? state.memberSpawnStatusesByTeam[teamName]?.[memberName] : undefined,
leadActivity: teamName ? state.leadActivityByTeam[teamName] : undefined,
progress: teamName ? getCurrentProvisioningProgressForTeam(state, teamName) : null,
memberSpawnSnapshot: teamName ? state.memberSpawnSnapshotsByTeam[teamName] : undefined,
memberSpawnStatuses: teamName ? state.memberSpawnStatusesByTeam[teamName] : undefined,
}))
);
useGraphMemberPopoverContext(teamName, memberName);
const member = teamData?.members.find((candidate) => candidate.name === memberName) ?? null;
const provisioningPresentation =
teamData && teamName

View file

@ -7,11 +7,9 @@
import { useMemo } from 'react';
import { KanbanTaskCard } from '@renderer/components/team/kanban/KanbanTaskCard';
import { useStore } from '@renderer/store';
import { selectTeamDataForName } from '@renderer/store/slices/teamSlice';
import { useShallow } from 'zustand/react/shallow';
import { isTaskBlocked, resolveTaskGraphColumn } from '../../core/domain/taskGraphSemantics';
import { useGraphActivityContext } from '../hooks/useGraphActivityContext';
import type { GraphNode } from '@claude-teams/agent-graph';
import type { KanbanColumnId, TeamTask } from '@shared/types';
@ -84,14 +82,10 @@ export const GraphTaskCard = ({
onDeleteTask,
}: GraphTaskCardProps): React.JSX.Element => {
const taskId = node.domainRef.kind === 'task' ? node.domainRef.taskId : '';
const { task, tasks, members } = useStore(
useShallow((s) => ({
tasks: selectTeamDataForName(s, teamName)?.tasks ?? [],
members: selectTeamDataForName(s, teamName)?.members ?? [],
task: selectTeamDataForName(s, teamName)?.tasks.find((t) => t.id === taskId),
}))
);
const { teamData } = useGraphActivityContext(teamName);
const tasks = teamData?.tasks ?? [];
const members = teamData?.members ?? [];
const task = tasks.find((candidate) => candidate.id === taskId);
const taskMap = useMemo(() => {
const map = new Map<string, TeamTask>();

View file

@ -1,3 +1,5 @@
import path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { TmuxInstallerRunnerAdapter } from '../TmuxInstallerRunnerAdapter';
@ -556,7 +558,7 @@ describe('TmuxInstallerRunnerAdapter', () => {
restartRequired: 'Possible',
},
],
resultFilePath: 'C:\\temp\\result.json',
resultFilePath: path.join(process.cwd(), 'tmp', 'result.json'),
})),
} as never
);

View file

@ -3,10 +3,10 @@ import * as fsp from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { decodeInstallerProcessOutput } from '../runtime/decodeInstallerProcessOutput';
import { createLogger } from '@shared/utils/logger';
import { decodeInstallerProcessOutput } from '../runtime/decodeInstallerProcessOutput';
const logger = createLogger('Feature:tmux-installer:windows-elevation');
interface ExecResult {

View file

@ -122,8 +122,8 @@ import {
} from './guards';
import type {
BoardTaskActivityService,
BoardTaskActivityDetailService,
BoardTaskActivityService,
BoardTaskExactLogDetailService,
BoardTaskExactLogsService,
BoardTaskLogStreamService,
@ -141,8 +141,8 @@ import type {
AttachmentFileData,
AttachmentMeta,
AttachmentPayload,
BoardTaskActivityEntry,
BoardTaskActivityDetailResult,
BoardTaskActivityEntry,
BoardTaskExactLogDetailResult,
BoardTaskExactLogSummariesResponse,
BoardTaskLogStreamResponse,

View file

@ -21,8 +21,8 @@ import { safeSendToRenderer } from '@main/utils/safeWebContentsSend';
import { stripMarkdown } from '@main/utils/textFormatting';
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { createLogger } from '@shared/utils/logger';
import { EventEmitter } from 'events';
import { Notification as ElectronNotification } from 'electron';
import { EventEmitter } from 'events';
import * as fsp from 'fs/promises';
import * as path from 'path';

View file

@ -10,8 +10,8 @@ export { HunkSnippetMatcher } from './HunkSnippetMatcher';
export { MemberStatsComputer } from './MemberStatsComputer';
export { ReviewApplierService } from './ReviewApplierService';
export { TaskBoundaryParser } from './TaskBoundaryParser';
export { BoardTaskActivityRecordSource } from './taskLogs/activity/BoardTaskActivityRecordSource';
export { BoardTaskActivityDetailService } from './taskLogs/activity/BoardTaskActivityDetailService';
export { BoardTaskActivityRecordSource } from './taskLogs/activity/BoardTaskActivityRecordSource';
export { BoardTaskActivityService } from './taskLogs/activity/BoardTaskActivityService';
export { BoardTaskExactLogDetailService } from './taskLogs/exact/BoardTaskExactLogDetailService';
export { BoardTaskExactLogsService } from './taskLogs/exact/BoardTaskExactLogsService';

View file

@ -1,3 +1,4 @@
import { isEnhancedAIChunk } from '@main/types';
import {
describeBoardTaskActivityLabel,
formatBoardTaskActivityTaskLabel,
@ -6,21 +7,21 @@ import {
describeBoardTaskActivityActorLabel,
describeBoardTaskActivityContextLines,
} from '@shared/utils/boardTaskActivityPresentation';
import { isEnhancedAIChunk } from '@main/types';
import { BoardTaskActivityRecordSource } from './BoardTaskActivityRecordSource';
import { BoardTaskExactLogChunkBuilder } from '../exact/BoardTaskExactLogChunkBuilder';
import { BoardTaskExactLogDetailSelector } from '../exact/BoardTaskExactLogDetailSelector';
import { BoardTaskExactLogStrictParser } from '../exact/BoardTaskExactLogStrictParser';
import { BoardTaskActivityRecordSource } from './BoardTaskActivityRecordSource';
import type { BoardTaskExactLogBundleCandidate } from '../exact/BoardTaskExactLogTypes';
import type { BoardTaskActivityRecord } from './BoardTaskActivityRecord';
import type { ContentBlock, EnhancedChunk, ParsedMessage, ToolUseResultData } from '@main/types';
import type {
BoardTaskActivityDetail,
BoardTaskActivityDetailMetadataRow,
BoardTaskActivityDetailResult,
} from '@shared/types';
import type { BoardTaskExactLogBundleCandidate } from '../exact/BoardTaskExactLogTypes';
import type { ContentBlock, EnhancedChunk, ParsedMessage, ToolUseResultData } from '@main/types';
const READ_ONLY_TOOL_NAMES = new Set(['task_get', 'task_get_comment']);
@ -236,7 +237,7 @@ function cloneBlock<T extends ContentBlock>(block: T): T {
} as T;
}
return { ...block } as T;
return { ...block };
}
function sanitizeToolResultContent(

View file

@ -70,9 +70,9 @@ import type {
TmuxAPI,
TmuxStatus,
TriggerTestResult,
UpdateSchedulePatch,
UpdateKanbanPatch,
UpdaterAPI,
UpdateSchedulePatch,
WaterfallData,
WslClaudeRootCandidate,
} from '@shared/types';

View file

@ -1,7 +1,9 @@
import { useEffect, useMemo, useState } from 'react';
import { buildGraphMemberNodeIdForMember } from '@features/agent-graph/core/domain/graphOwnerIdentity';
import { buildInlineActivityEntries } from '@features/agent-graph/renderer';
import {
buildGraphMemberNodeIdForMember,
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';

View file

@ -16,12 +16,12 @@ import { AlertCircle, ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
import { TaskActivityLinkedToolCard } from './TaskActivityLinkedToolCard';
import type { AIGroupDisplayItem, LinkedToolItem } from '@renderer/types/groups';
import type {
BoardTaskActivityDetail,
BoardTaskActivityEntry,
BoardTaskActivityTaskRef,
} from '@shared/types';
import type { AIGroupDisplayItem, LinkedToolItem } from '@renderer/types/groups';
interface TaskActivitySectionProps {
teamName: string;
@ -80,6 +80,14 @@ type ActivityDetailState =
| { status: 'error'; error: string }
| { status: 'ok'; detail: BoardTaskActivityDetail };
type ActivityMetadataProps = Readonly<{
detail: BoardTaskActivityDetail;
}>;
type ActivityDetailPanelProps = Readonly<{
detailState: ActivityDetailState;
}>;
function normalizeDetail(detail: BoardTaskActivityDetail): BoardTaskActivityDetail {
if (!detail.logDetail) {
return detail;
@ -117,11 +125,7 @@ function getFirstRenderableLinkedTool(detail: BoardTaskActivityDetail): LinkedTo
return null;
}
function ActivityMetadata({
detail,
}: {
detail: BoardTaskActivityDetail;
}): React.JSX.Element | null {
const ActivityMetadata = ({ detail }: ActivityMetadataProps): React.JSX.Element | null => {
const hasMetadata = detail.metadataRows.length > 0;
const hasContext = detail.contextLines.length > 0;
@ -155,16 +159,12 @@ function ActivityMetadata({
) : null}
</div>
);
}
};
function ActivityDetailPanel({
detailState,
}: {
detailState: ActivityDetailState;
}): React.JSX.Element {
const ActivityDetailPanel = ({ detailState }: ActivityDetailPanelProps): React.JSX.Element => {
if (detailState.status === 'loading') {
return (
<div className="border-[var(--color-border)]/20 bg-[var(--color-bg-elevated)]/18 flex items-center gap-2 rounded-md border px-3 py-3 text-xs text-[var(--color-text-muted)]">
<div className="border-[var(--color-border)]/20 bg-[var(--color-bg-elevated)]/18 flex items-center gap-2 rounded-md border p-3 text-xs text-[var(--color-text-muted)]">
<Loader2 size={12} className="animate-spin" />
Loading activity details...
</div>
@ -182,7 +182,7 @@ function ActivityDetailPanel({
if (detailState.status === 'missing') {
return (
<div className="border-[var(--color-border)]/20 bg-[var(--color-bg-elevated)]/18 rounded-md border px-3 py-3 text-xs text-[var(--color-text-muted)]">
<div className="border-[var(--color-border)]/20 bg-[var(--color-bg-elevated)]/18 rounded-md border p-3 text-xs text-[var(--color-text-muted)]">
Detailed transcript context is no longer available for this activity.
</div>
);
@ -202,7 +202,7 @@ function ActivityDetailPanel({
{linkedTool ? <TaskActivityLinkedToolCard linkedTool={linkedTool} /> : null}
</div>
);
}
};
const Row = ({
detailState,

View file

@ -1,10 +1,11 @@
import { useEffect, useMemo, useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
import { ExecutionSessionsSection } from './ExecutionSessionsSection';
import { isBoardTaskActivityUiEnabled, isBoardTaskExactLogsUiEnabled } from './featureGates';
import { TaskActivitySection } from './TaskActivitySection';
import { TaskLogStreamSection } from './TaskLogStreamSection';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -6,8 +6,9 @@
import { api } from '@renderer/api';
import { CLI_NOT_FOUND_MESSAGE } from '@shared/constants/cli';
import type { AppState } from '../types';
import { findPaneByTabId, updatePane } from '../utils/paneHelpers';
import type { AppState } from '../types';
import type {
ApiKeyEntry,
ApiKeySaveRequest,

View file

@ -33,7 +33,8 @@ describe('TeamMemberResolver', () => {
const members = resolver.resolveMembers(config, metaMembers, inboxNames, tasks, messages);
const names = members.map((member) => member.name);
expect(names).toEqual(['alice', 'bob', 'team-lead']);
expect(names).toHaveLength(3);
expect(names).toEqual(expect.arrayContaining(['alice', 'bob', 'team-lead']));
expect(names).not.toContain('stranger');
expect(names).not.toContain('user');