import { isLeadMember } from '@shared/utils/leadDetection'; import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors'; import { getParticipantAvatarUrlByIndex, LEAD_PARTICIPANT_AVATAR_URL, PARTICIPANT_AVATAR_URLS, } from './memberAvatarCatalog'; import type { LeadActivityState, MemberLaunchState, MemberRuntimeAdvisory, MemberSpawnLivenessSource, MemberSpawnStatus, MemberSpawnStatusEntry, MemberStatus, ResolvedTeamMember, TeamAgentRuntimeEntry, TeamProviderId, TeamReviewState, TeamTaskStatus, } from '@shared/types'; /** * UI display name for a team member. * "team-lead" → "lead"; everything else passes through unchanged. * Data layer (store, IPC, backend) must keep the original name untouched. */ export function displayMemberName(name: string): string { return name === 'team-lead' ? 'lead' : name; } function hashStringToIndex(str: string): number { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0; } return Math.abs(hash); } export function agentAvatarUrl(name: string, size = 64): string { void size; const normalized = name.trim().toLowerCase(); if (normalized === 'team-lead' || normalized === 'lead') { return LEAD_PARTICIPANT_AVATAR_URL; } // Temporarily disabled external avatar API. // return `https://robohash.org/${encodeURIComponent(name)}?size=${size}x${size}`; return getParticipantAvatarUrlByIndex( hashStringToIndex(normalized) % PARTICIPANT_AVATAR_URLS.length ); } export const STATUS_DOT_COLORS: Record = { active: 'bg-emerald-400', idle: 'bg-zinc-400', terminated: 'bg-red-400', unknown: 'bg-zinc-600', }; export function getMemberDotClass( member: ResolvedTeamMember, isTeamAlive?: boolean, isTeamProvisioning?: boolean, leadActivity?: LeadActivityState ): string { if (member.status === 'terminated') return STATUS_DOT_COLORS.terminated; if (member.removedAt) return STATUS_DOT_COLORS.terminated; // Lead activity check BEFORE provisioning fallback — when the lead process // is running (CLI logs present), show green even during provisioning. if (leadActivity && isLeadMember(member)) { return leadActivity === 'active' ? `${STATUS_DOT_COLORS.active} animate-pulse` : STATUS_DOT_COLORS.active; } if (isTeamProvisioning) return STATUS_DOT_COLORS.unknown; if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated; // When team is alive, all non-terminated members are online if (isTeamAlive) { if (member.currentTaskId) return `${STATUS_DOT_COLORS.active} animate-pulse`; return STATUS_DOT_COLORS.active; } if (member.status === 'unknown') return STATUS_DOT_COLORS.unknown; if (member.currentTaskId) return STATUS_DOT_COLORS.active; return member.status === 'active' ? STATUS_DOT_COLORS.active : STATUS_DOT_COLORS.idle; } export function getPresenceLabel( member: ResolvedTeamMember, isTeamAlive?: boolean, isTeamProvisioning?: boolean, leadActivity?: LeadActivityState, leadContextPercent?: number ): string { if (member.status === 'terminated') return 'terminated'; // Lead activity check before provisioning fallback (mirrors getMemberDotClass order). if (leadActivity && isLeadMember(member)) { if (leadActivity === 'active') { return leadContextPercent != null && leadContextPercent > 0 ? `processing (${Math.round(leadContextPercent)}%)` : 'processing'; } return 'ready'; } if (isTeamProvisioning) return 'connecting'; if (isTeamAlive === false) return 'offline'; if (member.status === 'unknown') return 'idle'; return member.currentTaskId ? 'working' : 'idle'; } /* ------------------------------------------------------------------ */ /* Spawn-status-aware helpers for progressive member card appearance */ /* ------------------------------------------------------------------ */ export const SPAWN_DOT_COLORS: Record = { offline: 'bg-zinc-600', waiting: 'bg-zinc-400 animate-pulse', spawning: 'bg-amber-400', online: 'bg-emerald-400 animate-pulse', error: 'bg-red-400', skipped: 'bg-zinc-500', }; export const SPAWN_PRESENCE_LABELS: Record = { offline: 'offline', waiting: 'starting', spawning: 'starting', online: 'ready', error: 'spawn failed', skipped: 'skipped', }; const OPENCODE_RUNTIME_CANDIDATE_RELAUNCH_GRACE_MS = 5 * 60 * 1000; function isLaunchStillStarting( spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState: MemberLaunchState | undefined, runtimeAlive: boolean | undefined, keepRuntimePendingInStarting = false ): boolean { if (spawnLaunchState === 'failed_to_start') { return false; } if (spawnLaunchState === 'skipped_for_launch') { return false; } if (spawnLaunchState === 'runtime_pending_permission') { return false; } if (spawnLaunchState === 'runtime_pending_bootstrap') { if (runtimeAlive !== true) { return true; } return keepRuntimePendingInStarting; } return spawnLaunchState === 'starting' || spawnStatus === 'waiting' || spawnStatus === 'spawning'; } /** * Returns dot class for a member during provisioning, respecting spawn status. * Falls back to the existing `getMemberDotClass` when no spawn status is available. */ export function getSpawnAwareDotClass( member: ResolvedTeamMember, spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState: MemberLaunchState | undefined, runtimeAlive: boolean | undefined, isLaunchSettling = false, isTeamAlive?: boolean, isTeamProvisioning?: boolean, leadActivity?: LeadActivityState ): string { const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling; if (isTeamAlive === false && !isTeamProvisioning) { return STATUS_DOT_COLORS.terminated; } if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') { return SPAWN_DOT_COLORS.error; } if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') { return SPAWN_DOT_COLORS.skipped; } if (spawnLaunchState === 'runtime_pending_permission') { return 'bg-amber-400 animate-pulse'; } if ( isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals) ) { return spawnStatus === 'spawning' ? SPAWN_DOT_COLORS.spawning : SPAWN_DOT_COLORS.waiting; } if (spawnLaunchState === 'runtime_pending_bootstrap' && spawnStatus === 'online') { return SPAWN_DOT_COLORS.online; } if (spawnStatus === 'waiting') { return SPAWN_DOT_COLORS.waiting; } if (spawnStatus === 'online') { return SPAWN_DOT_COLORS.online; } if (spawnStatus === 'offline' && isTeamProvisioning) { return SPAWN_DOT_COLORS.offline; } if (spawnStatus === 'spawning' && isTeamProvisioning) { return SPAWN_DOT_COLORS.spawning; } return getMemberDotClass(member, isTeamAlive, isTeamProvisioning, leadActivity); } /** * Returns presence label for a member during provisioning, respecting spawn status. */ export function getSpawnAwarePresenceLabel( member: ResolvedTeamMember, spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState: MemberLaunchState | undefined, livenessSource: MemberSpawnLivenessSource | undefined, runtimeAlive: boolean | undefined, isLaunchSettling = false, isTeamAlive?: boolean, isTeamProvisioning?: boolean, leadActivity?: LeadActivityState ): string { const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling; if (isTeamAlive === false && !isTeamProvisioning) { return 'offline'; } if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') { return SPAWN_PRESENCE_LABELS.error; } if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') { return SPAWN_PRESENCE_LABELS.skipped; } if (spawnLaunchState === 'runtime_pending_permission') { return 'connecting'; } if ( isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals) ) { return 'starting'; } if (spawnStatus === 'online' && keepLaunchSettlingVisuals) { return SPAWN_PRESENCE_LABELS.online; } if (spawnStatus === 'online' && livenessSource === 'process') { return 'online'; } if (spawnStatus && isTeamProvisioning) { return SPAWN_PRESENCE_LABELS[spawnStatus]; } return getPresenceLabel(member, isTeamAlive, isTeamProvisioning, leadActivity); } /** * Card container CSS classes based on spawn status (opacity + animation). * Used by MemberCard wrapper for fade-in transitions. */ export function getSpawnCardClass( spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState?: MemberLaunchState, runtimeAlive?: boolean, isLaunchSettling = false, isTeamAlive?: boolean, isTeamProvisioning?: boolean ): string { const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling; if (isTeamAlive === false && !isTeamProvisioning) { return ''; } if ( isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals) ) { return 'member-waiting-shimmer'; } if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') { return 'opacity-70'; } if (spawnLaunchState === 'runtime_pending_permission') { return 'member-waiting-shimmer'; } switch (spawnStatus) { case 'offline': return spawnLaunchState === 'starting' ? 'member-waiting-shimmer opacity-75' : 'opacity-40'; case 'waiting': return 'member-waiting-shimmer'; case 'spawning': return 'member-waiting-shimmer'; case 'online': return 'animate-[member-fade-in_0.4s_ease-out]'; case 'error': return 'opacity-80'; default: return ''; } } function formatRetryCountdown(ms: number): string { const totalSeconds = Math.max(1, Math.ceil(ms / 1000)); if (totalSeconds < 60) { return `${totalSeconds}s`; } const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; } function getRuntimeAdvisoryProviderLabel(providerId: TeamProviderId | undefined): string | null { switch (providerId) { case 'anthropic': return 'Anthropic'; case 'codex': return 'Codex'; case 'gemini': return 'Gemini'; case 'opencode': return 'OpenCode'; default: return null; } } function appendRuntimeAdvisoryRawMessage(base: string, message: string | undefined): string { const trimmed = formatRuntimeAdvisoryDisplayMessage(message); return trimmed ? `${base}\n\n${trimmed}` : base; } function isOpenCodeRuntimeDeliveryAdvisoryMessage(message: string | undefined): boolean { const displayMessage = formatRuntimeAdvisoryDisplayMessage(message); return ( displayMessage.startsWith('OpenCode runtime delivery') || displayMessage.startsWith('OpenCode returned an empty assistant turn') || displayMessage.startsWith('OpenCode accepted the prompt') || displayMessage.startsWith('OpenCode responded, but did not create') || displayMessage.startsWith('OpenCode created a reply without') || displayMessage.startsWith('OpenCode used tools, but did not create') ); } function formatRuntimeAdvisoryDisplayMessage(message: string | undefined): string { const trimmed = message?.trim(); if (!trimmed) { return ''; } if (trimmed === 'empty_assistant_turn') { return 'OpenCode returned an empty assistant turn.'; } if (trimmed === 'prompt_delivered_no_assistant_message') { return 'OpenCode accepted the prompt, but no assistant turn was recorded.'; } if ( trimmed === 'visible_reply_still_required' || trimmed === 'visible_reply_ack_only_still_requires_answer' || trimmed === 'plain_text_ack_only_still_requires_answer' ) { return 'OpenCode responded, but did not create a visible message_send reply.'; } if ( trimmed === 'visible_reply_destination_not_found_yet' || trimmed === 'visible_reply_missing_relayOfMessageId' ) { return 'OpenCode created a reply without the required relayOfMessageId correlation.'; } if (trimmed === 'non_visible_tool_without_task_progress') { return 'OpenCode used tools, but did not create a visible reply or task progress proof.'; } if ( trimmed.startsWith( 'OpenCode bootstrap MCP did not complete required tools before assistant response:' ) ) { return 'OpenCode runtime delivery did not complete.'; } return trimmed; } function formatRuntimeAdvisoryBaseLabel( advisory: MemberRuntimeAdvisory, providerId: TeamProviderId | undefined ): string { const providerLabel = getRuntimeAdvisoryProviderLabel(providerId); if (advisory.kind === 'api_error') { switch (advisory.reasonCode) { case 'quota_exhausted': return providerLabel ? `${providerLabel} quota error` : 'Quota error'; case 'rate_limited': return providerLabel ? `${providerLabel} rate limit` : 'Rate limit'; case 'auth_error': return providerLabel ? `${providerLabel} auth error` : 'Auth error'; case 'codex_native_timeout': return 'Codex native timeout'; case 'network_error': return 'Network error'; case 'provider_overloaded': return providerLabel ? `${providerLabel} overload` : 'Provider overload'; case 'protocol_proof_missing': return providerId === 'opencode' ? 'OpenCode proof missing' : 'Protocol proof missing'; case 'backend_error': case 'unknown': if ( providerId === 'opencode' && isOpenCodeRuntimeDeliveryAdvisoryMessage(advisory.message) ) { return 'OpenCode delivery error'; } return providerLabel ? `${providerLabel} API error` : 'API error'; default: return 'API error'; } } switch (advisory.reasonCode) { case 'quota_exhausted': return providerLabel ? `${providerLabel} quota retry` : 'Quota retry'; case 'rate_limited': return providerLabel ? `${providerLabel} rate limit` : 'Rate limit retry'; case 'auth_error': return providerLabel ? `${providerLabel} auth retry` : 'Auth retry'; case 'codex_native_timeout': return 'Codex native retry'; case 'network_error': return 'Network retry'; case 'provider_overloaded': return providerLabel ? `${providerLabel} overload retry` : 'Provider overload retry'; case 'protocol_proof_missing': return providerId === 'opencode' ? 'OpenCode proof missing' : 'Protocol proof missing'; case 'backend_error': case 'unknown': return 'Provider retry'; default: return 'retrying now'; } } function formatRuntimeAdvisoryTitle( advisory: MemberRuntimeAdvisory, providerId: TeamProviderId | undefined ): string { const providerLabel = getRuntimeAdvisoryProviderLabel(providerId); if (advisory.kind === 'api_error') { switch (advisory.reasonCode) { case 'quota_exhausted': return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} quota exhausted.`, advisory.message ); case 'rate_limited': return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} rate limited the request.`, advisory.message ); case 'auth_error': return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} authentication error.`, advisory.message ); case 'codex_native_timeout': return appendRuntimeAdvisoryRawMessage( 'Codex native mailbox turn timed out. The runtime stopped this turn after its watchdog limit; it was not an automatic SDK retry.', advisory.message ); case 'network_error': return appendRuntimeAdvisoryRawMessage('Network or connectivity error.', advisory.message); case 'provider_overloaded': return appendRuntimeAdvisoryRawMessage( 'Provider is temporarily overloaded.', advisory.message ); case 'protocol_proof_missing': return appendRuntimeAdvisoryRawMessage( providerId === 'opencode' ? 'OpenCode delivery completed without required visible/progress proof.' : 'Runtime delivery completed without required protocol proof.', advisory.message ); case 'backend_error': case 'unknown': if ( providerId === 'opencode' && isOpenCodeRuntimeDeliveryAdvisoryMessage(advisory.message) ) { return appendRuntimeAdvisoryRawMessage( 'OpenCode runtime delivery error.', advisory.message ); } return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} API error.`, advisory.message ); default: return advisory.message?.trim() || 'Provider API error.'; } } switch (advisory.reasonCode) { case 'quota_exhausted': return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} quota exhausted. SDK is retrying automatically.`, advisory.message ); case 'rate_limited': return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} rate limited the request. SDK is retrying automatically.`, advisory.message ); case 'auth_error': return appendRuntimeAdvisoryRawMessage( `${providerLabel ?? 'Provider'} authentication issue. SDK is retrying automatically.`, advisory.message ); case 'codex_native_timeout': return appendRuntimeAdvisoryRawMessage( 'Codex native mailbox turn timed out. A retry window was reported by the runtime.', advisory.message ); case 'network_error': return appendRuntimeAdvisoryRawMessage( 'Network or connectivity issue. SDK is retrying automatically.', advisory.message ); case 'provider_overloaded': return appendRuntimeAdvisoryRawMessage( 'Provider is temporarily overloaded. SDK is retrying automatically.', advisory.message ); case 'protocol_proof_missing': return appendRuntimeAdvisoryRawMessage( providerId === 'opencode' ? 'OpenCode delivery is waiting for required visible/progress proof.' : 'Runtime delivery is waiting for required protocol proof.', advisory.message ); case 'backend_error': case 'unknown': return appendRuntimeAdvisoryRawMessage( 'The SDK is retrying this request after a provider or backend error.', advisory.message ); default: return ( advisory.message?.trim() || 'The SDK is retrying this request after a provider or backend error.' ); } } export function getMemberRuntimeAdvisoryLabel( advisory: MemberRuntimeAdvisory | undefined, providerId?: TeamProviderId, nowMs = Date.now() ): string | null { if (!advisory) { return null; } const baseLabel = formatRuntimeAdvisoryBaseLabel(advisory, providerId); if (advisory.kind === 'api_error') { return baseLabel; } if (advisory.kind !== 'sdk_retrying') { return null; } const retryUntilMs = advisory.retryUntil ? Date.parse(advisory.retryUntil) : Number.NaN; if (!Number.isFinite(retryUntilMs)) { return baseLabel; } const remainingMs = retryUntilMs - nowMs; if (remainingMs <= 0) { return baseLabel; } return `${baseLabel} · ${formatRetryCountdown(remainingMs)}`; } export function getMemberRuntimeAdvisoryTitle( advisory: MemberRuntimeAdvisory | undefined, providerId?: TeamProviderId ): string | undefined { if (!advisory || (advisory.kind !== 'sdk_retrying' && advisory.kind !== 'api_error')) { return undefined; } return formatRuntimeAdvisoryTitle(advisory, providerId); } export function getMemberRuntimeAdvisoryTone( advisory: MemberRuntimeAdvisory | undefined ): 'error' | 'warning' | null { if (!advisory) { return null; } if (advisory.reasonCode === 'protocol_proof_missing') { return 'warning'; } return advisory.kind === 'api_error' ? 'error' : 'warning'; } export function getLaunchAwarePresenceLabel( member: ResolvedTeamMember, spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState: MemberLaunchState | undefined, livenessSource: MemberSpawnLivenessSource | undefined, runtimeAlive: boolean | undefined, runtimeAdvisory: MemberRuntimeAdvisory | undefined, isLaunchSettling = false, isTeamAlive?: boolean, isTeamProvisioning?: boolean, leadActivity?: LeadActivityState ): string { const basePresenceLabel = getSpawnAwarePresenceLabel( member, spawnStatus, spawnLaunchState, livenessSource, runtimeAlive, isLaunchSettling, isTeamAlive, isTeamProvisioning, leadActivity ); if ( basePresenceLabel === 'starting' || basePresenceLabel === 'connecting' || basePresenceLabel === 'spawn failed' || basePresenceLabel === 'skipped' || basePresenceLabel === 'offline' || basePresenceLabel === 'terminated' ) { return basePresenceLabel; } const advisoryLabel = getMemberRuntimeAdvisoryLabel(runtimeAdvisory, member.providerId); return advisoryLabel ?? basePresenceLabel; } export type MemberLaunchVisualState = | 'queued' | 'waiting' | 'spawning' | 'permission_pending' | 'bootstrap_stalled' | 'runtime_pending' | 'shell_only' | 'runtime_candidate' | 'registered_only' | 'stale_runtime' | 'settling' | 'error' | 'skipped' | null; export interface MemberLaunchPresentation { presenceLabel: string; dotClass: string; cardClass: string; runtimeAdvisoryLabel: string | null; runtimeAdvisoryTitle?: string; runtimeAdvisoryTone: 'error' | 'warning' | null; launchVisualState: MemberLaunchVisualState; launchStatusLabel: string | null; spawnBadgeLabel: string | null; } export function getMemberLaunchStatusLabel(visualState: MemberLaunchVisualState): string | null { switch (visualState) { case 'queued': return 'queued'; case 'waiting': return 'waiting to start'; case 'spawning': return 'starting'; case 'permission_pending': return 'awaiting permission'; case 'bootstrap_stalled': return 'bootstrap stalled'; case 'runtime_pending': return 'waiting for bootstrap'; case 'shell_only': return 'shell only'; case 'runtime_candidate': return 'bootstrap unconfirmed'; case 'registered_only': return 'registered'; case 'stale_runtime': return 'stale runtime'; case 'settling': return 'joining team'; case 'error': return 'failed'; case 'skipped': return 'skipped'; default: return null; } } function getLaunchVisualStateDotClass(visualState: MemberLaunchVisualState): string | null { switch (visualState) { case 'queued': return SPAWN_DOT_COLORS.waiting; case 'permission_pending': case 'bootstrap_stalled': case 'runtime_pending': case 'runtime_candidate': return 'bg-amber-400 animate-pulse'; case 'registered_only': return SPAWN_DOT_COLORS.waiting; case 'shell_only': return 'bg-amber-400'; case 'stale_runtime': return STATUS_DOT_COLORS.terminated; default: return null; } } export function shouldDisplayMemberCurrentTask({ member, isTeamAlive, spawnStatus, spawnLaunchState, spawnRuntimeAlive, runtimeEntry, }: { member: ResolvedTeamMember; isTeamAlive?: boolean; spawnStatus?: MemberSpawnStatus; spawnLaunchState?: MemberLaunchState; spawnRuntimeAlive?: boolean; runtimeEntry?: TeamAgentRuntimeEntry; }): boolean { if (member.removedAt || member.status === 'terminated') { return false; } if (isTeamAlive === false) { return false; } if (spawnStatus === 'offline' || spawnStatus === 'error' || spawnStatus === 'skipped') { return false; } if ( spawnLaunchState === 'failed_to_start' || spawnLaunchState === 'skipped_for_launch' || spawnLaunchState === 'runtime_pending_permission' ) { return false; } if ( runtimeEntry?.livenessKind === 'shell_only' || runtimeEntry?.livenessKind === 'registered_only' || runtimeEntry?.livenessKind === 'stale_metadata' || runtimeEntry?.livenessKind === 'not_found' ) { return false; } if (runtimeEntry?.alive === false && spawnStatus !== 'online') { return false; } if (spawnRuntimeAlive === false && spawnStatus !== 'online') { return false; } return true; } function isQueuedOpenCodeLaunch( member: ResolvedTeamMember, spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState: MemberLaunchState | undefined, runtimeEntry: TeamAgentRuntimeEntry | undefined, isLaunchSettling: boolean, isTeamProvisioning: boolean | undefined ): boolean { if (member.providerId !== 'opencode') { return false; } if (isTeamProvisioning !== true && !isLaunchSettling) { return false; } if (spawnStatus !== 'waiting' || spawnLaunchState !== 'starting') { return false; } // Only label lanes as queued before runtime evidence appears. Once the // backend has any liveness signal, show the exact runtime state instead. return runtimeEntry?.livenessKind == null; } function hasElapsedSinceIso( value: string | undefined, thresholdMs: number, nowMs: number ): boolean { if (!value) { return false; } const parsed = Date.parse(value); return Number.isFinite(parsed) && nowMs - parsed >= thresholdMs; } function hasBootstrapStallDiagnostic(value: string | undefined): boolean { const normalized = value?.trim().toLowerCase() ?? ''; return ( normalized.includes('no bootstrap check-in') || normalized.includes('bootstrap is unconfirmed') || normalized.includes('bootstrap unconfirmed') ); } export function isOpenCodeRelaunchActionable({ member, spawnEntry, runtimeEntry, nowMs = Date.now(), }: { member: ResolvedTeamMember; spawnEntry?: MemberSpawnStatusEntry; runtimeEntry?: TeamAgentRuntimeEntry; nowMs?: number; }): boolean { if (member.providerId !== 'opencode' || isLeadMember(member) || member.removedAt) { return false; } if (spawnEntry?.launchState === 'starting' || spawnEntry?.status === 'spawning') { return false; } if (spawnEntry?.launchState === 'runtime_pending_permission') { return false; } if (!spawnEntry) { const runtimeDiagnosticIsStuck = hasBootstrapStallDiagnostic(runtimeEntry?.runtimeDiagnostic); return ( runtimeDiagnosticIsStuck || runtimeEntry?.livenessKind === 'registered_only' || runtimeEntry?.livenessKind === 'stale_metadata' ); } if ( spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.launchState === 'skipped_for_launch' || spawnEntry?.status === 'error' || spawnEntry?.status === 'skipped' ) { return true; } const livenessKind = runtimeEntry?.livenessKind ?? spawnEntry?.livenessKind; const acceptedSpawnGraceElapsed = hasElapsedSinceIso( spawnEntry?.firstSpawnAcceptedAt, OPENCODE_RUNTIME_CANDIDATE_RELAUNCH_GRACE_MS, nowMs ); const hasExplicitBootstrapStall = spawnEntry?.bootstrapStalled === true || hasBootstrapStallDiagnostic(spawnEntry?.runtimeDiagnostic) || hasBootstrapStallDiagnostic(runtimeEntry?.runtimeDiagnostic); const launchIsNoLongerFresh = spawnEntry.launchState === 'confirmed_alive' || spawnEntry.status === 'online' || acceptedSpawnGraceElapsed || hasExplicitBootstrapStall; if ( livenessKind === 'registered_only' || livenessKind === 'stale_metadata' || livenessKind === 'not_found' ) { return launchIsNoLongerFresh; } if (livenessKind === 'runtime_process') { return hasExplicitBootstrapStall; } if (livenessKind !== 'runtime_process_candidate') { return false; } return acceptedSpawnGraceElapsed || hasExplicitBootstrapStall; } export function buildMemberLaunchPresentation({ member, spawnStatus, spawnLaunchState, spawnLivenessSource, spawnRuntimeAlive, spawnBootstrapConfirmed, spawnBootstrapStalled, runtimeAdvisory, runtimeEntry, isLaunchSettling = false, isTeamAlive, isTeamProvisioning, leadActivity, }: { member: ResolvedTeamMember; spawnStatus: MemberSpawnStatus | undefined; spawnLaunchState: MemberLaunchState | undefined; spawnLivenessSource: MemberSpawnLivenessSource | undefined; spawnRuntimeAlive: boolean | undefined; spawnBootstrapConfirmed?: boolean; spawnBootstrapStalled?: boolean; runtimeAdvisory: MemberRuntimeAdvisory | undefined; runtimeEntry?: TeamAgentRuntimeEntry; isLaunchSettling?: boolean; isTeamAlive?: boolean; isTeamProvisioning?: boolean; leadActivity?: LeadActivityState; }): MemberLaunchPresentation { const hasConfirmedSpawnLaunch = spawnLaunchState === 'confirmed_alive' && spawnBootstrapConfirmed === true; const effectiveSpawnStatus = hasConfirmedSpawnLaunch && (spawnStatus === 'waiting' || spawnStatus === 'spawning') ? 'online' : spawnStatus; const effectiveSpawnRuntimeAlive = hasConfirmedSpawnLaunch ? true : spawnRuntimeAlive; const presenceLabel = getLaunchAwarePresenceLabel( member, effectiveSpawnStatus, spawnLaunchState, spawnLivenessSource, effectiveSpawnRuntimeAlive, runtimeAdvisory, isLaunchSettling, isTeamAlive, isTeamProvisioning, leadActivity ); const baseDotClass = getSpawnAwareDotClass( member, effectiveSpawnStatus, spawnLaunchState, effectiveSpawnRuntimeAlive, isLaunchSettling, isTeamAlive, isTeamProvisioning, leadActivity ); const cardClass = getSpawnCardClass( effectiveSpawnStatus, spawnLaunchState, effectiveSpawnRuntimeAlive, isLaunchSettling, isTeamAlive, isTeamProvisioning ); const runtimeAdvisoryLabel = getMemberRuntimeAdvisoryLabel(runtimeAdvisory, member.providerId); const runtimeAdvisoryTitle = getMemberRuntimeAdvisoryTitle(runtimeAdvisory, member.providerId); const runtimeAdvisoryTone = getMemberRuntimeAdvisoryTone(runtimeAdvisory); const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling; let launchVisualState: MemberLaunchVisualState = null; if (isTeamAlive !== false || isTeamProvisioning) { if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') { launchVisualState = 'error'; } else if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') { launchVisualState = 'skipped'; } else if (spawnLaunchState === 'runtime_pending_permission') { launchVisualState = 'permission_pending'; } else if (spawnBootstrapStalled === true) { launchVisualState = 'bootstrap_stalled'; } else if (!hasConfirmedSpawnLaunch && runtimeEntry?.livenessKind === 'shell_only') { launchVisualState = 'shell_only'; } else if ( !hasConfirmedSpawnLaunch && runtimeEntry?.livenessKind === 'runtime_process_candidate' ) { launchVisualState = 'runtime_candidate'; } else if (!hasConfirmedSpawnLaunch && runtimeEntry?.livenessKind === 'registered_only') { launchVisualState = 'registered_only'; } else if ( !hasConfirmedSpawnLaunch && (runtimeEntry?.livenessKind === 'stale_metadata' || runtimeEntry?.livenessKind === 'not_found') ) { launchVisualState = 'stale_runtime'; } else if ( !hasConfirmedSpawnLaunch && isQueuedOpenCodeLaunch( member, spawnStatus, spawnLaunchState, runtimeEntry, isLaunchSettling, isTeamProvisioning ) ) { launchVisualState = 'queued'; } else if ( !hasConfirmedSpawnLaunch && isLaunchStillStarting( spawnStatus, spawnLaunchState, spawnRuntimeAlive, keepLaunchSettlingVisuals ) ) { launchVisualState = spawnStatus === 'spawning' ? 'spawning' : 'waiting'; } else if ( !hasConfirmedSpawnLaunch && spawnLaunchState === 'runtime_pending_bootstrap' && (runtimeEntry?.livenessKind === 'runtime_process' || (spawnStatus === 'online' && spawnRuntimeAlive === true)) ) { launchVisualState = 'runtime_pending'; } else if (isLaunchSettling && spawnLaunchState === 'confirmed_alive') { launchVisualState = 'settling'; } } const launchStatusLabel = getMemberLaunchStatusLabel(launchVisualState); const launchVisualStateDotClass = getLaunchVisualStateDotClass(launchVisualState); const shouldShowLaunchStatusAsPresence = launchVisualState === 'queued' || launchVisualState === 'permission_pending' || launchVisualState === 'bootstrap_stalled' || launchVisualState === 'runtime_pending' || launchVisualState === 'shell_only' || launchVisualState === 'runtime_candidate' || launchVisualState === 'registered_only' || launchVisualState === 'stale_runtime'; const displayPresenceLabel = runtimeAdvisoryTone === 'error' && runtimeAdvisoryLabel ? runtimeAdvisoryLabel : shouldShowLaunchStatusAsPresence ? (launchStatusLabel ?? presenceLabel) : presenceLabel; const spawnBadgeLabel = spawnStatus && spawnStatus !== 'online' ? spawnStatus === 'waiting' || spawnStatus === 'spawning' ? 'starting' : spawnStatus : null; return { presenceLabel: displayPresenceLabel, dotClass: runtimeAdvisoryTone === 'error' ? STATUS_DOT_COLORS.terminated : (launchVisualStateDotClass ?? baseDotClass), cardClass, runtimeAdvisoryLabel, runtimeAdvisoryTitle, runtimeAdvisoryTone, launchVisualState, launchStatusLabel, spawnBadgeLabel, }; } export const TASK_STATUS_STYLES: Record = { pending: { bg: 'bg-zinc-500/15', text: 'text-zinc-400' }, in_progress: { bg: 'bg-blue-500/15', text: 'text-blue-400' }, completed: { bg: 'bg-emerald-500/15', text: 'text-emerald-400' }, deleted: { bg: 'bg-red-500/15', text: 'text-red-400' }, }; export const TASK_STATUS_LABELS: Record = { pending: 'Pending', in_progress: 'In Progress', completed: 'Completed', deleted: 'Deleted', }; interface MemberColorInput { name: string; color?: string; removedAt?: number | string | null; agentType?: string; role?: string; } interface MemberAvatarInput { name: string; removedAt?: number | string | null; agentType?: string; } /** * Build a consistent name→colorName map for all members. * Active members receive colors sequentially from MEMBER_COLOR_PALETTE, * which is pre-ordered for maximum visual contrast between consecutive entries. * If a member has a stored color that hasn't been assigned yet, it is used instead. * Maps "user" to a reserved color. */ export function buildMemberColorMap(members: MemberColorInput[]): Map { return buildTeamMemberColorMap(members, { preferProvidedColors: true }); } export function buildMemberAvatarMap(members: readonly MemberAvatarInput[]): Map { const map = new Map(); const activeMembers = members.filter((member) => !member.removedAt); const leadMembers = activeMembers.filter((member) => isLeadMember(member)); const teammateMembers = activeMembers.filter((member) => !isLeadMember(member)); for (const [index, member] of leadMembers.entries()) { map.set(member.name, index === 0 ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name)); } for (const [index, member] of teammateMembers.entries()) { map.set( member.name, getParticipantAvatarUrlByIndex(1 + (index % (PARTICIPANT_AVATAR_URLS.length - 1))) ); } for (const member of members) { if (!map.has(member.name)) { map.set( member.name, isLeadMember(member) ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name) ); } } map.set('user', agentAvatarUrl('user')); map.set('system', agentAvatarUrl('system')); return map; } export function resolveMemberAvatarUrl( member: MemberAvatarInput, avatarMap?: ReadonlyMap, size = 64 ): string { return ( avatarMap?.get(member.name) ?? (isLeadMember(member) ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name, size)) ); } export const KANBAN_COLUMN_DISPLAY: Record< 'review' | 'approved', { label: string; bg: string; text: string } > = { review: { label: 'In Review', bg: 'bg-amber-500/15', text: 'text-amber-400' }, approved: { label: 'Approved', bg: 'bg-emerald-500/15', text: 'text-emerald-400' }, }; export const REVIEW_STATE_DISPLAY: Record< Exclude, { label: string; bg: string; text: string } > = { review: { label: 'In Review', bg: 'bg-amber-500/15', text: 'text-amber-400' }, needsFix: { label: 'Needs Fixes', bg: 'bg-rose-500/15', text: 'text-rose-400' }, approved: { label: 'Approved', bg: 'bg-emerald-500/15', text: 'text-emerald-400' }, };