import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { reconcileAnthropicRuntimeSelections, resolveAnthropicFastMode, resolveAnthropicRuntimeSelection, } from '@features/anthropic-runtime-profile/renderer'; import { mergeCodexCliStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; import { buildCodexFastModeArgs, reconcileCodexRuntimeSelections, resolveCodexFastMode, resolveCodexRuntimeSelection, } from '@features/codex-runtime-profile/renderer'; import { api } from '@renderer/api'; import { buildMemberDraftColorMap, buildMemberDraftSuggestions, buildMembersFromDrafts, clearMemberModelOverrides, createMemberDraft, normalizeLeadProviderForMode, normalizeMemberDraftForProviderMode, normalizeProviderForMode, validateMemberNameInline, } from '@renderer/components/team/members/MembersEditorSection'; import { TeamRosterEditorSection } from '@renderer/components/team/members/TeamRosterEditorSection'; import { AutoResizeTextarea } from '@renderer/components/ui/auto-resize-textarea'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useCreateTeamDraft } from '@renderer/hooks/useCreateTeamDraft'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions'; import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; import { applyStoredCreateTeamMemberRuntimePreferences, getStoredCreateTeamEffort, getStoredCreateTeamFastMode as getStoredTeamFastMode, getStoredCreateTeamLimitContext, getStoredCreateTeamMemberRuntimePreferences, getStoredCreateTeamModel as getStoredTeamModel, getStoredCreateTeamProvider as getStoredTeamProvider, getStoredCreateTeamSkipPermissions, migrateLegacyCreateTeamPreferences, setStoredCreateTeamEffort, setStoredCreateTeamFastMode, setStoredCreateTeamLimitContext, setStoredCreateTeamMemberRuntimePreferences, setStoredCreateTeamModel, setStoredCreateTeamProvider, setStoredCreateTeamSkipPermissions, } from '@renderer/services/createTeamPreferences'; import { useStore } from '@renderer/store'; import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; import { isGeminiUiFrozen } from '@renderer/utils/geminiUiFreeze'; import { normalizePath } from '@renderer/utils/pathNormalize'; import { resolveUiOwnedProviderBackendId } from '@renderer/utils/providerBackendIdentity'; import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus'; import { getTeamModelSelectionError, normalizeExplicitTeamModelForUi, } from '@renderer/utils/teamModelAvailability'; import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog'; import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath'; import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection'; import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors'; import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react'; import { AdvancedCliSection } from './AdvancedCliSection'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; import { CodexFastModeSelector } from './CodexFastModeSelector'; import { clearInheritedMemberModelsUnavailableForProvider, resolveProviderScopedMemberModel, } from './memberModelScope'; import { OptionalSettingsSection } from './OptionalSettingsSection'; import { ProjectPathSelector } from './ProjectPathSelector'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { buildReusableProviderPrepareModelResults, getProviderPrepareCachedSnapshot, type ProviderPrepareDiagnosticsModelResult, runProviderPrepareDiagnostics, } from './providerPrepareDiagnostics'; import { buildProviderPrepareMembersSignature, buildProviderPrepareRequestSignature, buildProviderPrepareRuntimeStatusSignature, } from './providerPrepareRequestSignature'; import { getShortLivedProviderPrepareModelResults, storeShortLivedProviderPrepareModelResults, } from './providerPrepareShortLivedCache'; import { getProvisioningModelIssue } from './provisioningModelIssues'; import { deriveEffectiveProvisioningPrepareState, failIncompleteProviderChecks, getPrimaryProvisioningFailureDetail, getProvisioningFailureHint, getProvisioningProviderBackendSummary, type ProvisioningProviderCheck, ProvisioningProviderStatusList, shouldHideProvisioningProviderStatusList, updateProviderCheck, } from './ProvisioningProviderStatusList'; import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox'; import { computeEffectiveTeamModel } from './TeamModelSelector'; import { getNextSuggestedTeamName } from './teamNameSets'; import type { MemberDraft } from '@renderer/components/team/members/MembersEditorSection'; const TEAM_COLOR_NAMES = [ 'blue', 'green', 'red', 'yellow', 'purple', 'cyan', 'orange', 'pink', ] as const; const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate'; import type { EffortLevel, Project, TeamCreateRequest, TeamFastMode, TeamProviderId, TeamProvisioningMemberInput, } from '@shared/types'; function getProviderLabel(providerId: TeamProviderId): string { return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic'; } function alignProvisioningChecks( existingChecks: ProvisioningProviderCheck[], providerIds: TeamProviderId[] ): ProvisioningProviderCheck[] { const existingByProviderId = new Map( existingChecks.map((check) => [check.providerId, check] as const) ); return providerIds.map( (providerId) => existingByProviderId.get(providerId) ?? { providerId, status: 'pending', backendSummary: null, details: [], } ); } export interface TeamCopyData { teamName: string; description?: string; color?: string; members: TeamProvisioningMemberInput[]; } export interface ActiveTeamRef { teamName: string; displayName: string; projectPath: string; } interface CreateTeamDialogProps { open: boolean; canCreate: boolean; provisioningErrorsByTeam: Record; clearProvisioningError?: (teamName?: string) => void; existingTeamNames: string[]; /** Team names currently in active provisioning (launching) — used to prevent name conflicts. */ provisioningTeamNames?: string[]; activeTeams?: ActiveTeamRef[]; initialData?: TeamCopyData; defaultProjectPath?: string | null; onClose: () => void; onCreate: (request: TeamCreateRequest) => Promise; onOpenTeam: (teamName: string, projectPath?: string) => void; } interface ValidationResult { valid: boolean; errors?: { teamName?: string; members?: string; cwd?: string; }; } import { CUSTOM_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles'; const DEFAULT_MEMBERS: { name: string; roleSelection: string; workflow?: string }[] = [ { name: 'alice', roleSelection: 'reviewer', workflow: 'Review every completed task in the project. Read the code changes, check for correctness, style, and potential issues. Approve the task or request changes with clear feedback.', }, { name: 'tom', roleSelection: 'developer', }, { name: 'bob', roleSelection: 'developer' }, { name: 'jack', roleSelection: 'developer' }, ]; /** Mirrors Claude CLI's `zuA()` sanitization: non-alphanumeric → `-`, then lowercase. */ function sanitizeTeamName(name: string): string { let result = name .replace(/[^a-zA-Z0-9]/g, '-') .replace(/-{2,}/g, '-') .toLowerCase(); // Trim leading/trailing dashes without backtracking-vulnerable regex while (result.startsWith('-')) result = result.slice(1); while (result.endsWith('-')) result = result.slice(0, -1); return result; } function validateTeamNameInline(name: string): string | null { const trimmed = name.trim(); if (!trimmed) return null; const sanitized = sanitizeTeamName(trimmed); if (!sanitized) { return 'Name must contain at least one letter or digit'; } if (sanitized.length > 128) { return 'Name is too long (max 128 chars)'; } return null; } function buildDefaultTeamDescription(teamName: string): string { const trimmedName = teamName.trim(); return trimmedName.length > 0 ? `${trimmedName} team for provisioning flow` : 'Team for provisioning flow'; } function validateRequest( request: TeamCreateRequest, options?: { requireCwd?: boolean } ): ValidationResult { const requireCwd = options?.requireCwd ?? true; const sanitized = sanitizeTeamName(request.teamName); if (!sanitized) { return { valid: false, errors: { teamName: 'Name must contain at least one letter or digit', }, }; } if (sanitized.length > 128) { return { valid: false, errors: { teamName: 'Name is too long (max 128 chars)', }, }; } if (requireCwd && !request.cwd.trim()) { return { valid: false, errors: { cwd: 'Select working directory (cwd)', }, }; } if (request.members.some((member) => !member.name.trim())) { return { valid: false, errors: { members: 'Member name cannot be empty', }, }; } if (request.members.some((member) => validateMemberNameInline(member.name.trim()) !== null)) { return { valid: false, errors: { members: 'Member name must start with alphanumeric, use only [a-zA-Z0-9._-], max 128 chars', }, }; } const uniqueNames = new Set(request.members.map((member) => member.name.trim().toLowerCase())); if (uniqueNames.size !== request.members.length) { return { valid: false, errors: { members: 'Member names must be unique', }, }; } return { valid: true }; } export const CreateTeamDialog = ({ open, canCreate, provisioningErrorsByTeam, clearProvisioningError, existingTeamNames, provisioningTeamNames = [], activeTeams, initialData, defaultProjectPath, onClose, onCreate, onOpenTeam, }: CreateTeamDialogProps): React.JSX.Element => { const { isLight } = useTheme(); const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const anthropicProviderFastModeDefault = useStore( (s) => s.appConfig?.providerConnections?.anthropic.fastModeDefault ?? false ); const cliStatus = useStore((s) => s.cliStatus); const cliStatusLoading = useStore((s) => s.cliStatusLoading); const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus); const fetchCliStatus = useStore((s) => s.fetchCliStatus); const loadingCliStatus = useMemo( () => !cliStatus && cliStatusLoading && multimodelEnabled ? createLoadingMultimodelCliStatus() : cliStatus, [cliStatus, cliStatusLoading, multimodelEnabled] ); const codexAccount = useCodexAccountSnapshot({ enabled: multimodelEnabled && loadingCliStatus?.flavor === 'agent_teams_orchestrator' && Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')), }); const effectiveCliStatus = useMemo( () => mergeCodexCliStatusWithSnapshot(loadingCliStatus, codexAccount.snapshot), [loadingCliStatus, codexAccount.snapshot] ); // ── Persisted draft state (survives tab navigation) ────────────────── const { teamName, setTeamName, members, setMembers, syncModelsWithLead, setSyncModelsWithLead, teammateWorktreeDefault, setTeammateWorktreeDefault, cwdMode, setCwdMode, selectedProjectPath, setSelectedProjectPath, customCwd, setCustomCwd, soloTeam, setSoloTeam, launchTeam, setLaunchTeam, teamColor, setTeamColor, isLoaded: draftLoaded, clearDraft, } = useCreateTeamDraft(); const descriptionDraft = useDraftPersistence({ key: 'createTeam:description' }); const promptDraft = useDraftPersistence({ key: 'createTeam:prompt' }); const promptChipDraft = useChipDraftPersistence('createTeam:prompt:chips'); // ── Transient UI state (NOT persisted) ─────────────────────────────── const [projects, setProjects] = useState([]); const [projectsLoading, setProjectsLoading] = useState(false); const [projectsError, setProjectsError] = useState(null); const [localError, setLocalError] = useState(null); const [prepareState, setPrepareState] = useState<'idle' | 'loading' | 'ready' | 'failed'>('idle'); const [prepareMessage, setPrepareMessage] = useState(null); const [prepareWarnings, setPrepareWarnings] = useState([]); const [prepareChecks, setPrepareChecks] = useState([]); const prepareRequestSeqRef = useRef(0); const lastAutoDescriptionRef = useRef(null); const [fieldErrors, setFieldErrors] = useState<{ teamName?: string; members?: string; cwd?: string; }>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [conflictDismissed, setConflictDismissed] = useState(false); const [selectedProviderId, setSelectedProviderIdRaw] = useState(() => normalizeLeadProviderForMode(getStoredTeamProvider(), multimodelEnabled) ); const [selectedModel, setSelectedModelRaw] = useState(() => getStoredTeamModel(normalizeLeadProviderForMode(getStoredTeamProvider(), multimodelEnabled)) ); const [limitContext, setLimitContextRaw] = useState(getStoredCreateTeamLimitContext); const [skipPermissions, setSkipPermissionsRaw] = useState(getStoredCreateTeamSkipPermissions); const [selectedEffort, setSelectedEffortRaw] = useState(getStoredCreateTeamEffort); const [selectedFastMode, setSelectedFastModeRaw] = useState(getStoredTeamFastMode); const [anthropicRuntimeNotice, setAnthropicRuntimeNotice] = useState(null); // Advanced CLI section state (use teamName-derived key for localStorage) const advancedKey = sanitizeTeamName(teamName.trim()) || '_new_'; const [worktreeEnabled, setWorktreeEnabledRaw] = useState(false); const [worktreeName, setWorktreeNameRaw] = useState(''); const [customArgs, setCustomArgsRaw] = useState(''); useEffect(() => { migrateLegacyCreateTeamPreferences(); }, []); // Re-read localStorage when advancedKey changes useEffect(() => { const storedEnabled = localStorage.getItem(`team:lastWorktreeEnabled:${advancedKey}`) === 'true'; const storedName = localStorage.getItem(`team:lastWorktreeName:${advancedKey}`) ?? ''; setWorktreeEnabledRaw(storedEnabled && Boolean(storedName)); setWorktreeNameRaw(storedName); setCustomArgsRaw(localStorage.getItem(`team:lastCustomArgs:${advancedKey}`) ?? ''); }, [advancedKey]); const setSelectedModel = (value: string): void => { const normalizedValue = normalizeExplicitTeamModelForUi(selectedProviderId, value); setSelectedModelRaw(normalizedValue); setStoredCreateTeamModel(selectedProviderId, normalizedValue); }; const setSelectedProviderId = (value: TeamProviderId): void => { const normalizedValue = normalizeLeadProviderForMode(value, multimodelEnabled); setSelectedProviderIdRaw(normalizedValue); setStoredCreateTeamProvider(normalizedValue); if (normalizedValue !== 'anthropic') { setLimitContextRaw(false); setStoredCreateTeamLimitContext(false); } setSelectedModelRaw(getStoredTeamModel(normalizedValue)); }; const setLimitContext = (value: boolean): void => { setLimitContextRaw(value); setStoredCreateTeamLimitContext(value); }; const setSkipPermissions = (value: boolean): void => { setSkipPermissionsRaw(value); setStoredCreateTeamSkipPermissions(value); }; const setSelectedEffort = (value: string): void => { setSelectedEffortRaw(value); setStoredCreateTeamEffort(value); }; const setSelectedFastMode = (value: TeamFastMode): void => { setSelectedFastModeRaw(value); setStoredCreateTeamFastMode(value); }; const setWorktreeEnabled = (value: boolean): void => { setWorktreeEnabledRaw(value); localStorage.setItem(`team:lastWorktreeEnabled:${advancedKey}`, String(value)); if (!value) { setWorktreeNameRaw(''); localStorage.setItem(`team:lastWorktreeName:${advancedKey}`, ''); } }; const setWorktreeName = (value: string): void => { setWorktreeNameRaw(value); localStorage.setItem(`team:lastWorktreeName:${advancedKey}`, value); }; const setCustomArgs = (value: string): void => { setCustomArgsRaw(value); localStorage.setItem(`team:lastCustomArgs:${advancedKey}`, value); }; const resetUIState = (): void => { setLocalError(null); setFieldErrors({}); setIsSubmitting(false); setPrepareState('idle'); setPrepareMessage(null); setPrepareWarnings([]); setPrepareChecks([]); setConflictDismissed(false); }; const resetFormState = (): void => { clearDraft(); lastAutoDescriptionRef.current = null; descriptionDraft.clearDraft(); promptDraft.clearDraft(); promptChipDraft.clearChipDraft(); resetUIState(); }; const persistCurrentMemberRuntimePreferences = useCallback( (nextMembers: readonly MemberDraft[] = members): void => { setStoredCreateTeamMemberRuntimePreferences(nextMembers); }, [members] ); const selectedProjectCwd = isEphemeralProjectPath(selectedProjectPath) ? '' : selectedProjectPath.trim(); const effectiveCwd = cwdMode === 'project' ? selectedProjectCwd : customCwd.trim(); const dialogTeamNameKey = sanitizeTeamName(teamName.trim()); /** All taken names: existing teams + teams currently being provisioned. */ const allTakenTeamNames = useMemo( () => [...new Set([...existingTeamNames, ...provisioningTeamNames])], [existingTeamNames, provisioningTeamNames] ); const suggestedTeamName = getNextSuggestedTeamName(allTakenTeamNames); // Clear stale provisioning error when dialog opens useEffect(() => { if (open && dialogTeamNameKey) { clearProvisioningError?.(dialogTeamNameKey); } }, [open, clearProvisioningError, dialogTeamNameKey]); const effectiveMemberDrafts = useMemo( () => (syncModelsWithLead ? members.map(clearMemberModelOverrides) : members), [members, syncModelsWithLead] ); const selectedMemberProviders = useMemo(() => { if (!multimodelEnabled) { return ['anthropic']; } if (soloTeam || syncModelsWithLead) { return [selectedProviderId]; } return Array.from( new Set([ selectedProviderId, ...members.flatMap((member) => !member.removedAt && isTeamProviderId(member.providerId) ? [member.providerId] : [] ), ]) ); }, [members, multimodelEnabled, selectedProviderId, soloTeam, syncModelsWithLead]); const runtimeBackendSummaryByProvider = useMemo(() => { const entries: (readonly [TeamProviderId, string | null])[] = ( effectiveCliStatus?.providers ?? [] ).map( (provider) => [ provider.providerId as TeamProviderId, getProvisioningProviderBackendSummary(provider), ] as const ); return new Map(entries); }, [effectiveCliStatus?.providers]); const runtimeProviderStatusById = useMemo( () => new Map( (effectiveCliStatus?.providers ?? []).map( (provider) => [provider.providerId, provider] as const ) ), [effectiveCliStatus?.providers] ); const runtimeBackendSummaryByProviderRef = useRef(runtimeBackendSummaryByProvider); const prepareChecksRef = useRef([]); const prepareModelResultsCacheRef = useRef( new Map>() ); const lastPrepareRequestSignatureRef = useRef(null); useEffect(() => { runtimeBackendSummaryByProviderRef.current = runtimeBackendSummaryByProvider; }, [runtimeBackendSummaryByProvider]); useEffect(() => { const sanitized = clearInheritedMemberModelsUnavailableForProvider({ members, selectedProviderId, runtimeProviderStatusById, }); if (sanitized.changed) { setMembers(sanitized.members); } }, [members, runtimeProviderStatusById, selectedProviderId, setMembers]); useEffect(() => { prepareChecksRef.current = prepareChecks; }, [prepareChecks]); useEffect(() => { if (!open) { lastPrepareRequestSignatureRef.current = null; } }, [open]); const prepareRuntimeStatusSignature = useMemo( () => buildProviderPrepareRuntimeStatusSignature( selectedMemberProviders, runtimeProviderStatusById ), [runtimeProviderStatusById, selectedMemberProviders] ); const prepareMembersSignature = useMemo( () => buildProviderPrepareMembersSignature(effectiveMemberDrafts), [effectiveMemberDrafts] ); const prepareRequestSignature = useMemo( () => buildProviderPrepareRequestSignature({ cwd: effectiveCwd, selectedProviderId, selectedModel, selectedMemberProviders, limitContext, runtimeStatusSignature: prepareRuntimeStatusSignature, membersSignature: prepareMembersSignature, }), [ effectiveCwd, limitContext, prepareMembersSignature, prepareRuntimeStatusSignature, selectedMemberProviders, selectedModel, selectedProviderId, ] ); useEffect(() => { if (multimodelEnabled) { return; } if (selectedProviderId !== 'anthropic') { setSelectedProviderIdRaw('anthropic'); setSelectedModelRaw(getStoredTeamModel('anthropic')); } const nextMembers = members.map((member) => normalizeMemberDraftForProviderMode(member, false)); const changed = nextMembers.some((member, index) => member !== members[index]); if (changed) { setMembers(nextMembers); } }, [members, multimodelEnabled, selectedProviderId, setMembers]); useEffect(() => { if (!open || cliStatus || cliStatusLoading) { return; } void refreshCliStatusForCurrentMode({ multimodelEnabled, bootstrapCliStatus, fetchCliStatus, }); }, [bootstrapCliStatus, cliStatus, cliStatusLoading, fetchCliStatus, multimodelEnabled, open]); useEffect(() => { if (!open || !canCreate || !launchTeam) { prepareRequestSeqRef.current += 1; lastPrepareRequestSignatureRef.current = null; return; } if (typeof api.teams.prepareProvisioning !== 'function') { prepareRequestSeqRef.current += 1; lastPrepareRequestSignatureRef.current = null; setPrepareState('failed'); setPrepareWarnings([]); setPrepareChecks([]); setPrepareMessage( 'Current preload version does not support team:prepareProvisioning. Restart the dev app.' ); return; } if (!effectiveCwd) { prepareRequestSeqRef.current += 1; lastPrepareRequestSignatureRef.current = null; setPrepareState('idle'); setPrepareWarnings([]); setPrepareChecks([]); setPrepareMessage('Select a working directory to validate the launch environment.'); return; } if (lastPrepareRequestSignatureRef.current === prepareRequestSignature) { return; } lastPrepareRequestSignatureRef.current = prepareRequestSignature; const requestSeq = ++prepareRequestSeqRef.current; const initialChecks = alignProvisioningChecks( prepareChecksRef.current, selectedMemberProviders ); setPrepareState('loading'); setPrepareMessage('Checking selected providers in parallel...'); setPrepareWarnings([]); setPrepareChecks(initialChecks); void (async () => { await Promise.resolve(); let checks = initialChecks; const providerPlans = selectedMemberProviders.map((providerId) => { const selectedModelChecks = (() => { const next = new Set(); let hasDefaultSelection = false; const supportsProviderDefaultCheck = providerId === 'codex' || providerId === 'gemini' || (providerId === 'anthropic' && selectedProviderId === 'anthropic'); const leadModel = computeEffectiveTeamModel( selectedModel, limitContext, selectedProviderId ); if (selectedProviderId === providerId && selectedModel.trim()) { if (leadModel?.trim()) { next.add(leadModel.trim()); } } else if (selectedProviderId === providerId && supportsProviderDefaultCheck) { hasDefaultSelection = true; } for (const member of effectiveMemberDrafts) { if (member.removedAt) { continue; } const scopedModel = resolveProviderScopedMemberModel({ memberProviderId: member.providerId, memberModel: member.model, selectedProviderId, runtimeProviderStatusById, }); if (scopedModel.providerId !== providerId) { continue; } if (scopedModel.model) { next.add(scopedModel.model); } else if (supportsProviderDefaultCheck) { hasDefaultSelection = true; } } if (supportsProviderDefaultCheck && hasDefaultSelection) { next.add(DEFAULT_PROVIDER_MODEL_SELECTION); } return Array.from(next); })(); const backendSummary = runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null; const cacheKey = buildProviderPrepareModelCacheKey({ cwd: effectiveCwd, providerId, backendSummary, limitContext, runtimeStatusSignature: prepareRuntimeStatusSignature, }); const cachedModelResultsById = { ...getShortLivedProviderPrepareModelResults({ providerId, cacheKey, }), ...(prepareModelResultsCacheRef.current.get(cacheKey) ?? {}), }; const cachedSnapshot = getProviderPrepareCachedSnapshot({ providerId, selectedModelIds: selectedModelChecks, cachedModelResultsById, }); return { providerId, selectedModelChecks, backendSummary, cacheKey, cachedModelResultsById, cachedSnapshot, }; }); try { for (const plan of providerPlans) { checks = updateProviderCheck(checks, plan.providerId, { status: plan.selectedModelChecks.length > 0 ? plan.cachedSnapshot.status : 'checking', backendSummary: plan.backendSummary, details: plan.cachedSnapshot.details, }); } if (prepareRequestSeqRef.current === requestSeq) { setPrepareChecks(checks); } const providerResults = await Promise.all( providerPlans.map(async (plan) => { const prepResult = await runProviderPrepareDiagnostics({ cwd: effectiveCwd, providerId: plan.providerId, selectedModelIds: plan.selectedModelChecks, prepareProvisioning: api.teams.prepareProvisioning, limitContext, cachedModelResultsById: plan.cachedModelResultsById, onModelProgress: ({ status, details }) => { checks = updateProviderCheck(checks, plan.providerId, { status, backendSummary: plan.backendSummary, details, }); if (prepareRequestSeqRef.current === requestSeq) { setPrepareChecks(checks); } }, }); return { ...plan, prepResult }; }) ); let anyFailure = false; let anyNotes = false; const collectedWarnings: string[] = []; for (const plan of providerResults) { if (plan.prepResult.warnings.length > 0) { anyNotes = true; collectedWarnings.push( ...plan.prepResult.warnings.map( (warning) => `${getProviderLabel(plan.providerId)}: ${warning}` ) ); } if (plan.prepResult.status === 'failed') { anyFailure = true; } else if (plan.prepResult.status === 'notes') { anyNotes = true; } if (prepareRequestSeqRef.current === requestSeq) { const reusableModelResults = buildReusableProviderPrepareModelResults( plan.prepResult.modelResultsById ); prepareModelResultsCacheRef.current.set(plan.cacheKey, reusableModelResults); storeShortLivedProviderPrepareModelResults({ providerId: plan.providerId, cacheKey: plan.cacheKey, modelResultsById: plan.prepResult.modelResultsById, }); } checks = updateProviderCheck(checks, plan.providerId, { status: plan.prepResult.status, backendSummary: plan.backendSummary, details: plan.prepResult.details, }); } if (prepareRequestSeqRef.current === requestSeq) { setPrepareChecks(checks); } if (prepareRequestSeqRef.current !== requestSeq) return; const failureMessage = getPrimaryProvisioningFailureDetail(checks) ?? 'Some selected providers need attention.'; setPrepareState(anyFailure ? 'failed' : 'ready'); setPrepareMessage( anyFailure ? failureMessage : anyNotes ? 'Selected providers are ready with notes.' : 'Selected providers are ready.' ); setPrepareWarnings(collectedWarnings); } catch (error) { if (prepareRequestSeqRef.current !== requestSeq) return; const failureMessage = error instanceof Error ? error.message : 'Failed to warm up Claude CLI environment'; setPrepareState('failed'); setPrepareWarnings([]); setPrepareChecks(failIncompleteProviderChecks(checks, failureMessage)); setPrepareMessage(failureMessage); } })(); }, [ open, canCreate, launchTeam, effectiveCwd, effectiveMemberDrafts, limitContext, prepareRequestSignature, runtimeProviderStatusById, selectedModel, selectedProviderId, selectedMemberProviders, ]); useEffect(() => { if (!open) { return; } setProjectsLoading(true); setProjectsError(null); let cancelled = false; void (async () => { try { const nextProjects = (await api.getProjects()).filter( (project) => !isEphemeralProjectPath(project.path) ); if (cancelled) { return; } // If defaultProjectPath is set but not in the fetched list (e.g. new project // without Claude sessions), add it as a synthetic entry so the Combobox can // display and select it. const normalizedDefaultProjectPath = defaultProjectPath ? normalizePath(defaultProjectPath) : null; if ( defaultProjectPath && normalizedDefaultProjectPath && !isEphemeralProjectPath(defaultProjectPath) && !nextProjects.some((p) => normalizePath(p.path) === normalizedDefaultProjectPath) ) { const folderName = defaultProjectPath.split(/[/\\]/).filter(Boolean).pop() ?? defaultProjectPath; nextProjects.unshift({ id: defaultProjectPath.replace(/[/\\]/g, '-'), path: defaultProjectPath, name: folderName, sessions: [], createdAt: Date.now(), }); } setProjects(nextProjects); } catch (error) { if (cancelled) { return; } setProjectsError(error instanceof Error ? error.message : 'Failed to load projects'); setProjects([]); } finally { if (!cancelled) { setProjectsLoading(false); } } })(); return () => { cancelled = true; }; }, [open, defaultProjectPath]); useEffect(() => { if (!open || !draftLoaded) { return; } if (initialData) { const nextSyncModelsWithLead = !initialData.members.some( (member) => member.providerId || member.model || member.effort ); setTeamName(initialData.teamName); descriptionDraft.setValue(initialData.description ?? ''); setTeamColor(initialData.color ?? ''); setMembers( initialData.members.map((m) => { const presetRoles: readonly string[] = PRESET_ROLES; const isPreset = m.role != null && presetRoles.includes(m.role); const isCustom = m.role != null && m.role.length > 0 && !isPreset; return normalizeMemberDraftForProviderMode( createMemberDraft({ name: m.name, roleSelection: isCustom ? CUSTOM_ROLE : (m.role ?? ''), customRole: isCustom ? m.role : '', workflow: m.workflow, isolation: m.isolation === 'worktree' ? 'worktree' : undefined, providerId: normalizeOptionalTeamProviderId(m.providerId), model: m.model ?? '', effort: m.effort, }), multimodelEnabled ); }) ); setTeammateWorktreeDefault( initialData.members.length > 0 && initialData.members.every((member) => member.isolation === 'worktree') ); setSyncModelsWithLead(nextSyncModelsWithLead, { persistStoredPreference: false }); return; } if (members.length > 0) { return; } const nextDefaultMembers = DEFAULT_MEMBERS.map((member) => createMemberDraft({ name: member.name, roleSelection: member.roleSelection, workflow: member.workflow, }) ); setMembers( syncModelsWithLead ? nextDefaultMembers : applyStoredCreateTeamMemberRuntimePreferences(nextDefaultMembers) ); // eslint-disable-next-line react-hooks/exhaustive-deps -- initialData is checked once on open/draftLoaded }, [open, draftLoaded]); useEffect(() => { if (!open || !draftLoaded || initialData || syncModelsWithLead || members.length === 0) { return; } persistCurrentMemberRuntimePreferences(members); }, [ draftLoaded, initialData, members, open, persistCurrentMemberRuntimePreferences, syncModelsWithLead, ]); useEffect(() => { if (!open || initialData || !draftLoaded) { return; } if (teamName.trim().length === 0) { setTeamName(suggestedTeamName); } }, [initialData, open, suggestedTeamName, draftLoaded]); // eslint-disable-line react-hooks/exhaustive-deps -- teamName read once useEffect(() => { if (!open || initialData) { return; } const resolvedTeamName = teamName.trim() || suggestedTeamName; const nextAutoDescription = buildDefaultTeamDescription(resolvedTeamName); const currentDescription = descriptionDraft.value.trim(); const previousAutoDescription = lastAutoDescriptionRef.current?.trim() ?? ''; const shouldSyncDescription = currentDescription.length === 0 || currentDescription === previousAutoDescription; if (shouldSyncDescription && descriptionDraft.value !== nextAutoDescription) { lastAutoDescriptionRef.current = nextAutoDescription; descriptionDraft.setValue(nextAutoDescription); return; } if (currentDescription === nextAutoDescription) { lastAutoDescriptionRef.current = nextAutoDescription; } }, [descriptionDraft, initialData, open, suggestedTeamName, teamName]); // Pre-select defaultProjectPath when projects loaded (only while dialog is open) useEffect(() => { if (!open) return; if (cwdMode !== 'project') { return; } if (selectedProjectPath) { return; } const selectableProjects = projects.filter((project) => !isEphemeralProjectPath(project.path)); if (selectableProjects.length === 0) { return; } if (defaultProjectPath && !isEphemeralProjectPath(defaultProjectPath)) { const normalizedDefaultProjectPath = normalizePath(defaultProjectPath); const match = selectableProjects.find( (p) => normalizePath(p.path) === normalizedDefaultProjectPath ); if (match) { setSelectedProjectPath(match.path); return; } } setSelectedProjectPath(selectableProjects[0].path); }, [open, cwdMode, projects, selectedProjectPath, defaultProjectPath]); useEffect(() => { if (!open || cwdMode !== 'project' || !selectedProjectPath) { return; } if (!isEphemeralProjectPath(selectedProjectPath)) { return; } setSelectedProjectPath(''); }, [open, cwdMode, selectedProjectPath, setSelectedProjectPath]); useFileListCacheWarmer(effectiveCwd || null); const { suggestions: taskSuggestions } = useTaskSuggestions(null); const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null); const description = descriptionDraft.value; const prompt = promptDraft.value; const memberColorMap = useMemo(() => buildMemberDraftColorMap(members), [members]); const mentionSuggestions = useMemo( () => soloTeam ? [ { id: 'team-lead', name: 'team-lead', subtitle: 'Team Lead', color: resolveTeamLeadColorName(), }, ] : buildMemberDraftSuggestions(members, memberColorMap), [memberColorMap, members, soloTeam] ); const effectiveModel = useMemo( () => computeEffectiveTeamModel( selectedModel, limitContext, selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ), [limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId] ); const anthropicRuntimeSelection = useMemo( () => selectedProviderId === 'anthropic' ? resolveAnthropicRuntimeSelection({ source: { modelCatalog: runtimeProviderStatusById.get('anthropic')?.modelCatalog, runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities, }, selectedModel, limitContext, }) : null, [limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId] ); const anthropicFastModeResolution = useMemo( () => selectedProviderId === 'anthropic' && anthropicRuntimeSelection ? resolveAnthropicFastMode({ selection: anthropicRuntimeSelection, selectedFastMode, providerFastModeDefault: anthropicProviderFastModeDefault, }) : null, [ anthropicProviderFastModeDefault, anthropicRuntimeSelection, selectedFastMode, selectedProviderId, ] ); const codexRuntimeSelection = useMemo( () => selectedProviderId === 'codex' ? resolveCodexRuntimeSelection({ source: { providerStatus: runtimeProviderStatusById.get('codex'), providerBackendId: resolveUiOwnedProviderBackendId( 'codex', runtimeProviderStatusById.get('codex') ), }, selectedModel, }) : null, [runtimeProviderStatusById, selectedModel, selectedProviderId] ); const codexFastModeResolution = useMemo( () => selectedProviderId === 'codex' && codexRuntimeSelection ? resolveCodexFastMode({ selection: codexRuntimeSelection, selectedFastMode, }) : null, [codexRuntimeSelection, selectedFastMode, selectedProviderId] ); useEffect(() => { if (selectedProviderId !== 'anthropic' && selectedProviderId !== 'codex') { setAnthropicRuntimeNotice(null); return; } const reconciliation = selectedProviderId === 'anthropic' ? reconcileAnthropicRuntimeSelections({ selection: anthropicRuntimeSelection ?? resolveAnthropicRuntimeSelection({ source: { modelCatalog: null, runtimeCapabilities: null, }, selectedModel, limitContext, }), selectedEffort, selectedFastMode, providerFastModeDefault: anthropicProviderFastModeDefault, }) : { nextEffort: selectedEffort, effortResetReason: null, ...reconcileCodexRuntimeSelections({ selection: codexRuntimeSelection ?? resolveCodexRuntimeSelection({ source: { providerStatus: runtimeProviderStatusById.get('codex'), providerBackendId: resolveUiOwnedProviderBackendId( 'codex', runtimeProviderStatusById.get('codex') ), }, selectedModel, }), selectedFastMode, }), }; const notices: string[] = []; if (reconciliation.nextEffort !== selectedEffort) { setSelectedEffortRaw(reconciliation.nextEffort); setStoredCreateTeamEffort(reconciliation.nextEffort); if (reconciliation.effortResetReason) { notices.push(reconciliation.effortResetReason); } } if (reconciliation.nextFastMode !== selectedFastMode) { setSelectedFastModeRaw(reconciliation.nextFastMode); setStoredCreateTeamFastMode(reconciliation.nextFastMode); if (reconciliation.fastModeResetReason) { notices.push(reconciliation.fastModeResetReason); } } setAnthropicRuntimeNotice(notices.length > 0 ? notices.join(' ') : null); }, [ anthropicProviderFastModeDefault, anthropicRuntimeSelection, codexRuntimeSelection, limitContext, runtimeProviderStatusById, selectedEffort, selectedFastMode, selectedModel, selectedProviderId, ]); const sanitizedTeamName = sanitizeTeamName(teamName.trim()); const teamNameInlineError = validateTeamNameInline(teamName); const isNameTakenByExistingTeam = existingTeamNames.includes(sanitizedTeamName); const isNameProvisioning = provisioningTeamNames.includes(sanitizedTeamName) && !isNameTakenByExistingTeam; const request = useMemo( () => ({ teamName: sanitizedTeamName, description: description.trim() || undefined, color: teamColor || undefined, members: soloTeam ? [] : buildMembersFromDrafts(effectiveMemberDrafts), cwd: effectiveCwd, prompt: prompt.trim() || undefined, providerId: selectedProviderId, providerBackendId: resolveUiOwnedProviderBackendId( selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ) ?? undefined, model: effectiveModel, effort: (selectedEffort as EffortLevel) || undefined, fastMode: selectedProviderId === 'anthropic' || selectedProviderId === 'codex' ? selectedFastMode : undefined, limitContext, skipPermissions, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, extraCliArgs: customArgs.trim() || undefined, }), [ sanitizedTeamName, description, teamColor, soloTeam, effectiveMemberDrafts, effectiveCwd, prompt, selectedProviderId, runtimeProviderStatusById, effectiveModel, selectedEffort, selectedFastMode, limitContext, skipPermissions, worktreeEnabled, worktreeName, customArgs, ] ); const requestValidation = useMemo( () => validateRequest(request, { requireCwd: launchTeam }), [request, launchTeam] ); const modelValidationError = useMemo(() => { const leadError = getTeamModelSelectionError( selectedProviderId, selectedModel, runtimeProviderStatusById.get(selectedProviderId) ); if (leadError) { return leadError; } for (const member of effectiveMemberDrafts) { if (member.removedAt) { continue; } const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId; const memberError = getTeamModelSelectionError( providerId, member.model, runtimeProviderStatusById.get(providerId) ); if (!memberError) { continue; } const memberName = member.name.trim(); return memberName ? `${memberName}: ${memberError}` : memberError; } return null; }, [effectiveMemberDrafts, runtimeProviderStatusById, selectedModel, selectedProviderId]); const leadModelIssueText = useMemo(() => { const issue = getProvisioningModelIssue( prepareChecks, selectedProviderId, effectiveModel ?? selectedModel ); return issue?.reason ?? issue?.detail ?? null; }, [effectiveModel, prepareChecks, selectedModel, selectedProviderId]); const memberModelIssueById = useMemo(() => { const next: Record = {}; for (const member of effectiveMemberDrafts) { if (member.removedAt) { continue; } if (syncModelsWithLead && leadModelIssueText) { next[member.id] = leadModelIssueText; continue; } const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId; const issue = getProvisioningModelIssue(prepareChecks, providerId, member.model); const issueText = issue?.reason ?? issue?.detail ?? null; if (issueText) { next[member.id] = issueText; } } return next; }, [ effectiveMemberDrafts, leadModelIssueText, prepareChecks, selectedProviderId, syncModelsWithLead, ]); const hasCreateFormErrors = !!teamNameInlineError || isNameTakenByExistingTeam || isNameProvisioning || !requestValidation.valid || !!modelValidationError; const internalArgs = useMemo(() => { const args: string[] = []; args.push('--input-format', 'stream-json', '--output-format', 'stream-json'); args.push('--verbose', '--setting-sources', 'user,project,local'); args.push('--mcp-config', '', '--disallowedTools', APP_TEAM_RUNTIME_DISALLOWED_TOOLS); if (skipPermissions) args.push('--dangerously-skip-permissions'); if (effectiveModel) args.push('--model', effectiveModel); const effectiveEffort = selectedProviderId === 'anthropic' ? selectedEffort || anthropicRuntimeSelection?.defaultEffort || '' : selectedEffort; if (effectiveEffort) args.push('--effort', effectiveEffort); if (selectedProviderId === 'anthropic') { const fastSettings = anthropicFastModeResolution?.resolvedFastMode ? { fastMode: true, fastModePerSessionOptIn: false } : { fastMode: false }; args.push('--settings', JSON.stringify(fastSettings)); } else if (selectedProviderId === 'codex') { args.push(...buildCodexFastModeArgs(codexFastModeResolution?.resolvedFastMode)); } return args; }, [ anthropicFastModeResolution?.resolvedFastMode, anthropicRuntimeSelection?.defaultEffort, codexFastModeResolution?.resolvedFastMode, effectiveModel, selectedEffort, selectedProviderId, skipPermissions, ]); const launchOptionalSummary = useMemo(() => { const summary: string[] = []; if (prompt.trim()) summary.push('Lead prompt'); if (skipPermissions) summary.push('Auto-approve tools'); if (selectedProviderId === 'anthropic' || selectedProviderId === 'codex') { if (selectedFastMode === 'on') summary.push('Fast mode'); else if (selectedFastMode === 'off') summary.push('Fast disabled'); else if (selectedProviderId === 'anthropic' && anthropicProviderFastModeDefault) { summary.push('Fast default'); } } if (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`); if (customArgs.trim()) summary.push('Custom CLI args'); return summary; }, [ anthropicProviderFastModeDefault, customArgs, prompt, selectedFastMode, selectedProviderId, skipPermissions, worktreeEnabled, worktreeName, ]); const teamDetailsSummary = useMemo(() => { const summary: string[] = []; if (description.trim()) summary.push('Description'); if (teamColor) summary.push(`Color: ${teamColor}`); return summary; }, [description, teamColor]); const handleSyncModelsWithLeadChange = useCallback( (checked: boolean): void => { setSyncModelsWithLead(checked); if (checked) { persistCurrentMemberRuntimePreferences(members); setMembers(members.map(clearMemberModelOverrides)); return; } if (getStoredCreateTeamMemberRuntimePreferences().length === 0) { return; } const nextMembers = applyStoredCreateTeamMemberRuntimePreferences(members); const hasRuntimeChanges = nextMembers.some((member, index) => { const previousMember = members[index]; return ( member.providerId !== previousMember?.providerId || member.model !== previousMember?.model || member.effort !== previousMember?.effort ); }); if (hasRuntimeChanges) { setMembers(nextMembers); } }, [members, persistCurrentMemberRuntimePreferences, setMembers, setSyncModelsWithLead] ); const activeError = localError ?? modelValidationError ?? provisioningErrorsByTeam[request.teamName] ?? null; const effectivePrepare = useMemo( () => deriveEffectiveProvisioningPrepareState({ state: prepareState, message: prepareMessage, warnings: prepareWarnings, checks: prepareChecks, }), [prepareChecks, prepareMessage, prepareState, prepareWarnings] ); const canOpenExistingTeam = activeError?.includes('Team already exists') === true && request.teamName.length > 0; const conflictingTeam = useMemo(() => { if (!launchTeam) return null; if (!activeTeams?.length || !effectiveCwd) return null; const norm = normalizePath(effectiveCwd); return activeTeams.find((t) => normalizePath(t.projectPath) === norm) ?? null; }, [activeTeams, effectiveCwd, launchTeam]); // Reset dismiss when conflict target changes useEffect(() => { setConflictDismissed(false); }, [conflictingTeam?.teamName, effectiveCwd]); const handleSubmit = (): void => { if (allTakenTeamNames.includes(sanitizedTeamName)) { const msg = isNameProvisioning ? 'Team is currently launching' : 'Team name already exists'; setFieldErrors({ teamName: msg }); setLocalError(msg); return; } const validation = validateRequest(request, { requireCwd: launchTeam }); if (!validation.valid) { const errors = validation.errors ?? {}; setFieldErrors(errors); const messages = Object.values(errors).filter(Boolean); setLocalError(messages.join(' · ') || 'Check form fields'); return; } if (modelValidationError) { setLocalError(modelValidationError); return; } setFieldErrors({}); setLocalError(null); setIsSubmitting(true); if (!launchTeam) { void (async () => { try { if (!syncModelsWithLead) { persistCurrentMemberRuntimePreferences(members); } await api.teams.createConfig({ teamName: request.teamName, displayName: request.displayName, description: request.description, color: request.color, members: request.members, cwd: effectiveCwd || undefined, providerBackendId: request.providerBackendId, fastMode: request.fastMode, }); onOpenTeam(request.teamName, effectiveCwd || undefined); resetFormState(); onClose(); } catch (error) { setLocalError(error instanceof Error ? error.message : 'Failed to create team config'); } finally { setIsSubmitting(false); } })(); return; } void (async () => { try { if (!syncModelsWithLead) { persistCurrentMemberRuntimePreferences(members); } await onCreate(request); onOpenTeam(request.teamName, effectiveCwd || undefined); resetFormState(); onClose(); } catch { // error is shown via provisioningError prop } finally { setIsSubmitting(false); } })(); }; const handleTeamNameChange = (value: string): void => { setTeamName(value); setFieldErrors((prev) => { if (!prev.teamName) return prev; // eslint-disable-next-line sonarjs/no-unused-vars -- destructured to omit teamName from rest const { teamName: _teamName, ...rest } = prev; const remaining = Object.values(rest).filter(Boolean); if (remaining.length === 0) { setLocalError(null); } else { setLocalError(remaining.join(' · ')); } return rest; }); }; return ( { if (!nextOpen) { resetUIState(); onClose(); } }} > {initialData ? 'Copy Team' : 'Create Team'} {initialData ? 'Create a new team based on an existing one.' : 'Team provisioning via local Claude CLI.'} {conflictingTeam && !conflictDismissed ? (

Another team “{conflictingTeam.displayName}” is already running for this working directory

Running two teams in the same directory is risky — they may conflict editing the same files. Consider using a different directory or a git worktree for isolation.

Working directory: {effectiveCwd}

) : null} {!canCreate ? (

Available only in local Electron mode.

) : null}
handleTeamNameChange(event.target.value)} placeholder={suggestedTeamName} /> {isNameTakenByExistingTeam ? (

Team name already exists

) : teamNameInlineError ? (

{teamNameInlineError}

) : isNameProvisioning ? (

A team with this name is currently launching

) : fieldErrors.teamName ? (

{fieldErrors.teamName}

) : null} {sanitizedTeamName && sanitizedTeamName !== teamName.trim() ? (

On disk: {sanitizedTeamName}

) : null}
setSoloTeam(checked === true)} />
} headerBottom={ soloTeam ? (

Only the team lead (main process) will be started — no teammates will be spawned. Works like a regular Claude session but with access to the task board for planning. Saves tokens by avoiding teammate coordination overhead. You can add members later from the team settings.

) : null } />
setLaunchTeam(checked === true)} />

Start the team immediately via local Claude CLI.

{launchTeam ? (
{selectedProviderId === 'anthropic' ? (
{anthropicRuntimeNotice ? (

{anthropicRuntimeNotice}

) : null}
) : null} {selectedProviderId === 'codex' ? (
{anthropicRuntimeNotice ? (

{anthropicRuntimeNotice}

) : null}
) : null}
Saved ) : null } />
) : null}
descriptionDraft.setValue(event.target.value)} placeholder="Brief description of the team purpose" /> {descriptionDraft.isSaved ? ( Saved ) : null}
{TEAM_COLOR_NAMES.map((colorName) => { const colorSet = getTeamColorSet(colorName); const isSelected = teamColor === colorName; return ( ); })}
{activeError ? (

{activeError}

) : null}
{canCreate && launchTeam && (effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading') ? ( <>
{effectivePrepare.message ?? (effectivePrepare.state === 'idle' ? 'Warming up CLI environment...' : 'Preparing environment...')}

Pre-flight check to catch errors before launch

) : null} {canCreate && launchTeam && effectivePrepare.state === 'ready' ? (
{prepareChecks.some((check) => check.status === 'notes') || prepareWarnings.length > 0 ? 'CLI environment ready (with notes)' : 'CLI environment ready'}
{effectivePrepare.message ? (

{effectivePrepare.message}

) : null} {prepareWarnings.length > 0 && prepareChecks.length === 0 ? (
{prepareWarnings.map((warning) => (

{warning}

))}
) : null}
) : null} {canCreate && launchTeam && effectivePrepare.state === 'failed' ? (

CLI environment is not available - launch is blocked

{effectivePrepare.message ?? 'Failed to prepare environment'}

Pre-flight check to catch errors before launch

{!shouldHideProvisioningProviderStatusList(prepareChecks, prepareMessage) ? ( ) : null} {prepareWarnings.length > 0 && prepareChecks.length === 0 ? (
{prepareWarnings.map((warning) => (

{warning}

))}
) : null}

{getProvisioningFailureHint(effectivePrepare.message, prepareChecks)}

) : null}
{canOpenExistingTeam ? ( ) : null}
); };