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:
parent
aed08113e6
commit
03dda6b486
17 changed files with 73 additions and 64 deletions
|
|
@ -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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ import type {
|
|||
TmuxAPI,
|
||||
TmuxStatus,
|
||||
TriggerTestResult,
|
||||
UpdateSchedulePatch,
|
||||
UpdateKanbanPatch,
|
||||
UpdaterAPI,
|
||||
UpdateSchedulePatch,
|
||||
WaterfallData,
|
||||
WslClaudeRootCandidate,
|
||||
} from '@shared/types';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue