From ca03b14c515a6b6ce758dada4112996260bbab77 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 28 Feb 2026 22:38:45 +0200 Subject: [PATCH] feat: implement timeout handling and logging for team data fetching - Added a timeout mechanism to the getBranch calls in TeamDataService to prevent hangs on Windows setups, improving reliability during team data retrieval. - Introduced performance logging in TeamDetailView and teamSlice to track the start and completion of team selection processes, enhancing debugging capabilities. - Updated error handling to provide clearer warnings during team provisioning and selection, improving user experience. --- src/main/services/team/TeamDataService.ts | 20 +++++++++++-- .../components/team/TeamDetailView.tsx | 1 + src/renderer/store/slices/teamSlice.ts | 29 +++++-------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index f001933e..66f70a9a 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -514,9 +514,25 @@ export class TeamDataService { 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 { - leadBranch = await gitIdentityResolver.getBranch(leadCwd); + // 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(leadCwd), 2000); } catch { // Lead cwd may not be a git repo — skip enrichment entirely return; @@ -532,7 +548,7 @@ export class TeamDataService { batch.map(async (member) => { if (!member.cwd) return; try { - const branch = await gitIdentityResolver.getBranch(member.cwd); + const branch = await withTimeout(gitIdentityResolver.getBranch(member.cwd), 2000); if (branch && branch !== leadBranch) { // eslint-disable-next-line no-param-reassign -- intentional in-place enrichment member.gitBranch = branch; diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 734af2f6..fffe9339 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -243,6 +243,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele if (!teamName) { return; } + console.warn(`[TeamDetailView] selectTeam start team=${teamName}`); void selectTeam(teamName); void fetchDeletedTasks(teamName); }, [teamName, selectTeam, fetchDeletedTasks]); diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index f4491cdf..7e6ab22e 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -384,26 +384,10 @@ export const createTeamSlice: StateCreator = (set, reviewActionError: null, }); - // If this team is being provisioned right now, config.json doesn't exist yet. - // Stay in loading state — the provisioning progress callback will re-call - // selectTeam once config is written. - const isProvisioningNow = Object.values(get().provisioningRuns).some( - (run) => - run.teamName === teamName && - !['ready', 'disconnected', 'failed', 'cancelled'].includes(run.state) - ); - if (isProvisioningNow) { - set({ - selectedTeamLoading: true, - selectedTeamData: null, - selectedTeamError: null, - }); - return; - } - const startedAt = Date.now(); const traceId = `${teamName}:${startedAt}`; - logger.info( + // NOTE: logger.info is not shown by default (level=WARN in dev). Use warn/console. + console.warn( `[selectTeam] start trace=${traceId} skipProjectAutoSelect=${opts?.skipProjectAutoSelect === true}` ); @@ -424,7 +408,7 @@ export const createTeamSlice: StateCreator = (set, selectedTeamError: null, }); - logger.info( + console.warn( `[selectTeam] done trace=${traceId} ms=${Date.now() - startedAt} tasks=${data.tasks.length} members=${data.members.length} messages=${data.messages.length}` ); @@ -479,12 +463,15 @@ export const createTeamSlice: StateCreator = (set, !['ready', 'disconnected', 'failed', 'cancelled'].includes(run.state) ); - if (isProvisioning) { + const msg = error instanceof Error ? error.message : String(error); + // IPC can report provisioning state explicitly. + if (msg === 'TEAM_PROVISIONING' || (msg.includes('TEAM_PROVISIONING') && isProvisioning)) { set({ selectedTeamLoading: true, selectedTeamData: null, selectedTeamError: null, }); + console.warn(`[selectTeam] provisioning team=${teamName} trace=${traceId}`); return; } @@ -494,7 +481,7 @@ export const createTeamSlice: StateCreator = (set, : error instanceof Error ? error.message : 'Failed to fetch team data'; - logger.error(`[selectTeam] fail team=${teamName} ms=${Date.now() - startedAt} ${message}`); + console.warn(`[selectTeam] fail team=${teamName} ms=${Date.now() - startedAt} ${message}`); set({ selectedTeamLoading: false, selectedTeamData: null,