From 3f8276147e5a6f65a82217ef533b4e300e0969a8 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 02:04:55 +0300 Subject: [PATCH] fix(team): make permission-pending launch copy honest --- .../utils/teamProvisioningPresentation.ts | 57 +++++++++++++-- .../teamProvisioningPresentation.test.ts | 70 ++++++++++++++++++- 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/renderer/utils/teamProvisioningPresentation.ts b/src/renderer/utils/teamProvisioningPresentation.ts index dd6e90bf..48edf876 100644 --- a/src/renderer/utils/teamProvisioningPresentation.ts +++ b/src/renderer/utils/teamProvisioningPresentation.ts @@ -32,6 +32,24 @@ interface FailedSpawnDetail { reason: string | null; } +function countPermissionBlockedMembers(memberSpawnStatuses: MemberSpawnStatusCollection): number { + if (!memberSpawnStatuses) { + return 0; + } + const entries = + memberSpawnStatuses instanceof Map + ? [...memberSpawnStatuses.values()] + : Object.values(memberSpawnStatuses); + + return entries.filter((entry) => entry.launchState === 'runtime_pending_permission').length; +} + +function buildAwaitingPermissionPhrase(count: number): string { + return count === 1 + ? '1 teammate awaiting permission approval' + : `${count} teammates awaiting permission approval`; +} + const ACTIVE_PROVISIONING_STATES = new Set([ 'validating', 'spawning', @@ -201,6 +219,7 @@ export function buildTeamProvisioningPresentation({ failedSpawnCount, expectedTeammateCount ); + const permissionBlockedCount = countPermissionBlockedMembers(memberSpawnStatuses); const { allTeammatesConfirmedAlive, hasMembersStillJoining, remainingJoinCount } = getLaunchJoinState({ @@ -251,12 +270,19 @@ export function buildTeamProvisioningPresentation({ remainingJoinCount === 1 ? '1 teammate still joining' : `${remainingJoinCount} teammates still joining`; + const pendingMembersAwaitApproval = + failedSpawnCount === 0 && + permissionBlockedCount > 0 && + permissionBlockedCount === remainingJoinCount; + const pendingDetailPhrase = pendingMembersAwaitApproval + ? buildAwaitingPermissionPhrase(permissionBlockedCount) + : joiningPhrase; const readyCompactDetail = failedSpawnCount > 0 ? (failedSpawnCompactDetail ?? `${failedSpawnCount} teammate${failedSpawnCount === 1 ? '' : 's'} failed to start`) : hasMembersStillJoining - ? joiningPhrase + ? pendingDetailPhrase : expectedTeammateCount === 0 ? 'Lead online' : `All ${expectedTeammateCount} teammates joined`; @@ -268,7 +294,7 @@ export function buildTeamProvisioningPresentation({ : allTeammatesConfirmedAlive ? `Team provisioned - all ${expectedTeammateCount} teammates joined` : hasMembersStillJoining - ? joiningPhrase + ? pendingDetailPhrase : 'Team provisioned - teammates are still joining'; const readyDetailSeverity = failedSpawnCount > 0 ? 'warning' : hasMembersStillJoining ? 'info' : undefined; @@ -316,6 +342,17 @@ export function buildTeamProvisioningPresentation({ } if (isActive) { + const activeJoiningPhrase = + remainingJoinCount === 1 + ? '1 teammate still joining' + : `${remainingJoinCount} teammates still joining`; + const activePendingDetailPhrase = + failedSpawnCount === 0 && + hasMembersStillJoining && + permissionBlockedCount > 0 && + permissionBlockedCount === remainingJoinCount + ? buildAwaitingPermissionPhrase(permissionBlockedCount) + : activeJoiningPhrase; return { progress, isActive: true, @@ -335,7 +372,11 @@ export function buildTeamProvisioningPresentation({ panelMessage: failedSpawnCount > 0 ? (failedSpawnPanelMessage ?? genericFailedSpawnPanelMessage ?? progress.message) - : progress.message, + : hasMembersStillJoining && + permissionBlockedCount > 0 && + permissionBlockedCount === remainingJoinCount + ? activePendingDetailPhrase + : progress.message, panelMessageSeverity: failedSpawnCount > 0 ? 'warning' : progress.messageSeverity, defaultLiveOutputOpen: false, compactTitle: 'Launching team', @@ -343,9 +384,13 @@ export function buildTeamProvisioningPresentation({ failedSpawnCount > 0 ? (failedSpawnCompactDetail ?? `${failedSpawnCount} teammate${failedSpawnCount === 1 ? '' : 's'} failed to start`) - : expectedTeammateCount > 0 && progressStepIndex >= 2 - ? `${heartbeatConfirmedCount}/${expectedTeammateCount} teammates confirmed` - : progress.message, + : hasMembersStillJoining && failedSpawnCount === 0 && permissionBlockedCount > 0 + ? permissionBlockedCount === remainingJoinCount + ? buildAwaitingPermissionPhrase(permissionBlockedCount) + : `${heartbeatConfirmedCount}/${expectedTeammateCount} teammates confirmed` + : expectedTeammateCount > 0 && progressStepIndex >= 2 + ? `${heartbeatConfirmedCount}/${expectedTeammateCount} teammates confirmed` + : progress.message, compactTone: failedSpawnCount > 0 ? 'warning' : 'default', }; } diff --git a/test/renderer/utils/teamProvisioningPresentation.test.ts b/test/renderer/utils/teamProvisioningPresentation.test.ts index 3da5dbaf..e2f1c33a 100644 --- a/test/renderer/utils/teamProvisioningPresentation.test.ts +++ b/test/renderer/utils/teamProvisioningPresentation.test.ts @@ -269,7 +269,7 @@ describe('buildTeamProvisioningPresentation', () => { expect(presentation?.panelMessage).toBe('1 teammate still joining'); }); - it('counts permission-blocked teammates as still joining while launch is finishing', () => { + it('surfaces permission-blocked teammates as awaiting approval while launch is finishing', () => { const presentation = buildTeamProvisioningPresentation({ progress: { runId: 'run-4c', @@ -329,8 +329,72 @@ describe('buildTeamProvisioningPresentation', () => { }); expect(presentation?.compactTitle).toBe('Finishing launch'); - expect(presentation?.compactDetail).toBe('1 teammate still joining'); - expect(presentation?.panelMessage).toBe('1 teammate still joining'); + expect(presentation?.compactDetail).toBe('1 teammate awaiting permission approval'); + expect(presentation?.panelMessage).toBe('1 teammate awaiting permission approval'); + }); + + it('surfaces permission-blocked teammates as awaiting approval while launch is still active', () => { + const presentation = buildTeamProvisioningPresentation({ + progress: { + runId: 'run-4d', + teamName: 'opencode-team', + state: 'finalizing', + startedAt: '2026-04-13T10:00:00.000Z', + updatedAt: '2026-04-13T10:00:08.000Z', + message: 'Waiting for runtime confirmation', + messageSeverity: undefined, + pid: 4321, + cliLogsTail: '', + assistantOutput: '', + }, + members: [ + { + name: 'team-lead', + agentType: 'team-lead', + status: 'active', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + { + name: 'bob', + agentType: 'engineer', + status: 'unknown', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + ], + memberSpawnStatuses: { + bob: { + status: 'online', + launchState: 'runtime_pending_permission', + updatedAt: '2026-04-13T10:00:07.000Z', + runtimeAlive: true, + livenessSource: 'process', + bootstrapConfirmed: false, + hardFailure: false, + agentToolAccepted: true, + pendingPermissionRequestIds: ['perm_1'], + firstSpawnAcceptedAt: '2026-04-13T10:00:01.000Z', + }, + }, + memberSpawnSnapshot: { + expectedMembers: ['bob'], + summary: { + confirmedCount: 0, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 1, + }, + }, + }); + + expect(presentation?.compactTitle).toBe('Launching team'); + expect(presentation?.compactDetail).toBe('1 teammate awaiting permission approval'); + expect(presentation?.panelMessage).toBe('1 teammate awaiting permission approval'); }); it('keeps a generic failed teammate message while launch is still active if only persisted failure counts remain', () => {