fix(team): align permission-blocked launch state
This commit is contained in:
parent
2b96adda33
commit
8cd3f04c20
12 changed files with 203 additions and 13 deletions
|
|
@ -732,6 +732,8 @@ function getLaunchStatusColor(visualState: GraphNode['launchVisualState']): stri
|
||||||
return hexWithAlpha('#d4d4d8', 0.8);
|
return hexWithAlpha('#d4d4d8', 0.8);
|
||||||
case 'spawning':
|
case 'spawning':
|
||||||
return hexWithAlpha('#f59e0b', 0.9);
|
return hexWithAlpha('#f59e0b', 0.9);
|
||||||
|
case 'permission_pending':
|
||||||
|
return hexWithAlpha('#f59e0b', 0.92);
|
||||||
case 'runtime_pending':
|
case 'runtime_pending':
|
||||||
return hexWithAlpha('#67e8f9', 0.9);
|
return hexWithAlpha('#67e8f9', 0.9);
|
||||||
case 'settling':
|
case 'settling':
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export type GraphNodeState =
|
||||||
export type GraphLaunchVisualState =
|
export type GraphLaunchVisualState =
|
||||||
| 'waiting'
|
| 'waiting'
|
||||||
| 'spawning'
|
| 'spawning'
|
||||||
|
| 'permission_pending'
|
||||||
| 'runtime_pending'
|
| 'runtime_pending'
|
||||||
| 'settling'
|
| 'settling'
|
||||||
| 'error';
|
| 'error';
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,23 @@ type RuntimeMemberSpawnState = Pick<
|
||||||
| 'runtimeAlive'
|
| 'runtimeAlive'
|
||||||
| 'bootstrapConfirmed'
|
| 'bootstrapConfirmed'
|
||||||
| 'hardFailure'
|
| 'hardFailure'
|
||||||
|
| 'pendingPermissionRequestIds'
|
||||||
| 'firstSpawnAcceptedAt'
|
| 'firstSpawnAcceptedAt'
|
||||||
| 'lastHeartbeatAt'
|
| 'lastHeartbeatAt'
|
||||||
| 'updatedAt'
|
| 'updatedAt'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
function normalizePendingPermissionRequestIds(value: unknown): string[] | undefined {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const normalized = value
|
||||||
|
.filter((item): item is string => typeof item === 'string')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMemberName(name: string): string {
|
function normalizeMemberName(name: string): string {
|
||||||
return name.trim();
|
return name.trim();
|
||||||
}
|
}
|
||||||
|
|
@ -48,15 +60,23 @@ function normalizeMemberName(name: string): string {
|
||||||
function buildDiagnostics(
|
function buildDiagnostics(
|
||||||
member: Pick<
|
member: Pick<
|
||||||
PersistedTeamLaunchMemberState,
|
PersistedTeamLaunchMemberState,
|
||||||
'agentToolAccepted' | 'runtimeAlive' | 'bootstrapConfirmed' | 'hardFailureReason' | 'sources'
|
| 'agentToolAccepted'
|
||||||
|
| 'runtimeAlive'
|
||||||
|
| 'bootstrapConfirmed'
|
||||||
|
| 'hardFailureReason'
|
||||||
|
| 'sources'
|
||||||
|
| 'pendingPermissionRequestIds'
|
||||||
>
|
>
|
||||||
): string[] {
|
): string[] {
|
||||||
const diagnostics: string[] = [];
|
const diagnostics: string[] = [];
|
||||||
if (member.agentToolAccepted) diagnostics.push('spawn accepted');
|
if (member.agentToolAccepted) diagnostics.push('spawn accepted');
|
||||||
if (member.runtimeAlive) diagnostics.push('runtime alive');
|
if (member.runtimeAlive) diagnostics.push('runtime alive');
|
||||||
if (member.bootstrapConfirmed) diagnostics.push('late heartbeat received');
|
if (member.bootstrapConfirmed) diagnostics.push('late heartbeat received');
|
||||||
if (member.runtimeAlive && !member.bootstrapConfirmed)
|
if ((member.pendingPermissionRequestIds?.length ?? 0) > 0) {
|
||||||
|
diagnostics.push('waiting for permission approval');
|
||||||
|
} else if (member.runtimeAlive && !member.bootstrapConfirmed) {
|
||||||
diagnostics.push('waiting for teammate check-in');
|
diagnostics.push('waiting for teammate check-in');
|
||||||
|
}
|
||||||
if (member.hardFailureReason)
|
if (member.hardFailureReason)
|
||||||
diagnostics.push(`hard failure reason: ${member.hardFailureReason}`);
|
diagnostics.push(`hard failure reason: ${member.hardFailureReason}`);
|
||||||
if (member.sources?.duplicateRespawnBlocked) diagnostics.push('respawn blocked as duplicate');
|
if (member.sources?.duplicateRespawnBlocked) diagnostics.push('respawn blocked as duplicate');
|
||||||
|
|
@ -133,7 +153,11 @@ export function hasMixedPersistedLaunchMetadata(
|
||||||
function deriveMemberLaunchState(
|
function deriveMemberLaunchState(
|
||||||
member: Pick<
|
member: Pick<
|
||||||
PersistedTeamLaunchMemberState,
|
PersistedTeamLaunchMemberState,
|
||||||
'hardFailure' | 'bootstrapConfirmed' | 'runtimeAlive' | 'agentToolAccepted'
|
| 'hardFailure'
|
||||||
|
| 'bootstrapConfirmed'
|
||||||
|
| 'runtimeAlive'
|
||||||
|
| 'agentToolAccepted'
|
||||||
|
| 'pendingPermissionRequestIds'
|
||||||
>
|
>
|
||||||
): MemberLaunchState {
|
): MemberLaunchState {
|
||||||
if (member.hardFailure) {
|
if (member.hardFailure) {
|
||||||
|
|
@ -142,6 +166,9 @@ function deriveMemberLaunchState(
|
||||||
if (member.bootstrapConfirmed) {
|
if (member.bootstrapConfirmed) {
|
||||||
return 'confirmed_alive';
|
return 'confirmed_alive';
|
||||||
}
|
}
|
||||||
|
if ((member.pendingPermissionRequestIds?.length ?? 0) > 0) {
|
||||||
|
return 'runtime_pending_permission';
|
||||||
|
}
|
||||||
if (member.runtimeAlive || member.agentToolAccepted) {
|
if (member.runtimeAlive || member.agentToolAccepted) {
|
||||||
return 'runtime_pending_bootstrap';
|
return 'runtime_pending_bootstrap';
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +324,9 @@ function normalizePersistedMemberState(
|
||||||
typeof parsed.hardFailureReason === 'string' && parsed.hardFailureReason.trim().length > 0
|
typeof parsed.hardFailureReason === 'string' && parsed.hardFailureReason.trim().length > 0
|
||||||
? parsed.hardFailureReason.trim()
|
? parsed.hardFailureReason.trim()
|
||||||
: undefined,
|
: undefined,
|
||||||
|
pendingPermissionRequestIds: normalizePendingPermissionRequestIds(
|
||||||
|
parsed.pendingPermissionRequestIds
|
||||||
|
),
|
||||||
firstSpawnAcceptedAt:
|
firstSpawnAcceptedAt:
|
||||||
typeof parsed.firstSpawnAcceptedAt === 'string' ? parsed.firstSpawnAcceptedAt : undefined,
|
typeof parsed.firstSpawnAcceptedAt === 'string' ? parsed.firstSpawnAcceptedAt : undefined,
|
||||||
lastHeartbeatAt:
|
lastHeartbeatAt:
|
||||||
|
|
@ -315,6 +345,7 @@ function normalizePersistedMemberState(
|
||||||
const launchState =
|
const launchState =
|
||||||
parsed.launchState === 'starting' ||
|
parsed.launchState === 'starting' ||
|
||||||
parsed.launchState === 'runtime_pending_bootstrap' ||
|
parsed.launchState === 'runtime_pending_bootstrap' ||
|
||||||
|
parsed.launchState === 'runtime_pending_permission' ||
|
||||||
parsed.launchState === 'confirmed_alive' ||
|
parsed.launchState === 'confirmed_alive' ||
|
||||||
parsed.launchState === 'failed_to_start'
|
parsed.launchState === 'failed_to_start'
|
||||||
? parsed.launchState
|
? parsed.launchState
|
||||||
|
|
@ -423,6 +454,9 @@ export function snapshotFromRuntimeMemberStatuses(params: {
|
||||||
bootstrapConfirmed: runtime?.bootstrapConfirmed === true,
|
bootstrapConfirmed: runtime?.bootstrapConfirmed === true,
|
||||||
hardFailure: runtime?.hardFailure === true || runtime?.launchState === 'failed_to_start',
|
hardFailure: runtime?.hardFailure === true || runtime?.launchState === 'failed_to_start',
|
||||||
hardFailureReason: runtime?.hardFailureReason ?? runtime?.error,
|
hardFailureReason: runtime?.hardFailureReason ?? runtime?.error,
|
||||||
|
pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds?.length
|
||||||
|
? [...new Set(runtime.pendingPermissionRequestIds)]
|
||||||
|
: undefined,
|
||||||
firstSpawnAcceptedAt: runtime?.firstSpawnAcceptedAt,
|
firstSpawnAcceptedAt: runtime?.firstSpawnAcceptedAt,
|
||||||
lastHeartbeatAt: runtime?.lastHeartbeatAt,
|
lastHeartbeatAt: runtime?.lastHeartbeatAt,
|
||||||
lastRuntimeAliveAt: runtime?.runtimeAlive ? updatedAt : undefined,
|
lastRuntimeAliveAt: runtime?.runtimeAlive ? updatedAt : undefined,
|
||||||
|
|
@ -460,6 +494,9 @@ export function snapshotToMemberSpawnStatuses(
|
||||||
} else if (entry.launchState === 'confirmed_alive') {
|
} else if (entry.launchState === 'confirmed_alive') {
|
||||||
status = 'online';
|
status = 'online';
|
||||||
livenessSource = 'heartbeat';
|
livenessSource = 'heartbeat';
|
||||||
|
} else if (entry.launchState === 'runtime_pending_permission') {
|
||||||
|
status = entry.runtimeAlive ? 'online' : 'waiting';
|
||||||
|
livenessSource = entry.runtimeAlive ? 'process' : undefined;
|
||||||
} else if (entry.launchState === 'runtime_pending_bootstrap') {
|
} else if (entry.launchState === 'runtime_pending_bootstrap') {
|
||||||
status = entry.runtimeAlive ? 'online' : 'waiting';
|
status = entry.runtimeAlive ? 'online' : 'waiting';
|
||||||
livenessSource = entry.runtimeAlive ? 'process' : undefined;
|
livenessSource = entry.runtimeAlive ? 'process' : undefined;
|
||||||
|
|
@ -476,6 +513,7 @@ export function snapshotToMemberSpawnStatuses(
|
||||||
runtimeAlive: entry.runtimeAlive,
|
runtimeAlive: entry.runtimeAlive,
|
||||||
bootstrapConfirmed: entry.bootstrapConfirmed,
|
bootstrapConfirmed: entry.bootstrapConfirmed,
|
||||||
hardFailure: entry.hardFailure,
|
hardFailure: entry.hardFailure,
|
||||||
|
pendingPermissionRequestIds: entry.pendingPermissionRequestIds,
|
||||||
firstSpawnAcceptedAt: entry.firstSpawnAcceptedAt,
|
firstSpawnAcceptedAt: entry.firstSpawnAcceptedAt,
|
||||||
lastHeartbeatAt: entry.lastHeartbeatAt,
|
lastHeartbeatAt: entry.lastHeartbeatAt,
|
||||||
updatedAt: entry.lastEvaluatedAt,
|
updatedAt: entry.lastEvaluatedAt,
|
||||||
|
|
|
||||||
|
|
@ -1535,6 +1535,7 @@ function deriveMemberLaunchState(entry: {
|
||||||
runtimeAlive?: boolean;
|
runtimeAlive?: boolean;
|
||||||
bootstrapConfirmed?: boolean;
|
bootstrapConfirmed?: boolean;
|
||||||
hardFailure?: boolean;
|
hardFailure?: boolean;
|
||||||
|
pendingPermissionRequestIds?: string[];
|
||||||
}): MemberLaunchState {
|
}): MemberLaunchState {
|
||||||
if (entry.hardFailure) {
|
if (entry.hardFailure) {
|
||||||
return 'failed_to_start';
|
return 'failed_to_start';
|
||||||
|
|
@ -1542,6 +1543,9 @@ function deriveMemberLaunchState(entry: {
|
||||||
if (entry.bootstrapConfirmed) {
|
if (entry.bootstrapConfirmed) {
|
||||||
return 'confirmed_alive';
|
return 'confirmed_alive';
|
||||||
}
|
}
|
||||||
|
if ((entry.pendingPermissionRequestIds?.length ?? 0) > 0) {
|
||||||
|
return 'runtime_pending_permission';
|
||||||
|
}
|
||||||
if (entry.runtimeAlive || entry.agentToolAccepted) {
|
if (entry.runtimeAlive || entry.agentToolAccepted) {
|
||||||
return 'runtime_pending_bootstrap';
|
return 'runtime_pending_bootstrap';
|
||||||
}
|
}
|
||||||
|
|
@ -2846,16 +2850,20 @@ function buildGeminiPostLaunchHydrationPrompt(
|
||||||
const status = run.memberSpawnStatuses.get(member.name);
|
const status = run.memberSpawnStatuses.get(member.name);
|
||||||
const label =
|
const label =
|
||||||
status?.launchState === 'failed_to_start'
|
status?.launchState === 'failed_to_start'
|
||||||
? `failed to start${status.hardFailureReason ? ` — ${status.hardFailureReason}` : status.error ? ` — ${status.error}` : ''}`
|
? `failed to start${status.hardFailureReason ? ` - ${status.hardFailureReason}` : status.error ? ` - ${status.error}` : ''}`
|
||||||
: status?.launchState === 'confirmed_alive'
|
: status?.launchState === 'confirmed_alive'
|
||||||
? 'bootstrap confirmed'
|
? 'bootstrap confirmed'
|
||||||
: status?.runtimeAlive
|
: status?.launchState === 'runtime_pending_permission'
|
||||||
? 'runtime online and ready for instructions'
|
? status?.runtimeAlive
|
||||||
: status?.launchState === 'runtime_pending_bootstrap'
|
? 'runtime online and waiting for permission approval'
|
||||||
? 'spawn accepted, runtime not confirmed yet'
|
: 'waiting for permission approval'
|
||||||
: status?.status === 'spawning'
|
: status?.runtimeAlive
|
||||||
? 'spawn in progress'
|
? 'runtime online and ready for instructions'
|
||||||
: 'runtime state unclear';
|
: status?.launchState === 'runtime_pending_bootstrap'
|
||||||
|
? 'spawn accepted, runtime not confirmed yet'
|
||||||
|
: status?.status === 'spawning'
|
||||||
|
? 'spawn in progress'
|
||||||
|
: 'runtime state unclear';
|
||||||
return `- @${member.name}: ${label}`;
|
return `- @${member.name}: ${label}`;
|
||||||
})
|
})
|
||||||
.join('\n')}\n`
|
.join('\n')}\n`
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,8 @@ export const MemberDetailDialog = ({
|
||||||
);
|
);
|
||||||
const restartInFlight =
|
const restartInFlight =
|
||||||
spawnEntry?.launchState === 'starting' ||
|
spawnEntry?.launchState === 'starting' ||
|
||||||
spawnEntry?.launchState === 'runtime_pending_bootstrap';
|
spawnEntry?.launchState === 'runtime_pending_bootstrap' ||
|
||||||
|
spawnEntry?.launchState === 'runtime_pending_permission';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !member) {
|
if (!open || !member) {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,10 @@ function summarizeLiveLaunchJoinMilestones(params: {
|
||||||
heartbeatConfirmedCount += 1;
|
heartbeatConfirmedCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (entry.launchState === 'runtime_pending_bootstrap') {
|
if (
|
||||||
|
entry.launchState === 'runtime_pending_bootstrap' ||
|
||||||
|
entry.launchState === 'runtime_pending_permission'
|
||||||
|
) {
|
||||||
if (entry.runtimeAlive === true) {
|
if (entry.runtimeAlive === true) {
|
||||||
processOnlyAliveCount += 1;
|
processOnlyAliveCount += 1;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,9 @@ function isLaunchStillStarting(
|
||||||
if (spawnLaunchState === 'failed_to_start') {
|
if (spawnLaunchState === 'failed_to_start') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (spawnLaunchState === 'runtime_pending_bootstrap') {
|
if (spawnLaunchState === 'runtime_pending_bootstrap') {
|
||||||
if (runtimeAlive !== true) {
|
if (runtimeAlive !== true) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -167,6 +170,9 @@ export function getSpawnAwareDotClass(
|
||||||
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
||||||
return SPAWN_DOT_COLORS.error;
|
return SPAWN_DOT_COLORS.error;
|
||||||
}
|
}
|
||||||
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
||||||
|
return 'bg-amber-400 animate-pulse';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
||||||
) {
|
) {
|
||||||
|
|
@ -211,6 +217,9 @@ export function getSpawnAwarePresenceLabel(
|
||||||
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
||||||
return SPAWN_PRESENCE_LABELS.error;
|
return SPAWN_PRESENCE_LABELS.error;
|
||||||
}
|
}
|
||||||
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
||||||
|
return 'connecting';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
||||||
) {
|
) {
|
||||||
|
|
@ -249,6 +258,9 @@ export function getSpawnCardClass(
|
||||||
) {
|
) {
|
||||||
return 'member-waiting-shimmer';
|
return 'member-waiting-shimmer';
|
||||||
}
|
}
|
||||||
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
||||||
|
return 'member-waiting-shimmer';
|
||||||
|
}
|
||||||
switch (spawnStatus) {
|
switch (spawnStatus) {
|
||||||
case 'offline':
|
case 'offline':
|
||||||
return spawnLaunchState === 'starting' ? 'member-waiting-shimmer opacity-75' : 'opacity-40';
|
return spawnLaunchState === 'starting' ? 'member-waiting-shimmer opacity-75' : 'opacity-40';
|
||||||
|
|
@ -433,6 +445,7 @@ export function getLaunchAwarePresenceLabel(
|
||||||
export type MemberLaunchVisualState =
|
export type MemberLaunchVisualState =
|
||||||
| 'waiting'
|
| 'waiting'
|
||||||
| 'spawning'
|
| 'spawning'
|
||||||
|
| 'permission_pending'
|
||||||
| 'runtime_pending'
|
| 'runtime_pending'
|
||||||
| 'settling'
|
| 'settling'
|
||||||
| 'error'
|
| 'error'
|
||||||
|
|
@ -455,6 +468,8 @@ export function getMemberLaunchStatusLabel(visualState: MemberLaunchVisualState)
|
||||||
return 'waiting to start';
|
return 'waiting to start';
|
||||||
case 'spawning':
|
case 'spawning':
|
||||||
return 'starting';
|
return 'starting';
|
||||||
|
case 'permission_pending':
|
||||||
|
return 'awaiting permission';
|
||||||
case 'runtime_pending':
|
case 'runtime_pending':
|
||||||
return 'connecting';
|
return 'connecting';
|
||||||
case 'settling':
|
case 'settling':
|
||||||
|
|
@ -527,6 +542,8 @@ export function buildMemberLaunchPresentation({
|
||||||
if (isTeamAlive !== false || isTeamProvisioning) {
|
if (isTeamAlive !== false || isTeamProvisioning) {
|
||||||
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
||||||
launchVisualState = 'error';
|
launchVisualState = 'error';
|
||||||
|
} else if (spawnLaunchState === 'runtime_pending_permission') {
|
||||||
|
launchVisualState = 'permission_pending';
|
||||||
} else if (
|
} else if (
|
||||||
spawnLaunchState === 'runtime_pending_bootstrap' &&
|
spawnLaunchState === 'runtime_pending_bootstrap' &&
|
||||||
spawnStatus === 'online' &&
|
spawnStatus === 'online' &&
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ function isMemberLaunchPending(spawnEntry: MemberSpawnStatusEntry | undefined):
|
||||||
return (
|
return (
|
||||||
spawnEntry.launchState === 'starting' ||
|
spawnEntry.launchState === 'starting' ||
|
||||||
spawnEntry.launchState === 'runtime_pending_bootstrap' ||
|
spawnEntry.launchState === 'runtime_pending_bootstrap' ||
|
||||||
|
spawnEntry.launchState === 'runtime_pending_permission' ||
|
||||||
spawnEntry.status === 'waiting' ||
|
spawnEntry.status === 'waiting' ||
|
||||||
spawnEntry.status === 'spawning'
|
spawnEntry.status === 'spawning'
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -679,6 +679,7 @@ export type MemberSpawnStatus = 'offline' | 'waiting' | 'spawning' | 'online' |
|
||||||
export type MemberLaunchState =
|
export type MemberLaunchState =
|
||||||
| 'starting'
|
| 'starting'
|
||||||
| 'runtime_pending_bootstrap'
|
| 'runtime_pending_bootstrap'
|
||||||
|
| 'runtime_pending_permission'
|
||||||
| 'confirmed_alive'
|
| 'confirmed_alive'
|
||||||
| 'failed_to_start';
|
| 'failed_to_start';
|
||||||
export type TeamLaunchAggregateState = 'clean_success' | 'partial_pending' | 'partial_failure';
|
export type TeamLaunchAggregateState = 'clean_success' | 'partial_pending' | 'partial_failure';
|
||||||
|
|
@ -933,6 +934,7 @@ export interface PersistedTeamLaunchMemberState {
|
||||||
bootstrapConfirmed: boolean;
|
bootstrapConfirmed: boolean;
|
||||||
hardFailure: boolean;
|
hardFailure: boolean;
|
||||||
hardFailureReason?: string;
|
hardFailureReason?: string;
|
||||||
|
pendingPermissionRequestIds?: string[];
|
||||||
firstSpawnAcceptedAt?: string;
|
firstSpawnAcceptedAt?: string;
|
||||||
lastHeartbeatAt?: string;
|
lastHeartbeatAt?: string;
|
||||||
lastRuntimeAliveAt?: string;
|
lastRuntimeAliveAt?: string;
|
||||||
|
|
@ -1046,6 +1048,8 @@ export interface MemberSpawnStatusEntry {
|
||||||
bootstrapConfirmed?: boolean;
|
bootstrapConfirmed?: boolean;
|
||||||
/** Hard failure observed from spawn/bootstrap/runtime evidence. */
|
/** Hard failure observed from spawn/bootstrap/runtime evidence. */
|
||||||
hardFailure?: boolean;
|
hardFailure?: boolean;
|
||||||
|
/** Pending runtime permission request ids currently blocking bootstrap. */
|
||||||
|
pendingPermissionRequestIds?: string[];
|
||||||
/** ISO timestamp of the first accepted teammate spawn for this member. */
|
/** ISO timestamp of the first accepted teammate spawn for this member. */
|
||||||
firstSpawnAcceptedAt?: string;
|
firstSpawnAcceptedAt?: string;
|
||||||
/** ISO timestamp of the latest confirmed heartbeat/bootstrap message. */
|
/** ISO timestamp of the latest confirmed heartbeat/bootstrap message. */
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,37 @@ describe('MemberCard starting-state visuals', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows an awaiting permission badge for teammates blocked on runtime permissions', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberCard, {
|
||||||
|
member,
|
||||||
|
memberColor: 'blue',
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
spawnStatus: 'online',
|
||||||
|
spawnLaunchState: 'runtime_pending_permission',
|
||||||
|
spawnRuntimeAlive: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('awaiting permission');
|
||||||
|
expect(host.querySelector('[aria-label="connecting"]')).not.toBeNull();
|
||||||
|
expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shows ready instead of idle for confirmed teammates while launch is still settling', async () => {
|
it('shows ready instead of idle for confirmed teammates while launch is still settling', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,26 @@ describe('memberHelpers spawn-aware presence', () => {
|
||||||
expect(settling.launchStatusLabel).toBe('joining team');
|
expect(settling.launchStatusLabel).toBe('joining team');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces permission-blocked teammates as awaiting permission instead of generic starting', () => {
|
||||||
|
const permissionPending = buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: 'online',
|
||||||
|
spawnLaunchState: 'runtime_pending_permission',
|
||||||
|
spawnLivenessSource: 'process',
|
||||||
|
spawnRuntimeAlive: true,
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(permissionPending.presenceLabel).toBe('connecting');
|
||||||
|
expect(permissionPending.launchVisualState).toBe('permission_pending');
|
||||||
|
expect(permissionPending.launchStatusLabel).toBe('awaiting permission');
|
||||||
|
expect(permissionPending.dotClass).toContain('bg-amber-400');
|
||||||
|
expect(permissionPending.cardClass).toContain('member-waiting-shimmer');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns shared launch status labels without changing generic presence labels', () => {
|
it('returns shared launch status labels without changing generic presence labels', () => {
|
||||||
expect(
|
expect(
|
||||||
buildMemberLaunchPresentation({
|
buildMemberLaunchPresentation({
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,70 @@ 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', () => {
|
||||||
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
|
progress: {
|
||||||
|
runId: 'run-4c',
|
||||||
|
teamName: 'opencode-team',
|
||||||
|
state: 'ready',
|
||||||
|
startedAt: '2026-04-13T10:00:00.000Z',
|
||||||
|
updatedAt: '2026-04-13T10:00:08.000Z',
|
||||||
|
message: 'Launch completed',
|
||||||
|
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('Finishing launch');
|
||||||
|
expect(presentation?.compactDetail).toBe('1 teammate still joining');
|
||||||
|
expect(presentation?.panelMessage).toBe('1 teammate still joining');
|
||||||
|
});
|
||||||
|
|
||||||
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', () => {
|
||||||
const presentation = buildTeamProvisioningPresentation({
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
progress: {
|
progress: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue