fix(team): make permission-pending launch copy honest
This commit is contained in:
parent
400eaf9acd
commit
3f8276147e
2 changed files with 118 additions and 9 deletions
|
|
@ -32,6 +32,24 @@ interface FailedSpawnDetail {
|
||||||
reason: string | null;
|
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([
|
const ACTIVE_PROVISIONING_STATES = new Set([
|
||||||
'validating',
|
'validating',
|
||||||
'spawning',
|
'spawning',
|
||||||
|
|
@ -201,6 +219,7 @@ export function buildTeamProvisioningPresentation({
|
||||||
failedSpawnCount,
|
failedSpawnCount,
|
||||||
expectedTeammateCount
|
expectedTeammateCount
|
||||||
);
|
);
|
||||||
|
const permissionBlockedCount = countPermissionBlockedMembers(memberSpawnStatuses);
|
||||||
|
|
||||||
const { allTeammatesConfirmedAlive, hasMembersStillJoining, remainingJoinCount } =
|
const { allTeammatesConfirmedAlive, hasMembersStillJoining, remainingJoinCount } =
|
||||||
getLaunchJoinState({
|
getLaunchJoinState({
|
||||||
|
|
@ -251,12 +270,19 @@ export function buildTeamProvisioningPresentation({
|
||||||
remainingJoinCount === 1
|
remainingJoinCount === 1
|
||||||
? '1 teammate still joining'
|
? '1 teammate still joining'
|
||||||
: `${remainingJoinCount} teammates still joining`;
|
: `${remainingJoinCount} teammates still joining`;
|
||||||
|
const pendingMembersAwaitApproval =
|
||||||
|
failedSpawnCount === 0 &&
|
||||||
|
permissionBlockedCount > 0 &&
|
||||||
|
permissionBlockedCount === remainingJoinCount;
|
||||||
|
const pendingDetailPhrase = pendingMembersAwaitApproval
|
||||||
|
? buildAwaitingPermissionPhrase(permissionBlockedCount)
|
||||||
|
: joiningPhrase;
|
||||||
const readyCompactDetail =
|
const readyCompactDetail =
|
||||||
failedSpawnCount > 0
|
failedSpawnCount > 0
|
||||||
? (failedSpawnCompactDetail ??
|
? (failedSpawnCompactDetail ??
|
||||||
`${failedSpawnCount} teammate${failedSpawnCount === 1 ? '' : 's'} failed to start`)
|
`${failedSpawnCount} teammate${failedSpawnCount === 1 ? '' : 's'} failed to start`)
|
||||||
: hasMembersStillJoining
|
: hasMembersStillJoining
|
||||||
? joiningPhrase
|
? pendingDetailPhrase
|
||||||
: expectedTeammateCount === 0
|
: expectedTeammateCount === 0
|
||||||
? 'Lead online'
|
? 'Lead online'
|
||||||
: `All ${expectedTeammateCount} teammates joined`;
|
: `All ${expectedTeammateCount} teammates joined`;
|
||||||
|
|
@ -268,7 +294,7 @@ export function buildTeamProvisioningPresentation({
|
||||||
: allTeammatesConfirmedAlive
|
: allTeammatesConfirmedAlive
|
||||||
? `Team provisioned - all ${expectedTeammateCount} teammates joined`
|
? `Team provisioned - all ${expectedTeammateCount} teammates joined`
|
||||||
: hasMembersStillJoining
|
: hasMembersStillJoining
|
||||||
? joiningPhrase
|
? pendingDetailPhrase
|
||||||
: 'Team provisioned - teammates are still joining';
|
: 'Team provisioned - teammates are still joining';
|
||||||
const readyDetailSeverity =
|
const readyDetailSeverity =
|
||||||
failedSpawnCount > 0 ? 'warning' : hasMembersStillJoining ? 'info' : undefined;
|
failedSpawnCount > 0 ? 'warning' : hasMembersStillJoining ? 'info' : undefined;
|
||||||
|
|
@ -316,6 +342,17 @@ export function buildTeamProvisioningPresentation({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isActive) {
|
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 {
|
return {
|
||||||
progress,
|
progress,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
|
@ -335,7 +372,11 @@ export function buildTeamProvisioningPresentation({
|
||||||
panelMessage:
|
panelMessage:
|
||||||
failedSpawnCount > 0
|
failedSpawnCount > 0
|
||||||
? (failedSpawnPanelMessage ?? genericFailedSpawnPanelMessage ?? progress.message)
|
? (failedSpawnPanelMessage ?? genericFailedSpawnPanelMessage ?? progress.message)
|
||||||
: progress.message,
|
: hasMembersStillJoining &&
|
||||||
|
permissionBlockedCount > 0 &&
|
||||||
|
permissionBlockedCount === remainingJoinCount
|
||||||
|
? activePendingDetailPhrase
|
||||||
|
: progress.message,
|
||||||
panelMessageSeverity: failedSpawnCount > 0 ? 'warning' : progress.messageSeverity,
|
panelMessageSeverity: failedSpawnCount > 0 ? 'warning' : progress.messageSeverity,
|
||||||
defaultLiveOutputOpen: false,
|
defaultLiveOutputOpen: false,
|
||||||
compactTitle: 'Launching team',
|
compactTitle: 'Launching team',
|
||||||
|
|
@ -343,9 +384,13 @@ export function buildTeamProvisioningPresentation({
|
||||||
failedSpawnCount > 0
|
failedSpawnCount > 0
|
||||||
? (failedSpawnCompactDetail ??
|
? (failedSpawnCompactDetail ??
|
||||||
`${failedSpawnCount} teammate${failedSpawnCount === 1 ? '' : 's'} failed to start`)
|
`${failedSpawnCount} teammate${failedSpawnCount === 1 ? '' : 's'} failed to start`)
|
||||||
: expectedTeammateCount > 0 && progressStepIndex >= 2
|
: hasMembersStillJoining && failedSpawnCount === 0 && permissionBlockedCount > 0
|
||||||
? `${heartbeatConfirmedCount}/${expectedTeammateCount} teammates confirmed`
|
? permissionBlockedCount === remainingJoinCount
|
||||||
: progress.message,
|
? buildAwaitingPermissionPhrase(permissionBlockedCount)
|
||||||
|
: `${heartbeatConfirmedCount}/${expectedTeammateCount} teammates confirmed`
|
||||||
|
: expectedTeammateCount > 0 && progressStepIndex >= 2
|
||||||
|
? `${heartbeatConfirmedCount}/${expectedTeammateCount} teammates confirmed`
|
||||||
|
: progress.message,
|
||||||
compactTone: failedSpawnCount > 0 ? 'warning' : 'default',
|
compactTone: failedSpawnCount > 0 ? 'warning' : 'default',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ describe('buildTeamProvisioningPresentation', () => {
|
||||||
expect(presentation?.panelMessage).toBe('1 teammate still joining');
|
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({
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
progress: {
|
progress: {
|
||||||
runId: 'run-4c',
|
runId: 'run-4c',
|
||||||
|
|
@ -329,8 +329,72 @@ describe('buildTeamProvisioningPresentation', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(presentation?.compactTitle).toBe('Finishing launch');
|
expect(presentation?.compactTitle).toBe('Finishing launch');
|
||||||
expect(presentation?.compactDetail).toBe('1 teammate still joining');
|
expect(presentation?.compactDetail).toBe('1 teammate awaiting permission approval');
|
||||||
expect(presentation?.panelMessage).toBe('1 teammate still joining');
|
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', () => {
|
it('keeps a generic failed teammate message while launch is still active if only persisted failure counts remain', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue