feat: integrate provisioning snapshots into TeamListView for enhanced team display

- Added provisioningSnapshotByTeam to manage synthetic team summaries during active provisioning.
- Implemented teamsWithProvisioning to merge real teams with synthetic cards, improving the team list display.
- Updated filtering and rendering logic in TeamListView to utilize the new teamsWithProvisioning array, ensuring accurate representation of teams during provisioning states.
This commit is contained in:
iliya 2026-03-15 11:03:26 +02:00
parent 7502cc8e53
commit 65a9928cb5
2 changed files with 60 additions and 6 deletions

View file

@ -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<TeamSummary[]>(() => {
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 => {
</p>
) : null}
{teams.length > 0 ? (
{teamsWithProvisioning.length > 0 ? (
<div className="mt-3 flex items-center gap-2">
<div className="relative flex-1">
<Search
@ -594,7 +605,7 @@ export const TeamListView = (): React.JSX.Element => {
</div>
<TeamListFilterPopover
filter={filter}
teams={teams}
teams={teamsWithProvisioning}
aliveTeams={aliveTeams}
onFilterChange={setFilter}
/>
@ -633,7 +644,7 @@ export const TeamListView = (): React.JSX.Element => {
);
}
if (teams.length === 0) {
if (teamsWithProvisioning.length === 0) {
return <TeamEmptyState />;
}

View file

@ -353,6 +353,8 @@ export interface TeamSlice {
lastSendMessageResult: SendMessageResult | null;
reviewActionError: string | null;
provisioningRuns: Record<string, TeamProvisioningProgress>;
/** Synthetic TeamSummary snapshots for teams currently being provisioned (before config.json exists). */
provisioningSnapshotByTeam: Record<string, TeamSummary>;
currentProvisioningRunIdByTeam: Record<string, string | null>;
currentRuntimeRunIdByTeam: Record<string, string | null>;
/** Runs explicitly cleared after Unknown runId polling; late events/progress for them are ignored. */
@ -610,6 +612,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
crossTeamTargetsLoading: false,
reviewActionError: null,
provisioningRuns: {},
provisioningSnapshotByTeam: {},
currentProvisioningRunIdByTeam: {},
currentRuntimeRunIdByTeam: {},
ignoredProvisioningRunIds: {},
@ -743,7 +746,22 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (set,
[progress.teamName]: progress.runId,
},
provisioningErrorByTeam: nextErrors,
provisioningSnapshotByTeam: nextSnapshots,
};
});