diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 5c5d99db..baa0b592 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -41,6 +41,7 @@ import { createChipFromSelection } from '@renderer/utils/chipUtils'; import { sumContextInjectionTokens } from '@renderer/utils/contextMath'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { formatProjectPath } from '@renderer/utils/pathDisplay'; +import { buildPendingRuntimeSummaryCopy } from '@renderer/utils/teamLaunchSummaryCopy'; import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize'; import { nameColorSet } from '@renderer/utils/projectColor'; import { resolveProjectIdByPath } from '@renderer/utils/projectLookup'; @@ -263,7 +264,12 @@ const TeamOfflineStatusBanner = memo(function TeamOfflineStatusBanner({ const message = summary?.teamLaunchState === 'partial_pending' ? summary.runtimeAlivePendingCount != null && summary.runtimeAlivePendingCount > 0 - ? `Last launch is still reconciling - ${summary.confirmedCount ?? 0}/${summary.expectedMemberCount ?? summary.memberCount} teammates confirmed alive, ${summary.runtimeAlivePendingCount} runtime${summary.runtimeAlivePendingCount === 1 ? '' : 's'} pending bootstrap` + ? buildPendingRuntimeSummaryCopy({ + confirmedCount: summary.confirmedCount, + expectedMemberCount: summary.expectedMemberCount, + memberCount: summary.memberCount, + runtimeAlivePendingCount: summary.runtimeAlivePendingCount, + }) : 'Last launch is still reconciling' : summary?.partialLaunchFailure ? summary.missingMemberCount > 0 diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx index 0b909a0c..5269fbbe 100644 --- a/src/renderer/components/team/TeamListView.tsx +++ b/src/renderer/components/team/TeamListView.tsx @@ -28,6 +28,7 @@ import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildTaskCountsByTeam, normalizePath } from '@renderer/utils/pathNormalize'; import { getBaseName } from '@renderer/utils/pathUtils'; import { nameColorSet } from '@renderer/utils/projectColor'; +import { buildPendingRuntimeSummaryCopy } from '@renderer/utils/teamLaunchSummaryCopy'; import { isLeadMember } from '@shared/utils/leadDetection'; import { CheckCircle, @@ -981,7 +982,13 @@ export const TeamListView = (): React.JSX.Element => { {team.teamLaunchState === 'partial_pending' ? (

{team.runtimeAlivePendingCount && team.runtimeAlivePendingCount > 0 - ? `Last launch is still reconciling — ${team.confirmedCount ?? 0}/${team.expectedMemberCount ?? team.memberCount} teammates confirmed alive, ${team.runtimeAlivePendingCount} runtime${team.runtimeAlivePendingCount === 1 ? '' : 's'} pending bootstrap.` + ? buildPendingRuntimeSummaryCopy({ + confirmedCount: team.confirmedCount, + expectedMemberCount: team.expectedMemberCount, + memberCount: team.memberCount, + runtimeAlivePendingCount: team.runtimeAlivePendingCount, + includePeriod: true, + }) : 'Last launch is still reconciling.'}

) : team.partialLaunchFailure || team.teamLaunchState === 'partial_failure' ? ( diff --git a/src/renderer/utils/teamLaunchSummaryCopy.ts b/src/renderer/utils/teamLaunchSummaryCopy.ts new file mode 100644 index 00000000..895e1ecc --- /dev/null +++ b/src/renderer/utils/teamLaunchSummaryCopy.ts @@ -0,0 +1,17 @@ +export function buildPendingRuntimeSummaryCopy(input: { + confirmedCount?: number | null; + expectedMemberCount?: number | null; + memberCount?: number | null; + runtimeAlivePendingCount?: number | null; + includePeriod?: boolean; +}): string { + const pendingCount = input.runtimeAlivePendingCount ?? 0; + if (pendingCount <= 0) { + return input.includePeriod + ? 'Last launch is still reconciling.' + : 'Last launch is still reconciling'; + } + const expectedCount = input.expectedMemberCount ?? input.memberCount ?? 0; + const message = `Last launch is still reconciling - ${input.confirmedCount ?? 0}/${expectedCount} teammates confirmed alive, ${pendingCount} runtime${pendingCount === 1 ? '' : 's'} still awaiting confirmation`; + return input.includePeriod ? `${message}.` : message; +} diff --git a/test/renderer/utils/teamLaunchSummaryCopy.test.ts b/test/renderer/utils/teamLaunchSummaryCopy.test.ts new file mode 100644 index 00000000..d712425e --- /dev/null +++ b/test/renderer/utils/teamLaunchSummaryCopy.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { buildPendingRuntimeSummaryCopy } from '@renderer/utils/teamLaunchSummaryCopy'; + +describe('buildPendingRuntimeSummaryCopy', () => { + it('uses generic runtime confirmation wording instead of bootstrap-specific copy', () => { + expect( + buildPendingRuntimeSummaryCopy({ + confirmedCount: 2, + expectedMemberCount: 4, + runtimeAlivePendingCount: 2, + }) + ).toBe( + 'Last launch is still reconciling - 2/4 teammates confirmed alive, 2 runtimes still awaiting confirmation' + ); + }); + + it('can emit the punctuated list-card variant', () => { + expect( + buildPendingRuntimeSummaryCopy({ + confirmedCount: 1, + expectedMemberCount: 3, + runtimeAlivePendingCount: 1, + includePeriod: true, + }) + ).toBe( + 'Last launch is still reconciling - 1/3 teammates confirmed alive, 1 runtime still awaiting confirmation.' + ); + }); +});