diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx index 673d3345..da404d6f 100644 --- a/src/renderer/components/team/TeamListView.tsx +++ b/src/renderer/components/team/TeamListView.tsx @@ -225,6 +225,7 @@ export const TeamListView = (): React.JSX.Element => { provisioningErrorByTeam, clearProvisioningError, provisioningRuns, + provisioningSnapshotByTeam, currentProvisioningRunIdByTeam, leadActivityByTeam, } = useStore( @@ -234,6 +235,7 @@ export const TeamListView = (): React.JSX.Element => { provisioningErrorByTeam: s.provisioningErrorByTeam, clearProvisioningError: s.clearProvisioningError, provisioningRuns: s.provisioningRuns, + provisioningSnapshotByTeam: s.provisioningSnapshotByTeam, currentProvisioningRunIdByTeam: s.currentProvisioningRunIdByTeam, leadActivityByTeam: s.leadActivityByTeam, })) @@ -251,6 +253,15 @@ export const TeamListView = (): React.JSX.Element => { ); }, [currentProvisioningRunIdByTeam, provisioningState]); + /** Merge real teams with synthetic launching cards for active provisioning. */ + const teamsWithProvisioning = useMemo(() => { + const existingNames = new Set(teams.map((t) => t.teamName)); + const synthetic = provisioningTeamNames + .filter((name) => !existingNames.has(name) && provisioningSnapshotByTeam[name]) + .map((name) => provisioningSnapshotByTeam[name]); + return synthetic.length > 0 ? [...teams, ...synthetic] : teams; + }, [teams, provisioningTeamNames, provisioningSnapshotByTeam]); + // Fetch alive teams on mount and when teams list changes useEffect(() => { if (!electronMode) return; @@ -303,7 +314,7 @@ export const TeamListView = (): React.JSX.Element => { ]); const filteredTeams = useMemo(() => { - let result = teams; + let result = teamsWithProvisioning; const q = searchQuery.trim().toLowerCase(); if (q) { @@ -364,7 +375,7 @@ export const TeamListView = (): React.JSX.Element => { return result; }, [ - teams, + teamsWithProvisioning, searchQuery, currentProjectPath, aliveTeams, @@ -577,7 +588,7 @@ export const TeamListView = (): React.JSX.Element => {

) : null} - {teams.length > 0 ? ( + {teamsWithProvisioning.length > 0 ? (
{
@@ -633,7 +644,7 @@ export const TeamListView = (): React.JSX.Element => { ); } - if (teams.length === 0) { + if (teamsWithProvisioning.length === 0) { return ; } diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 39e6f410..dad82e01 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -353,6 +353,8 @@ export interface TeamSlice { lastSendMessageResult: SendMessageResult | null; reviewActionError: string | null; provisioningRuns: Record; + /** Synthetic TeamSummary snapshots for teams currently being provisioned (before config.json exists). */ + provisioningSnapshotByTeam: Record; currentProvisioningRunIdByTeam: Record; currentRuntimeRunIdByTeam: Record; /** Runs explicitly cleared after Unknown runId polling; late events/progress for them are ignored. */ @@ -610,6 +612,7 @@ export const createTeamSlice: StateCreator = (set, crossTeamTargetsLoading: false, reviewActionError: null, provisioningRuns: {}, + provisioningSnapshotByTeam: {}, currentProvisioningRunIdByTeam: {}, currentRuntimeRunIdByTeam: {}, ignoredProvisioningRunIds: {}, @@ -743,7 +746,22 @@ export const createTeamSlice: StateCreator = (set, } } } - set({ teams, teamByName, teamBySessionId, teamsLoading: false, teamsError: null }); + // Atomic update: set teams AND clean up provisioning snapshots in one call + // to prevent any render cycle with duplicate cards. + set((state) => { + const nextSnapshots = { ...state.provisioningSnapshotByTeam }; + for (const team of teams) { + delete nextSnapshots[team.teamName]; + } + return { + teams, + teamByName, + teamBySessionId, + teamsLoading: false, + teamsError: null, + provisioningSnapshotByTeam: nextSnapshots, + }; + }); } catch (error) { // On refresh failure, keep existing teams visible set({ @@ -1413,6 +1431,21 @@ export const createTeamSlice: StateCreator = (set, ...state.currentProvisioningRunIdByTeam, [request.teamName]: pendingRunId, }, + // Synthetic card for the team list — visible until fetchTeams() picks up the real team. + provisioningSnapshotByTeam: { + ...state.provisioningSnapshotByTeam, + [request.teamName]: { + teamName: request.teamName, + displayName: request.displayName || request.teamName, + description: request.description || '', + color: request.color, + memberCount: request.members.length, + members: request.members.map((m) => ({ name: m.name, role: m.role })), + taskCount: 0, + lastActivity: null, + projectPath: request.cwd || undefined, + }, + }, })); try { if (typeof api.teams.createTeam !== 'function') { @@ -1751,6 +1784,15 @@ export const createTeamSlice: StateCreator = (set, } else { delete nextErrors[progress.teamName]; } + // Clean up provisioning snapshot on terminal failure states + const nextSnapshots = + progress.state === 'failed' || progress.state === 'cancelled' + ? (() => { + const s = { ...state.provisioningSnapshotByTeam }; + delete s[progress.teamName]; + return s; + })() + : state.provisioningSnapshotByTeam; return { provisioningRuns: nextRuns, currentProvisioningRunIdByTeam: nextCurrentRunIdByTeam, @@ -1759,6 +1801,7 @@ export const createTeamSlice: StateCreator = (set, [progress.teamName]: progress.runId, }, provisioningErrorByTeam: nextErrors, + provisioningSnapshotByTeam: nextSnapshots, }; });