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.
This commit is contained in:
parent
21513bb6f8
commit
1b3bd8752b
2 changed files with 71 additions and 87 deletions
|
|
@ -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<void> {
|
||||
// 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 <T>(p: Promise<T>, ms: number): Promise<T> => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
try {
|
||||
return await Promise.race([
|
||||
p,
|
||||
new Promise<T>((_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):
|
||||
|
|
|
|||
|
|
@ -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<string, string>();
|
||||
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 = ({
|
|||
}
|
||||
>
|
||||
<MemberList
|
||||
members={data.members}
|
||||
members={membersWithLiveBranches}
|
||||
memberTaskCounts={memberTaskCounts}
|
||||
taskMap={taskMap}
|
||||
pendingRepliesByMember={pendingRepliesByMember}
|
||||
|
|
@ -1975,7 +2027,7 @@ export const TeamDetailView = ({
|
|||
currentName={data.config.name}
|
||||
currentDescription={data.config.description ?? ''}
|
||||
currentColor={data.config.color ?? ''}
|
||||
currentMembers={data.members.filter((m) => !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 = ({
|
|||
<AddMemberDialog
|
||||
open={addMemberDialogOpen}
|
||||
teamName={teamName}
|
||||
existingNames={data.members.map((m) => 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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue