From 1b3bd8752bd510711f84bf5ff0f0342659432d84 Mon Sep 17 00:00:00 2001 From: iliya Date: Wed, 1 Apr 2026 19:11:51 +0300 Subject: [PATCH] refactor: remove enrichMemberBranches method and streamline branch tracking - Eliminated the enrichMemberBranches method from TeamDataService to simplify member branch enrichment logic. - Updated TeamDetailView to utilize live branch tracking for both lead and member worktrees, enhancing the accuracy of displayed member branches. - Adjusted various references to ensure membersWithLiveBranches is used consistently across the component. --- src/main/services/team/TeamDataService.ts | 74 +--------------- .../components/team/TeamDetailView.tsx | 84 +++++++++++++++---- 2 files changed, 71 insertions(+), 87 deletions(-) diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index 92bdbf5e..fbaf89b5 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -27,8 +27,6 @@ import { randomUUID } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; -import { gitIdentityResolver } from '../parsing/GitIdentityResolver'; - import { atomicWriteAsync } from './atomicWrite'; import { extractLeadSessionMessagesFromJsonl } from './leadSessionMessageExtractor'; import { buildTaskChangePresenceDescriptor } from './taskChangePresenceUtils'; @@ -691,10 +689,6 @@ export class TeamDataService { ); mark('resolveMembers'); - // Enrich members with git branch when it differs from lead's branch - await this.enrichMemberBranches(members, config); - mark('enrichBranches'); - mark('syncComments'); let processes: TeamProcess[] = []; @@ -714,9 +708,9 @@ export class TeamDataService { 'sentMessages' )} membersMeta=${msSince('metaMembers')} kanban=${msSince('kanbanState')} kanbanGc=${msSince( 'kanbanGc' - )} resolveMembers=${msSince('resolveMembers')} enrichBranches=${msSince( - 'enrichBranches' - )} syncComments=${msSince('syncComments')} processes=${msSince('processes')}` + )} resolveMembers=${msSince('resolveMembers')} syncComments=${msSince('syncComments')} processes=${msSince( + 'processes' + )}` ); } @@ -804,68 +798,6 @@ export class TeamDataService { } } - /** - * Enriches members with gitBranch when their cwd differs from the lead's. - * Mutates members in-place for efficiency (called right after resolveMembers). - */ - private async enrichMemberBranches( - members: ResolvedTeamMember[], - config: TeamConfig - ): Promise { - // Determine lead's cwd — prefer explicit member entry, fall back to config.projectPath - const leadEntry = config.members?.find((m) => isLeadMember(m)); - const leadCwd = leadEntry?.cwd ?? config.projectPath; - if (!leadCwd) return; - - const withTimeout = async (p: Promise, ms: number): Promise => { - let timer: NodeJS.Timeout | null = null; - try { - return await Promise.race([ - p, - new Promise((_resolve, reject) => { - timer = setTimeout(() => reject(new Error('timeout')), ms); - }), - ]); - } finally { - if (timer) clearTimeout(timer); - } - }; - - let leadBranch: string | null = null; - try { - // Git can hang on some Windows setups (network drives, locked repos, credential prompts). - // Branch is best-effort; never block team:getData on it. - leadBranch = await withTimeout(gitIdentityResolver.getBranch(path.normalize(leadCwd)), 2000); - } catch { - // Lead cwd may not be a git repo — skip enrichment entirely - return; - } - - const candidates = members.filter((m) => m.cwd && m.cwd !== leadCwd); - if (candidates.length === 0) return; - - const concurrency = process.platform === 'win32' ? 4 : 8; - for (let i = 0; i < candidates.length; i += concurrency) { - const batch = candidates.slice(i, i + concurrency); - await Promise.all( - batch.map(async (member) => { - if (!member.cwd) return; - try { - const branch = await withTimeout( - gitIdentityResolver.getBranch(path.normalize(member.cwd)), - 2000 - ); - if (branch && branch !== leadBranch) { - member.gitBranch = branch; - } - } catch { - // Member cwd may not be a git repo — skip silently - } - }) - ); - } - } - /** * Ensures a member exists in members.meta.json. * Members can appear in the UI from three sources (see TeamMemberResolver): diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index da7dec5a..ba39079f 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -771,17 +771,69 @@ export const TeamDetailView = ({ }; }, [projectId]); - // Live git branch polling for the team's project path + // Live git branch tracking for the lead project and member worktrees const teamProjectPath = data?.config.projectPath?.trim() ?? null; - const branchSyncPaths = useMemo( - () => (teamProjectPath ? [teamProjectPath] : []), - [teamProjectPath] - ); - // Live branch sync now uses main-side background tracking instead of renderer polling. + const leadProjectPath = useMemo(() => { + const explicitLeadPath = data?.members.find((member) => isLeadMember(member))?.cwd?.trim(); + return explicitLeadPath && explicitLeadPath.length > 0 ? explicitLeadPath : teamProjectPath; + }, [data?.members, teamProjectPath]); + const branchSyncPaths = useMemo(() => { + const uniquePaths = new Map(); + const addPath = (candidate: string | null | undefined): void => { + const trimmed = candidate?.trim(); + if (!trimmed) return; + const key = normalizePath(trimmed); + if (!key || uniquePaths.has(key)) return; + uniquePaths.set(key, trimmed); + }; + + addPath(leadProjectPath); + for (const member of data?.members ?? []) { + addPath(member.cwd); + } + + return Array.from(uniquePaths.values()); + }, [data?.members, leadProjectPath]); useBranchSync(branchSyncPaths, { live: true }); - const leadBranch = useStore((s) => - teamProjectPath ? (s.branchByPath[normalizePath(teamProjectPath)] ?? null) : null + const trackedBranches = useStore( + useShallow((s) => + Object.fromEntries( + branchSyncPaths.map((projectPath) => { + const normalizedPath = normalizePath(projectPath); + return [normalizedPath, s.branchByPath[normalizedPath] ?? null] as const; + }) + ) + ) ); + const leadBranch = leadProjectPath + ? (trackedBranches[normalizePath(leadProjectPath)] ?? null) + : null; + const membersWithLiveBranches = useMemo(() => { + if (!data) return []; + + return data.members.map((member) => { + const memberPath = member.cwd?.trim(); + const nextGitBranch = + memberPath && !isLeadMember(member) && leadBranch !== null + ? (() => { + const branch = trackedBranches[normalizePath(memberPath)] ?? null; + return branch && branch !== leadBranch ? branch : undefined; + })() + : undefined; + + if (member.gitBranch === nextGitBranch) { + return member; + } + + const nextMember: ResolvedTeamMember = { ...member }; + if (nextGitBranch) { + nextMember.gitBranch = nextGitBranch; + } else { + delete nextMember.gitBranch; + } + return nextMember; + }); + }, [data, leadBranch, trackedBranches]); // Filter sessions to team-only using sessionHistory + leadSessionId const teamSessionIds = useMemo(() => { @@ -852,7 +904,7 @@ export const TeamDetailView = ({ return result; }, [data, timeWindow, kanbanFilter.selectedOwners]); - const activeMembers = useStableActiveMembers(data?.members); + const activeMembers = useStableActiveMembers(membersWithLiveBranches); const kanbanDisplayTasks = useMemo(() => { const query = kanbanSearch.trim(); @@ -985,12 +1037,12 @@ export const TeamDetailView = ({ const pendingMemberProfile = useStore((s) => s.pendingMemberProfile); useEffect(() => { if (!pendingMemberProfile || !data) return; - const member = data.members.find((m) => m.name === pendingMemberProfile); + const member = membersWithLiveBranches.find((m) => m.name === pendingMemberProfile); if (member) { setSelectedMember(member); } useStore.getState().closeMemberProfile(); - }, [pendingMemberProfile, data]); + }, [pendingMemberProfile, membersWithLiveBranches]); const handleDeleteTask = useCallback( (taskId: string) => { @@ -1608,7 +1660,7 @@ export const TeamDetailView = ({ } > !isLeadMember(m))} + currentMembers={membersWithLiveBranches.filter((m) => !isLeadMember(m))} projectPath={data.config.projectPath} onClose={() => setEditDialogOpen(false)} onSaved={() => void selectTeam(teamName)} @@ -1984,8 +2036,8 @@ export const TeamDetailView = ({ m.name)} - existingMembers={data.members} + existingNames={membersWithLiveBranches.map((m) => m.name)} + existingMembers={membersWithLiveBranches} projectPath={data.config.projectPath} adding={addingMemberLoading} onClose={() => setAddMemberDialogOpen(false)} @@ -2068,7 +2120,7 @@ export const TeamDetailView = ({ mode="launch" open={launchDialogOpen} teamName={teamName} - members={data?.members ?? []} + members={membersWithLiveBranches} defaultProjectPath={data.config.projectPath} provisioningError={provisioningError} clearProvisioningError={clearProvisioningError}