diff --git a/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts b/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts new file mode 100644 index 00000000..6ac0fdad --- /dev/null +++ b/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts @@ -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, + })) + ); +} diff --git a/src/features/agent-graph/renderer/index.ts b/src/features/agent-graph/renderer/index.ts index fca5e786..91f45840 100644 --- a/src/features/agent-graph/renderer/index.ts +++ b/src/features/agent-graph/renderer/index.ts @@ -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'; diff --git a/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx index ff74df0f..5b4d5c82 100644 --- a/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx @@ -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] diff --git a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx index ffb7d100..afc454ac 100644 --- a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx @@ -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 diff --git a/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx b/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx index 903fdeea..a051ba17 100644 --- a/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx +++ b/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx @@ -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(); diff --git a/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts b/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts index fadaa318..e4d1d935 100644 --- a/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts +++ b/src/features/tmux-installer/main/adapters/output/runtime/__tests__/TmuxInstallerRunnerAdapter.test.ts @@ -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 ); diff --git a/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts b/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts index 0d90b6da..74e2092f 100644 --- a/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts +++ b/src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts @@ -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 { diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index c6d9dee7..f35e2628 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -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, diff --git a/src/main/services/infrastructure/NotificationManager.ts b/src/main/services/infrastructure/NotificationManager.ts index 095966c4..1f493c96 100644 --- a/src/main/services/infrastructure/NotificationManager.ts +++ b/src/main/services/infrastructure/NotificationManager.ts @@ -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'; diff --git a/src/main/services/team/index.ts b/src/main/services/team/index.ts index ed3deb52..3223ab4a 100644 --- a/src/main/services/team/index.ts +++ b/src/main/services/team/index.ts @@ -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'; diff --git a/src/main/services/team/taskLogs/activity/BoardTaskActivityDetailService.ts b/src/main/services/team/taskLogs/activity/BoardTaskActivityDetailService.ts index dd5805cc..f3ff491d 100644 --- a/src/main/services/team/taskLogs/activity/BoardTaskActivityDetailService.ts +++ b/src/main/services/team/taskLogs/activity/BoardTaskActivityDetailService.ts @@ -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(block: T): T { } as T; } - return { ...block } as T; + return { ...block }; } function sanitizeToolResultContent( diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index 016db015..5f2df4aa 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -70,9 +70,9 @@ import type { TmuxAPI, TmuxStatus, TriggerTestResult, - UpdateSchedulePatch, UpdateKanbanPatch, UpdaterAPI, + UpdateSchedulePatch, WaterfallData, WslClaudeRootCandidate, } from '@shared/types'; diff --git a/src/renderer/components/team/members/MemberDetailDialog.tsx b/src/renderer/components/team/members/MemberDetailDialog.tsx index 26336c43..258a2357 100644 --- a/src/renderer/components/team/members/MemberDetailDialog.tsx +++ b/src/renderer/components/team/members/MemberDetailDialog.tsx @@ -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'; diff --git a/src/renderer/components/team/taskLogs/TaskActivitySection.tsx b/src/renderer/components/team/taskLogs/TaskActivitySection.tsx index ee29d864..5a99c65e 100644 --- a/src/renderer/components/team/taskLogs/TaskActivitySection.tsx +++ b/src/renderer/components/team/taskLogs/TaskActivitySection.tsx @@ -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} ); -} +}; -function ActivityDetailPanel({ - detailState, -}: { - detailState: ActivityDetailState; -}): React.JSX.Element { +const ActivityDetailPanel = ({ detailState }: ActivityDetailPanelProps): React.JSX.Element => { if (detailState.status === 'loading') { return ( -
+
Loading activity details...
@@ -182,7 +182,7 @@ function ActivityDetailPanel({ if (detailState.status === 'missing') { return ( -
+
Detailed transcript context is no longer available for this activity.
); @@ -202,7 +202,7 @@ function ActivityDetailPanel({ {linkedTool ? : null}
); -} +}; const Row = ({ detailState, diff --git a/src/renderer/components/team/taskLogs/TaskLogsPanel.tsx b/src/renderer/components/team/taskLogs/TaskLogsPanel.tsx index 5bd7913e..0d364575 100644 --- a/src/renderer/components/team/taskLogs/TaskLogsPanel.tsx +++ b/src/renderer/components/team/taskLogs/TaskLogsPanel.tsx @@ -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'; diff --git a/src/renderer/store/slices/extensionsSlice.ts b/src/renderer/store/slices/extensionsSlice.ts index 58063575..dbb32224 100644 --- a/src/renderer/store/slices/extensionsSlice.ts +++ b/src/renderer/store/slices/extensionsSlice.ts @@ -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, diff --git a/test/main/services/team/TeamMemberResolver.test.ts b/test/main/services/team/TeamMemberResolver.test.ts index e0718abf..1a0b855c 100644 --- a/test/main/services/team/TeamMemberResolver.test.ts +++ b/test/main/services/team/TeamMemberResolver.test.ts @@ -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');