import React from 'react'; import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdentity'; import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog'; import { AlertTriangle, CheckCircle2, Loader2 } from 'lucide-react'; import type { CliProviderStatus, TeamProviderId } from '@shared/types'; export type ProvisioningProviderCheckStatus = 'pending' | 'checking' | 'ready' | 'notes' | 'failed'; export type ProvisioningPrepareState = 'idle' | 'loading' | 'ready' | 'failed'; export interface ProvisioningProviderCheck { providerId: TeamProviderId; status: ProvisioningProviderCheckStatus; backendSummary?: string | null; details: string[]; } export function getProvisioningProviderLabel(providerId: TeamProviderId): string { return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic'; } export function createInitialProviderChecks( providerIds: TeamProviderId[] ): ProvisioningProviderCheck[] { return providerIds.map((providerId) => ({ providerId, status: 'pending', backendSummary: null, details: [], })); } export function getProvisioningProviderBackendSummary( provider: | Pick< CliProviderStatus, 'providerId' | 'selectedBackendId' | 'resolvedBackendId' | 'availableBackends' | 'backend' > | null | undefined ): string | null { if (!provider) { return null; } const options = provider.availableBackends ?? []; const optionById = new Map(options.map((option) => [option.id, option.label])); const effectiveBackendId = provider.resolvedBackendId ?? provider.selectedBackendId; const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null; const inferredProviderId: TeamProviderId | undefined = provider.providerId === 'anthropic' || provider.providerId === 'codex' || provider.providerId === 'gemini' || provider.providerId === 'opencode' ? provider.providerId : effectiveBackendId === 'codex-native' || options.some((option) => option.id === 'codex-native') ? 'codex' : undefined; const normalizedLabel = formatProviderBackendLabel(inferredProviderId, effectiveBackendId ?? undefined) ?? null; const baseSummary = effectiveBackendId ? (normalizedLabel ?? optionById.get(effectiveBackendId) ?? provider.backend?.label ?? effectiveBackendId) : (provider.backend?.label ?? null); if (!baseSummary) { return null; } const suffixes: string[] = []; if (effectiveOption?.audience === 'internal') { suffixes.push('internal'); } if (effectiveOption?.state && effectiveOption.state !== 'ready') { switch (effectiveOption.state) { case 'locked': suffixes.push('locked'); break; case 'disabled': suffixes.push('disabled'); break; case 'authentication-required': suffixes.push('auth required'); break; case 'runtime-missing': suffixes.push('runtime missing'); break; case 'degraded': suffixes.push('degraded'); break; default: break; } } return suffixes.length > 0 ? `${baseSummary} - ${suffixes.join(', ')}` : baseSummary; } export function updateProviderCheck( checks: ProvisioningProviderCheck[], providerId: TeamProviderId, patch: Partial ): ProvisioningProviderCheck[] { return checks.map((check) => check.providerId === providerId ? { ...check, ...patch, } : check ); } export function failIncompleteProviderChecks( checks: ProvisioningProviderCheck[], detail: string ): ProvisioningProviderCheck[] { return checks.map((check) => check.status === 'ready' || check.status === 'notes' || check.status === 'failed' ? check : { ...check, status: 'failed', details: check.details.length > 0 ? check.details : [detail], } ); } type ProvisioningDetailSummary = | 'CLI binary missing' | 'Working directory missing' | 'CLI binary could not be started' | 'CLI preflight did not complete' | 'Authentication required' | 'Runtime provider is not configured' | 'CLI preflight failed' | 'Selected model compatibility pending' | 'Selected model available' | 'Selected model verified' | 'Selected model unavailable' | 'Selected model verification timed out' | 'Selected model check failed' | 'Ready with notes' | 'Needs attention'; function isSelectedModelDetail(lower: string): boolean { return lower.includes('selected model'); } function isFormattedModelDetail(lower: string): boolean { return ( lower.includes(' - checking...') || lower.includes(' - verified') || lower.includes(' - available for launch') || lower.includes(' - compatible, deep verification pending') || lower.includes(' - unavailable') || lower.includes(' - check failed') ); } function isModelDetail(lower: string): boolean { return isSelectedModelDetail(lower) || isFormattedModelDetail(lower); } function getStatusLabel(status: ProvisioningProviderCheckStatus): string { switch (status) { case 'checking': return 'checking...'; case 'ready': return 'OK'; case 'notes': return 'OK (notes)'; case 'failed': return 'ERR'; case 'pending': default: return 'waiting'; } } function summarizeDetail( detail: string, status: ProvisioningProviderCheckStatus ): ProvisioningDetailSummary | null { const lower = detail.toLowerCase(); if (lower.includes('spawn ') && lower.includes(' enoent')) { return 'CLI binary missing'; } if (lower.includes('working directory does not exist:')) { return 'Working directory missing'; } if ( lower.includes('eacces') || lower.includes('enoexec') || lower.includes('bad cpu type in executable') || lower.includes('image not found') ) { return 'CLI binary could not be started'; } if (lower.includes('preflight check for `') && lower.includes('-p` did not complete')) { return 'CLI preflight did not complete'; } if (lower.includes('not authenticated') || lower.includes('not logged in')) { return 'Authentication required'; } if (lower.includes('provider is not configured for runtime use')) { return 'Runtime provider is not configured'; } if (lower.includes('claude cli binary failed to start')) { return 'CLI binary could not be started'; } if (lower.includes('claude cli preflight check failed')) { return 'CLI preflight failed'; } if (isModelDetail(lower) && lower.includes('compatible, deep verification pending')) { return 'Selected model compatibility pending'; } if (isSelectedModelDetail(lower) && lower.includes('verified for launch')) { return 'Selected model verified'; } if (isSelectedModelDetail(lower) && lower.includes('available for launch')) { return 'Selected model available'; } if (isSelectedModelDetail(lower) && lower.includes('is unavailable')) { return 'Selected model unavailable'; } if ( isSelectedModelDetail(lower) && lower.includes('could not be verified') && lower.includes('timed out') ) { return 'Selected model verification timed out'; } if (isSelectedModelDetail(lower) && lower.includes('could not be verified')) { return 'Selected model check failed'; } if (lower.includes(' - verified')) { return 'Selected model verified'; } if (lower.includes(' - available for launch')) { return 'Selected model available'; } if (lower.includes(' - unavailable -')) { return 'Selected model unavailable'; } if (lower.includes(' - check failed') && lower.includes('timed out')) { return 'Selected model verification timed out'; } if (lower.includes(' - check failed -')) { return 'Selected model check failed'; } if (status === 'notes') { return 'Ready with notes'; } if (status === 'failed') { return 'Needs attention'; } return null; } function getModelDetailSummary(details: string[]): string | null { let compatibilityPendingCount = 0; let availableCount = 0; let verifiedCount = 0; let unavailableCount = 0; let timedOutCount = 0; let checkFailedCount = 0; let checkingCount = 0; for (const detail of details) { const lower = detail.toLowerCase(); if (!isModelDetail(lower)) { continue; } if (lower.includes('compatible, deep verification pending')) { compatibilityPendingCount += 1; continue; } if ( lower.includes(' - available for launch') || (isSelectedModelDetail(lower) && lower.includes('is available for launch')) ) { availableCount += 1; continue; } if ( lower.includes(' - verified') || (isSelectedModelDetail(lower) && lower.includes('verified for launch')) ) { verifiedCount += 1; continue; } if ( lower.includes(' - unavailable -') || (isSelectedModelDetail(lower) && lower.includes('is unavailable')) ) { unavailableCount += 1; continue; } if ( lower.includes('timed out') && (lower.includes('check failed') || (isSelectedModelDetail(lower) && lower.includes('could not be verified'))) ) { timedOutCount += 1; continue; } if ( lower.includes(' - check failed -') || (isSelectedModelDetail(lower) && lower.includes('could not be verified')) ) { checkFailedCount += 1; continue; } if (lower.includes(' - checking...')) { checkingCount += 1; } } const parts: string[] = []; if (unavailableCount > 0) { parts.push(`${unavailableCount} model${unavailableCount === 1 ? '' : 's'} unavailable`); } if (checkFailedCount > 0) { parts.push(`${checkFailedCount} model${checkFailedCount === 1 ? '' : 's'} check failed`); } if (timedOutCount > 0) { parts.push(`${timedOutCount} model${timedOutCount === 1 ? '' : 's'} timed out`); } if (compatibilityPendingCount > 0) { parts.push(`${compatibilityPendingCount} compatible, deep verification pending`); } if (checkingCount > 0) { parts.push(`${checkingCount} checking`); } if (availableCount > 0) { parts.push(`${availableCount} available`); } if (verifiedCount > 0) { parts.push(`${verifiedCount} verified`); } return parts.length > 0 ? `Selected model checks - ${parts.join(', ')}` : null; } function hasCompatibilityPendingDetails(checks: ProvisioningProviderCheck[]): boolean { return checks.some((check) => check.details.some((detail) => detail.toLowerCase().includes('compatible, deep verification pending') ) ); } function getDisplayStatusText(check: ProvisioningProviderCheck): string { const modelSummary = getModelDetailSummary(check.details); if (modelSummary) { return modelSummary; } const summarizedDetails = check.details .map((detail) => summarizeDetail(detail, check.status)) .filter((detail): detail is ProvisioningDetailSummary => Boolean(detail)); const summary = check.status === 'failed' ? (summarizedDetails.find( (detail) => detail === 'Selected model unavailable' || detail === 'Selected model check failed' || detail === 'Authentication required' || detail === 'CLI preflight failed' || detail === 'CLI binary could not be started' ) ?? summarizedDetails[0] ?? null) : (summarizedDetails[0] ?? null); return summary ?? getStatusLabel(check.status); } function getDetailTone( detail: string, status: ProvisioningProviderCheckStatus ): 'success' | 'failure' | 'checking' | 'neutral' { const summary = summarizeDetail(detail, status); if (summary === 'Selected model verified' || summary === 'Selected model available') { return 'success'; } if (summary === 'Selected model verification timed out') { return 'neutral'; } if ( summary === 'Selected model unavailable' || summary === 'Selected model check failed' || summary === 'CLI binary missing' || summary === 'Working directory missing' || summary === 'CLI binary could not be started' || summary === 'CLI preflight did not complete' || summary === 'Authentication required' || summary === 'Runtime provider is not configured' || summary === 'CLI preflight failed' || summary === 'Needs attention' ) { return 'failure'; } if (detail.toLowerCase().includes(' - checking...')) { return 'checking'; } return 'neutral'; } function getDetailColorClass(detail: string, status: ProvisioningProviderCheckStatus): string { switch (getDetailTone(detail, status)) { case 'success': return 'text-emerald-400'; case 'failure': return 'text-red-300'; case 'checking': return 'text-[var(--color-text-secondary)]'; case 'neutral': default: return 'text-[var(--color-text-muted)]'; } } export function getPrimaryProvisioningFailureDetail( checks: ProvisioningProviderCheck[] ): string | null { for (const check of checks) { if (check.status !== 'failed') { continue; } const unavailableDetail = check.details.find((detail) => detail.toLowerCase().includes('selected model') && detail.toLowerCase().includes('is unavailable') ? true : detail.toLowerCase().includes(' - unavailable -') ); if (unavailableDetail) { return unavailableDetail; } } for (const check of checks) { if (check.status !== 'failed') { continue; } const preferredFailure = check.details.find( (detail) => getDetailTone(detail, check.status) === 'failure' ); if (preferredFailure) { return preferredFailure; } const nonSuccessDetail = check.details.find( (detail) => getDetailTone(detail, check.status) !== 'success' ); if (nonSuccessDetail) { return nonSuccessDetail; } if (check.details.length > 0) { return check.details[0]; } } return null; } export function deriveEffectiveProvisioningPrepareState(params: { state: ProvisioningPrepareState; message: string | null; warnings: string[]; checks: ProvisioningProviderCheck[]; }): { state: ProvisioningPrepareState; message: string | null } { if (params.state !== 'loading') { return { state: params.state, message: params.message, }; } if (params.checks.length === 0) { return { state: params.state, message: params.message, }; } const hasPendingChecks = params.checks.some( (check) => check.status === 'pending' || check.status === 'checking' ); if (hasPendingChecks) { if (hasCompatibilityPendingDetails(params.checks)) { return { state: params.state, message: 'Deep verification is still running. OpenCode free models may take around 20 seconds.', }; } return { state: params.state, message: params.message, }; } if (params.checks.some((check) => check.status === 'failed')) { return { state: 'failed', message: getPrimaryProvisioningFailureDetail(params.checks) ?? params.message ?? 'Some selected providers need attention.', }; } const hasNotes = params.warnings.length > 0 || params.checks.some((check) => check.status === 'notes'); return { state: 'ready', message: hasNotes ? 'Selected providers are ready with notes.' : 'Selected providers are ready.', }; } export function shouldHideProvisioningProviderStatusList( checks: ProvisioningProviderCheck[], message: string | null | undefined ): boolean { const normalizedMessage = (message ?? '').trim().toLowerCase(); if (!normalizedMessage || checks.length === 0) { return false; } return checks.every((check) => { if (check.status !== 'failed') { return false; } const summary = getDisplayStatusText(check).toLowerCase(); const visibleDetails = check.details.filter( (detail) => detail.trim().toLowerCase() !== normalizedMessage ); return summary === 'working directory missing' && visibleDetails.length === 0; }); } function getStatusColor(status: ProvisioningProviderCheckStatus): string { switch (status) { case 'ready': return 'text-emerald-400'; case 'notes': return 'text-sky-300'; case 'failed': return 'text-red-300'; case 'checking': return 'text-[var(--color-text-secondary)]'; case 'pending': default: return 'text-[var(--color-text-muted)]'; } } const StatusIcon = ({ status }: { status: ProvisioningProviderCheckStatus }): React.JSX.Element => { if (status === 'checking') { return ; } if (status === 'ready') { return ; } if (status === 'notes' || status === 'failed') { return ; } return ; }; export const ProvisioningProviderStatusList = ({ checks, className = '', suppressDetailsMatching, }: { checks: ProvisioningProviderCheck[]; className?: string; suppressDetailsMatching?: string | null; }): React.JSX.Element | null => { if (checks.length === 0) { return null; } return (
{checks.map((check) => { const visibleDetails = check.details.filter( (detail) => detail.trim() !== (suppressDetailsMatching ?? '').trim() ); return (
{getProvisioningProviderLabel(check.providerId)} {check.backendSummary ? ` (${check.backendSummary})` : ''}:{' '} {getDisplayStatusText(check)}
{visibleDetails.length > 0 ? (
{visibleDetails.map((detail) => (

{detail}

))}
) : null}
); })}
); }; export function getProvisioningFailureHint( message: string | null | undefined, checks: ProvisioningProviderCheck[] ): string { const combined = [message ?? '', ...checks.flatMap((check) => check.details)] .join('\n') .toLowerCase(); if (combined.includes('working directory does not exist:')) { return 'Choose an existing working directory, then reopen this dialog.'; } if (combined.includes('not authenticated') || combined.includes('not logged in')) { return 'Authenticate the required provider in Claude CLI, then reopen this dialog.'; } if (combined.includes('provider is not configured for runtime use')) { return 'Configure the selected provider runtime, then reopen this dialog.'; } if ( combined.includes('spawn ') || combined.includes(' enoent') || combined.includes('eacces') || combined.includes('enoexec') || combined.includes('bad cpu type in executable') || combined.includes('image not found') ) { return 'Make sure the local Claude CLI binary exists and can be started, then reopen this dialog.'; } return 'Resolve the issue above, then reopen this dialog.'; }