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, 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 { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions'; 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 { useShallow } from 'zustand/react/shallow'; import { AdvancedCliSection } from './AdvancedCliSection'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; import { CodexFastModeSelector } from './CodexFastModeSelector'; import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt'; import { clearInheritedMemberModelsUnavailableForProvider, resolveProviderScopedMemberModel, } from './memberModelScope'; import { OptionalSettingsSection } from './OptionalSettingsSection'; import { isDeletedProjectPathSelection, isSelectableProjectPathProject, } from './projectPathOptions'; import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects'; import { ProjectPathSelector } from './ProjectPathSelector'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { mergeReusableProviderPrepareModelResults, type ProviderPrepareDiagnosticsModelResult, runProviderPrepareDiagnostics, } from './providerPrepareDiagnostics'; import { buildProviderPreparePlans, type ProviderPreparePlan } from './providerPreparePlans'; import { buildProviderPrepareModelChecksSignature, buildProviderPrepareRuntimeStatusSignature, } from './providerPrepareRequestSignature'; import { getShortLivedProviderPrepareModelIssueReasons, storeShortLivedProviderPrepareModelResults, } from './providerPrepareShortLivedCache'; import { getProvisioningModelIssue } from './provisioningModelIssues'; import { ProvisioningProviderRuntimeSettingsDialog } from './ProvisioningProviderRuntimeSettingsDialog'; import { deriveEffectiveProvisioningPrepareState, getPrimaryProvisioningFailureDetail, getProvisioningFailureHint, getProvisioningProviderBackendSummary, getProvisioningProviderProgressMessage, type ProvisioningProviderCheck, ProvisioningProviderStatusList, shouldHideProvisioningProviderStatusList, updateProviderCheck, } from './ProvisioningProviderStatusList'; import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox'; import { analyzeTeammateRuntimeCompatibility, useTmuxRuntimeReadiness, } from './teammateRuntimeCompatibility'; import { TeammateRuntimeCompatibilityNotice } from './TeammateRuntimeCompatibilityNotice'; import { computeEffectiveTeamModel } from './TeamModelSelector'; import { getNextSuggestedTeamName } from './teamNameSets'; import { getWorktreeGitBlockingMessage, getWorktreeGitControlDisabledReason, useWorktreeGitReadiness, WorktreeGitReadinessBanner, } from './WorktreeGitReadinessBanner'; import type { MemberDraft } from '@renderer/components/team/members/MembersEditorSection'; import type { CliProviderId, EffortLevel, TeamCreateRequest, TeamFastMode, TeamProviderId, TeamProvisioningMemberInput, TeamProvisioningModelCheckRequest, } from '@shared/types'; const TEAM_COLOR_NAMES = [ 'blue', 'green', 'red', 'yellow', 'purple', 'cyan', 'orange', 'pink', ] as const; const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate'; 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 }; } type IdleWindow = Window & { requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number; cancelIdleCallback?: (id: number) => void; }; interface ScheduledIdleHandle { kind: 'idle' | 'timeout'; id: number; } function scheduleIdle(cb: () => void): ScheduledIdleHandle { const idleWindow = window as IdleWindow; if (typeof idleWindow.requestIdleCallback === 'function') { return { kind: 'idle', id: idleWindow.requestIdleCallback(cb, { timeout: 2000 }) }; } return { kind: 'timeout', id: window.setTimeout(cb, 0) }; } function cancelScheduledIdle(handle: ScheduledIdleHandle | null): void { if (!handle) return; if (handle.kind === 'idle') { const idleWindow = window as IdleWindow; if (typeof idleWindow.cancelIdleCallback === 'function') { idleWindow.cancelIdleCallback(handle.id); } return; } window.clearTimeout(handle.id); } function cancelScheduledIdleSet(handles: Set): void { for (const handle of handles) { cancelScheduledIdle(handle); } handles.clear(); } function isCurrentPrepareGeneration(ref: { current: number }, generation: number): boolean { return ref.current === generation; } 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, cliStatusLoading } = useStore( useShallow((s) => ({ cliStatus: s.cliStatus, cliStatusLoading: s.cliStatusLoading })) ); const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus); const fetchCliStatus = useStore((s) => s.fetchCliStatus); const openDashboard = useStore((s) => s.openDashboard); 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 [prepareProviderInvalidationEpochById, setPrepareProviderInvalidationEpochById] = useState< Partial> >({}); const [providerSettingsProviderId, setProviderSettingsProviderId] = useState(null); const prepareRequestSeqRef = useRef(0); const prepareIdleHandlesRef = useRef(new Set()); const prepareUnmountGenerationRef = useRef(0); const appliedDefaultProjectPathRef = useRef(null); 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 = useMemo(() => sanitizeTeamName(teamName.trim()) || '_new_', [teamName]); const [worktreeEnabled, setWorktreeEnabledRaw] = useState(false); const [worktreeName, setWorktreeNameRaw] = useState(''); const [customArgs, setCustomArgsRaw] = useState(''); useEffect(() => { migrateLegacyCreateTeamPreferences(); }, []); useEffect(() => { if (!open) { setProviderSettingsProviderId(null); } }, [open]); // 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 = useCallback( (value: string): void => { const normalizedValue = normalizeExplicitTeamModelForUi(selectedProviderId, value); setSelectedModelRaw(normalizedValue); setStoredCreateTeamModel(selectedProviderId, normalizedValue); }, [selectedProviderId] ); const setSelectedProviderId = useCallback( (value: TeamProviderId): void => { const normalizedValue = normalizeLeadProviderForMode(value, multimodelEnabled); setSelectedProviderIdRaw(normalizedValue); setStoredCreateTeamProvider(normalizedValue); setSelectedModelRaw(getStoredTeamModel(normalizedValue)); }, [multimodelEnabled] ); const setLimitContext = useCallback((value: boolean): void => { setLimitContextRaw(value); setStoredCreateTeamLimitContext(value); }, []); const setSkipPermissions = useCallback((value: boolean): void => { setSkipPermissionsRaw(value); setStoredCreateTeamSkipPermissions(value); }, []); const setSelectedEffort = useCallback((value: string): void => { setSelectedEffortRaw(value); setStoredCreateTeamEffort(value); }, []); const setSelectedFastMode = useCallback((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 selectedProjectPathDeleted = useMemo( () => cwdMode === 'project' && selectedProjectPath.length > 0 && isDeletedProjectPathSelection(projects, selectedProjectPath), [cwdMode, projects, selectedProjectPath] ); const selectedProjectCwd = isEphemeralProjectPath(selectedProjectPath) || selectedProjectPathDeleted ? '' : 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 = useMemo( () => getNextSuggestedTeamName(allTakenTeamNames), [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 hasSelectedWorktreeIsolation = !soloTeam && effectiveMemberDrafts.some((member) => !member.removedAt && member.isolation === 'worktree'); const worktreeGitReadiness = useWorktreeGitReadiness( effectiveCwd || null, open && canCreate && hasSelectedWorktreeIsolation ); const worktreeIsolationDisabledReason = !soloTeam && canCreate ? getWorktreeGitControlDisabledReason(worktreeGitReadiness) : null; const worktreeGitBlockingMessage = getWorktreeGitBlockingMessage( worktreeGitReadiness, hasSelectedWorktreeIsolation ); const worktreeGitBlocksSubmission = Boolean(worktreeGitBlockingMessage); const tmuxRuntime = useTmuxRuntimeReadiness(open && canCreate); 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 hasSelectedAnthropicRuntime = selectedMemberProviders.includes('anthropic'); const effectiveAnthropicRuntimeLimitContext = hasSelectedAnthropicRuntime ? limitContext : false; 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 selectedProviderBackendId = useMemo( () => resolveUiOwnedProviderBackendId( selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ), [runtimeProviderStatusById, selectedProviderId] ); const runtimeBackendSummaryByProviderRef = useRef(runtimeBackendSummaryByProvider); const prepareChecksRef = useRef([]); const prepareMessageRef = useRef(null); const prepareModelResultsCacheRef = useRef( new Map>() ); const lastPrepareProviderSignatureByIdRef = useRef(new Map()); const pendingPrepareProviderSignatureByIdRef = useRef(new Map()); const prepareProviderRequestSeqByIdRef = useRef(new Map()); const prepareWarningsByProviderIdRef = useRef(new Map()); 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(() => { prepareMessageRef.current = prepareMessage; }, [prepareMessage]); const invalidatePrepareProvider = useCallback((providerId: CliProviderId): void => { if (!isTeamProviderId(providerId)) { return; } lastPrepareProviderSignatureByIdRef.current.delete(providerId); pendingPrepareProviderSignatureByIdRef.current.delete(providerId); prepareProviderRequestSeqByIdRef.current.set( providerId, (prepareProviderRequestSeqByIdRef.current.get(providerId) ?? 0) + 1 ); prepareWarningsByProviderIdRef.current.delete(providerId); setPrepareProviderInvalidationEpochById((current) => ({ ...current, [providerId]: (current[providerId] ?? 0) + 1, })); }, []); useEffect(() => { if (!open) { lastPrepareProviderSignatureByIdRef.current.clear(); pendingPrepareProviderSignatureByIdRef.current.clear(); prepareProviderRequestSeqByIdRef.current.clear(); prepareWarningsByProviderIdRef.current.clear(); } }, [open]); useEffect(() => { const generation = ++prepareUnmountGenerationRef.current; const idleHandles = prepareIdleHandlesRef.current; const lastProviderSignatures = lastPrepareProviderSignatureByIdRef.current; const pendingProviderSignatures = pendingPrepareProviderSignatureByIdRef.current; const providerRequestSeqs = prepareProviderRequestSeqByIdRef.current; const warningsByProviderId = prepareWarningsByProviderIdRef.current; return () => { // React StrictMode replays effect cleanup/setup in development; defer // invalidation so the replay does not cancel the live prepare request. queueMicrotask(() => { if (!isCurrentPrepareGeneration(prepareUnmountGenerationRef, generation)) { return; } cancelScheduledIdleSet(idleHandles); prepareRequestSeqRef.current += 1; lastProviderSignatures.clear(); pendingProviderSignatures.clear(); providerRequestSeqs.clear(); warningsByProviderId.clear(); }); }; }, []); const selectedEffortForCurrentSelection = useMemo( () => getAvailableTeamEffortValue({ providerId: selectedProviderId, model: selectedModel, limitContext: effectiveAnthropicRuntimeLimitContext, providerStatus: runtimeProviderStatusById.get(selectedProviderId), value: selectedEffort, }), [ effectiveAnthropicRuntimeLimitContext, runtimeProviderStatusById, selectedEffort, selectedModel, selectedProviderId, ] ); const selectedModelChecksByProvider = useMemo(() => { const modelsByProvider = new Map(); const leadEffort = (selectedEffortForCurrentSelection as EffortLevel | '') || undefined; const addModel = ( providerId: TeamProviderId, model: string | undefined, effort?: EffortLevel ): void => { const trimmed = model?.trim() ?? ''; if (!trimmed) { return; } const existing = modelsByProvider.get(providerId) ?? []; if (!existing.some((entry) => entry.model === trimmed && entry.effort === effort)) { modelsByProvider.set(providerId, [ ...existing, { providerId, model: trimmed, ...(effort ? { effort } : {}), }, ]); } }; const addDefaultSelection = (providerId: TeamProviderId, effort?: EffortLevel): void => { if ( providerId === 'codex' || providerId === 'gemini' || (providerId === 'anthropic' && selectedProviderId === 'anthropic') ) { addModel(providerId, DEFAULT_PROVIDER_MODEL_SELECTION, effort); } }; const leadModel = computeEffectiveTeamModel( selectedModel, effectiveAnthropicRuntimeLimitContext, selectedProviderId ); if (selectedModel.trim()) { addModel(selectedProviderId, leadModel, leadEffort); } else { addDefaultSelection(selectedProviderId, leadEffort); } for (const member of effectiveMemberDrafts) { if (member.removedAt) { continue; } const memberProviderId = normalizeOptionalTeamProviderId(member.providerId); const inheritsDefaultRuntime = !memberProviderId || memberProviderId === selectedProviderId; const explicitMemberModel = member.model?.trim() ?? ''; const memberEffort = member.effort ?? (inheritsDefaultRuntime && !explicitMemberModel ? leadEffort : undefined); const scopedModel = resolveProviderScopedMemberModel({ memberProviderId: member.providerId, memberModel: member.model, selectedProviderId, runtimeProviderStatusById, }); if (scopedModel.model) { addModel(scopedModel.providerId, scopedModel.model, memberEffort); } else { addDefaultSelection(scopedModel.providerId, memberEffort); } } return modelsByProvider; }, [ effectiveAnthropicRuntimeLimitContext, effectiveMemberDrafts, runtimeProviderStatusById, selectedEffortForCurrentSelection, selectedModel, selectedProviderId, ]); const selectedModelChecksByProviderSignature = useMemo( () => buildProviderPrepareModelChecksSignature(selectedModelChecksByProvider), [selectedModelChecksByProvider] ); const shortLivedModelIssueReasons = useMemo(() => { void prepareChecks; void selectedModelChecksByProviderSignature; const modelAdvisoryReasonByProvider: Partial>> = {}; const modelIssueReasonByProvider: Partial>> = {}; const modelUnavailableReasonByProvider: Partial< Record> > = {}; for (const providerId of selectedMemberProviders) { const backendSummary = runtimeBackendSummaryByProvider.get(providerId) ?? null; const providerRuntimeStatusSignature = buildProviderPrepareRuntimeStatusSignature( [providerId], runtimeProviderStatusById ); const providerModelChecksSignature = buildProviderPrepareModelChecksSignature( new Map([[providerId, selectedModelChecksByProvider.get(providerId) ?? []]]) ); const cacheKey = buildProviderPrepareModelCacheKey({ cwd: effectiveCwd, providerId, backendSummary, limitContext: effectiveAnthropicRuntimeLimitContext, runtimeStatusSignature: providerRuntimeStatusSignature, modelChecksSignature: providerModelChecksSignature, }); const issueReasons = getShortLivedProviderPrepareModelIssueReasons({ providerId, cacheKey, }); if (Object.keys(issueReasons.modelAdvisoryReasonByValue).length > 0) { modelAdvisoryReasonByProvider[providerId] = issueReasons.modelAdvisoryReasonByValue; } if (Object.keys(issueReasons.modelIssueReasonByValue).length > 0) { modelIssueReasonByProvider[providerId] = issueReasons.modelIssueReasonByValue; } if (Object.keys(issueReasons.modelUnavailableReasonByValue).length > 0) { modelUnavailableReasonByProvider[providerId] = issueReasons.modelUnavailableReasonByValue; } } return { modelAdvisoryReasonByProvider, modelIssueReasonByProvider, modelUnavailableReasonByProvider, }; }, [ effectiveAnthropicRuntimeLimitContext, effectiveCwd, prepareChecks, runtimeBackendSummaryByProvider, runtimeProviderStatusById, selectedModelChecksByProvider, selectedModelChecksByProviderSignature, selectedMemberProviders, ]); 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]); const handleCodexReconnect = useCallback( (mode: 'browser' | 'device_code' = 'browser') => { void (async () => { await codexAccount.startChatgptLogin(mode); })(); }, [codexAccount] ); useEffect(() => { if (!open || !canCreate || !launchTeam) { cancelScheduledIdleSet(prepareIdleHandlesRef.current); prepareRequestSeqRef.current += 1; lastPrepareProviderSignatureByIdRef.current.clear(); pendingPrepareProviderSignatureByIdRef.current.clear(); prepareProviderRequestSeqByIdRef.current.clear(); prepareWarningsByProviderIdRef.current.clear(); return; } if (typeof api.teams.prepareProvisioning !== 'function') { cancelScheduledIdleSet(prepareIdleHandlesRef.current); prepareRequestSeqRef.current += 1; lastPrepareProviderSignatureByIdRef.current.clear(); pendingPrepareProviderSignatureByIdRef.current.clear(); prepareProviderRequestSeqByIdRef.current.clear(); prepareWarningsByProviderIdRef.current.clear(); setPrepareState('failed'); setPrepareWarnings([]); setPrepareChecks([]); setPrepareMessage( 'Current preload version does not support team:prepareProvisioning. Restart the dev app.' ); return; } if (!effectiveCwd) { cancelScheduledIdleSet(prepareIdleHandlesRef.current); prepareRequestSeqRef.current += 1; lastPrepareProviderSignatureByIdRef.current.clear(); pendingPrepareProviderSignatureByIdRef.current.clear(); prepareProviderRequestSeqByIdRef.current.clear(); prepareWarningsByProviderIdRef.current.clear(); setPrepareState('idle'); setPrepareWarnings([]); setPrepareChecks([]); setPrepareMessage('Select a working directory to validate the launch environment.'); return; } const selectedProviderIdSet = new Set(selectedMemberProviders); for (const providerId of Array.from(lastPrepareProviderSignatureByIdRef.current.keys())) { if (!selectedProviderIdSet.has(providerId)) { lastPrepareProviderSignatureByIdRef.current.delete(providerId); pendingPrepareProviderSignatureByIdRef.current.delete(providerId); prepareProviderRequestSeqByIdRef.current.delete(providerId); prepareWarningsByProviderIdRef.current.delete(providerId); } } const providerPlans = buildProviderPreparePlans({ cwd: effectiveCwd, providerIds: selectedMemberProviders, selectedModelChecksByProvider, backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current, limitContext: effectiveAnthropicRuntimeLimitContext, runtimeProviderStatusById, cachedModelResultsByCacheKey: prepareModelResultsCacheRef.current, }); const changedPlans = providerPlans.filter((plan) => { const lastSignature = lastPrepareProviderSignatureByIdRef.current.get(plan.providerId); const pendingSignature = pendingPrepareProviderSignatureByIdRef.current.get(plan.providerId); return lastSignature !== plan.requestSignature && pendingSignature !== plan.requestSignature; }); const loadingMessage = getProvisioningProviderProgressMessage( changedPlans.map((plan) => plan.providerId), selectedMemberProviders.length ); const getSelectedWarnings = (): string[] => selectedMemberProviders.flatMap( (providerId) => prepareWarningsByProviderIdRef.current.get(providerId) ?? [] ); const commitChecks = (nextChecks: ProvisioningProviderCheck[]): void => { prepareChecksRef.current = nextChecks; setPrepareChecks(nextChecks); }; const applyPrepareOutcome = ( nextChecks: ProvisioningProviderCheck[], pendingMessage: string | null ): void => { const selectedWarnings = getSelectedWarnings(); setPrepareWarnings(selectedWarnings); if (nextChecks.some((check) => check.status === 'pending' || check.status === 'checking')) { setPrepareState('loading'); setPrepareMessage(pendingMessage); return; } const anyFailure = nextChecks.some((check) => check.status === 'failed'); const anyNotes = selectedWarnings.length > 0 || nextChecks.some((check) => check.status === 'notes'); const failureMessage = getPrimaryProvisioningFailureDetail(nextChecks) ?? 'Some selected providers need attention.'; setPrepareState(anyFailure ? 'failed' : 'ready'); setPrepareMessage( anyFailure ? failureMessage : anyNotes ? 'All selected providers are ready, with notes.' : 'All selected providers are ready.' ); }; let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders); for (const plan of changedPlans) { checks = updateProviderCheck(checks, plan.providerId, { status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking', backendSummary: plan.backendSummary, details: plan.cachedSnapshot.details, supportDiagnostics: undefined, }); prepareWarningsByProviderIdRef.current.delete(plan.providerId); } commitChecks(checks); applyPrepareOutcome( checks, changedPlans.length > 0 ? loadingMessage : (prepareMessageRef.current ?? getProvisioningProviderProgressMessage([], selectedMemberProviders.length)) ); if (changedPlans.length === 0) { return; } for (const plan of changedPlans) { pendingPrepareProviderSignatureByIdRef.current.set(plan.providerId, plan.requestSignature); } const idleHandle = scheduleIdle(() => { prepareIdleHandlesRef.current.delete(idleHandle); const generation = prepareRequestSeqRef.current; const runningPlans = changedPlans.flatMap((plan) => { if ( pendingPrepareProviderSignatureByIdRef.current.get(plan.providerId) !== plan.requestSignature ) { return []; } pendingPrepareProviderSignatureByIdRef.current.delete(plan.providerId); const requestSeq = (prepareProviderRequestSeqByIdRef.current.get(plan.providerId) ?? 0) + 1; prepareProviderRequestSeqByIdRef.current.set(plan.providerId, requestSeq); lastPrepareProviderSignatureByIdRef.current.set(plan.providerId, plan.requestSignature); return [{ ...plan, requestSeq }]; }); if (runningPlans.length === 0) { return; } const isPlanCurrent = (plan: ProviderPreparePlan & { requestSeq: number }): boolean => prepareRequestSeqRef.current === generation && lastPrepareProviderSignatureByIdRef.current.get(plan.providerId) === plan.requestSignature && prepareProviderRequestSeqByIdRef.current.get(plan.providerId) === plan.requestSeq && !pendingPrepareProviderSignatureByIdRef.current.has(plan.providerId); void (async () => { await Promise.all( runningPlans.map(async (plan) => { try { const prepResult = await runProviderPrepareDiagnostics({ cwd: effectiveCwd, providerId: plan.providerId, selectedModelIds: plan.selectedModelIds, selectedModelChecks: plan.selectedModelChecks, prepareProvisioning: api.teams.prepareProvisioning, limitContext: effectiveAnthropicRuntimeLimitContext, cachedModelResultsById: plan.cachedModelResultsById, onModelProgress: ({ status, details }) => { if (!isPlanCurrent(plan)) { return; } const nextChecks = updateProviderCheck( prepareChecksRef.current, plan.providerId, { status, backendSummary: plan.backendSummary, details, supportDiagnostics: undefined, } ); commitChecks(nextChecks); applyPrepareOutcome(nextChecks, loadingMessage); }, }); if (!isPlanCurrent(plan)) { return; } prepareWarningsByProviderIdRef.current.set( plan.providerId, prepResult.warnings.map( (warning) => `${getProviderLabel(plan.providerId)}: ${warning}` ) ); prepareModelResultsCacheRef.current.set( plan.cacheKey, mergeReusableProviderPrepareModelResults( prepareModelResultsCacheRef.current.get(plan.cacheKey), prepResult.modelResultsById ) ); storeShortLivedProviderPrepareModelResults({ providerId: plan.providerId, cacheKey: plan.cacheKey, modelResultsById: prepResult.modelResultsById, }); const nextChecks = updateProviderCheck(prepareChecksRef.current, plan.providerId, { status: prepResult.status, backendSummary: plan.backendSummary, details: prepResult.details, supportDiagnostics: prepResult.supportDiagnostics, }); commitChecks(nextChecks); applyPrepareOutcome(nextChecks, loadingMessage); } catch (error) { if (!isPlanCurrent(plan)) { return; } const failureMessage = error instanceof Error ? error.message : 'Failed to prepare selected providers'; const nextChecks = updateProviderCheck(prepareChecksRef.current, plan.providerId, { status: 'failed', backendSummary: plan.backendSummary, details: [failureMessage], supportDiagnostics: undefined, }); prepareWarningsByProviderIdRef.current.delete(plan.providerId); commitChecks(nextChecks); applyPrepareOutcome(nextChecks, failureMessage); } }) ); })(); }); prepareIdleHandlesRef.current.add(idleHandle); }, [ open, canCreate, launchTeam, effectiveCwd, effectiveMemberDrafts, effectiveAnthropicRuntimeLimitContext, prepareProviderInvalidationEpochById, runtimeProviderStatusById, selectedModel, selectedModelChecksByProvider, selectedModelChecksByProviderSignature, selectedProviderId, selectedMemberProviders, ]); useEffect(() => { if (!open) { return; } setProjectsLoading(true); setProjectsError(null); let cancelled = false; void (async () => { try { const nextProjects = await loadProjectPathProjects({ defaultProjectPath }); if (cancelled) { return; } 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, mcpPolicy: m.mcpPolicy, }), 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) { appliedDefaultProjectPathRef.current = null; return; } if (cwdMode !== 'project') { return; } const selectableProjects = projects.filter(isSelectableProjectPathProject); if (selectableProjects.length === 0) { return; } if (defaultProjectPath && !isEphemeralProjectPath(defaultProjectPath)) { const normalizedDefaultProjectPath = normalizePath(defaultProjectPath); const defaultAlreadyApplied = appliedDefaultProjectPathRef.current === normalizedDefaultProjectPath; const match = selectableProjects.find( (p) => normalizePath(p.path) === normalizedDefaultProjectPath ); if (match && !defaultAlreadyApplied) { appliedDefaultProjectPathRef.current = normalizedDefaultProjectPath; if (normalizePath(selectedProjectPath) !== normalizedDefaultProjectPath) { setSelectedProjectPath(match.path); } return; } } if (selectedProjectPath) { 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, setSelectedProjectPath]); useEffect(() => { if (!open || cwdMode !== 'project' || !selectedProjectPath) { return; } if ( !isEphemeralProjectPath(selectedProjectPath) && !isDeletedProjectPathSelection(projects, selectedProjectPath) ) { return; } setSelectedProjectPath(''); }, [open, cwdMode, projects, 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, effectiveAnthropicRuntimeLimitContext, selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ), [ effectiveAnthropicRuntimeLimitContext, runtimeProviderStatusById, selectedModel, selectedProviderId, ] ); const teammateRuntimeCompatibility = useMemo( () => analyzeTeammateRuntimeCompatibility({ leadProviderId: selectedProviderId, leadProviderBackendId: selectedProviderBackendId, members: effectiveMemberDrafts, soloTeam: soloTeam || !canCreate, extraCliArgs: launchTeam ? customArgs : undefined, tmuxStatus: tmuxRuntime.status, tmuxStatusLoading: tmuxRuntime.loading, tmuxStatusError: tmuxRuntime.error, }), [ customArgs, effectiveMemberDrafts, launchTeam, canCreate, selectedProviderBackendId, selectedProviderId, soloTeam, tmuxRuntime.error, tmuxRuntime.loading, tmuxRuntime.status, ] ); const teammateRuntimeProviderNoticeById: | Partial> | undefined = teammateRuntimeCompatibility.providerNoticeProviderId ? { [teammateRuntimeCompatibility.providerNoticeProviderId]: ( { onClose(); openDashboard(); }} /> ), } : undefined; const showRosterTeammateRuntimeCompatibility = teammateRuntimeCompatibility.visible && !teammateRuntimeCompatibility.providerNoticeProviderId; const anthropicRuntimeSelection = useMemo( () => selectedProviderId === 'anthropic' ? resolveAnthropicRuntimeSelection({ source: { modelCatalog: runtimeProviderStatusById.get('anthropic')?.modelCatalog, runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities, }, selectedModel, limitContext: effectiveAnthropicRuntimeLimitContext, }) : null, [ effectiveAnthropicRuntimeLimitContext, 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: effectiveAnthropicRuntimeLimitContext, }), selectedEffort: selectedEffortForCurrentSelection, selectedFastMode, providerFastModeDefault: anthropicProviderFastModeDefault, runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities, }) : { nextEffort: selectedEffortForCurrentSelection, effortResetReason: null, ...reconcileCodexRuntimeSelections({ selection: codexRuntimeSelection ?? resolveCodexRuntimeSelection({ source: { providerStatus: runtimeProviderStatusById.get('codex'), providerBackendId: resolveUiOwnedProviderBackendId( 'codex', runtimeProviderStatusById.get('codex') ), }, selectedModel, }), selectedFastMode, }), }; const notices: string[] = []; if (selectedEffortForCurrentSelection !== selectedEffort) { setSelectedEffortRaw(selectedEffortForCurrentSelection); setStoredCreateTeamEffort(selectedEffortForCurrentSelection); } if (reconciliation.nextEffort !== selectedEffortForCurrentSelection) { 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, effectiveAnthropicRuntimeLimitContext, runtimeProviderStatusById, selectedEffort, selectedEffortForCurrentSelection, 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, { inheritedProviderId: selectedProviderId, }), cwd: effectiveCwd, prompt: prompt.trim() || undefined, providerId: selectedProviderId, providerBackendId: selectedProviderBackendId ?? undefined, model: effectiveModel, effort: (selectedEffortForCurrentSelection as EffortLevel) || undefined, fastMode: selectedProviderId === 'anthropic' || selectedProviderId === 'codex' ? selectedFastMode : undefined, limitContext: effectiveAnthropicRuntimeLimitContext, skipPermissions, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, extraCliArgs: customArgs.trim() || undefined, }), [ sanitizedTeamName, description, teamColor, soloTeam, effectiveMemberDrafts, effectiveCwd, prompt, selectedProviderId, selectedProviderBackendId, effectiveModel, selectedEffortForCurrentSelection, selectedFastMode, effectiveAnthropicRuntimeLimitContext, skipPermissions, worktreeEnabled, worktreeName, customArgs, ] ); const requestValidation = useMemo( () => validateRequest(request, { requireCwd: launchTeam }), [request, launchTeam] ); const modelValidationError = useMemo(() => { if (selectedProviderId === 'opencode') { if (!selectedModel.trim()) { return 'OpenCode lead requires a selected model.'; } const activeMemberCount = soloTeam ? 0 : effectiveMemberDrafts.filter((member) => !member.removedAt && member.name.trim()).length; if (activeMemberCount === 0) { return 'OpenCode lead requires at least one OpenCode teammate.'; } } 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, soloTeam, ]); 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 || teammateRuntimeCompatibility.blocksSubmission || worktreeGitBlocksSubmission; 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' ? selectedEffortForCurrentSelection || anthropicRuntimeSelection?.defaultEffort || '' : selectedEffortForCurrentSelection; 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, selectedEffortForCurrentSelection, 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 (effectiveAnthropicRuntimeLimitContext) { summary.push('Anthropic limited to 200K context'); } if (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`); if (customArgs.trim()) summary.push('Custom CLI args'); return summary; }, [ anthropicProviderFastModeDefault, customArgs, effectiveAnthropicRuntimeLimitContext, 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 showCodexReconnectPrompt = shouldShowCodexReconnectPrompt({ effectiveCliStatus, selectedProviderIds: selectedMemberProviders, prepareMessage: effectivePrepare.message, prepareChecks, }); 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; } if (teammateRuntimeCompatibility.blocksSubmission) { setLocalError(teammateRuntimeCompatibility.message); return; } if (worktreeGitBlockingMessage) { setLocalError(worktreeGitBlockingMessage); 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, prompt: request.prompt, providerId: request.providerId, providerBackendId: request.providerBackendId, model: request.model, effort: request.effort, fastMode: request.fastMode, limitContext: request.limitContext, skipPermissions: request.skipPermissions, worktree: request.worktree, extraCliArgs: request.extraCliArgs, }); 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; }); }; const rosterHeaderTop = useMemo( () => (
setSoloTeam(checked === true)} />
), [setSoloTeam, soloTeam] ); const rosterHeaderBottom = useMemo( () => showRosterTeammateRuntimeCompatibility || soloTeam || (canCreate && hasSelectedWorktreeIsolation) ? (
{showRosterTeammateRuntimeCompatibility ? ( { onClose(); openDashboard(); }} /> ) : null} {soloTeam ? (

Only the team lead (main process) will be started — no teammates will be spawned. Works like a regular agent session in your chosen runtime (Claude Code, Codex, OpenCode, Gemini) 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} {canCreate && hasSelectedWorktreeIsolation ? ( ) : null}
) : null, [ canCreate, hasSelectedWorktreeIsolation, onClose, openDashboard, showRosterTeammateRuntimeCompatibility, soloTeam, teammateRuntimeCompatibility, worktreeGitReadiness, ] ); return ( { if (!nextOpen) { resetUIState(); onClose(); } }} > {initialData ? 'Copy Team' : 'Create Team'} {initialData ? 'Create a new team based on an existing one.' : 'Set up your team and choose how it starts.'} {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}
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' ? 'Checking selected providers...' : 'Preparing environment...')}

Pre-flight check to catch errors before launch

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

{effectivePrepare.message}

) : null} setProviderSettingsProviderId(providerId)} /> {prepareWarnings.length > 0 && prepareChecks.length === 0 ? (
{prepareWarnings.map((warning, index) => (

{warning}

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

Runtime environment is not available - launch is blocked

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

Pre-flight check to catch errors before launch

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

{warning}

))}
) : null}

{getProvisioningFailureHint(effectivePrepare.message, prepareChecks)}

{showCodexReconnectPrompt ? (
handleCodexReconnect('browser')} onDeviceCodeReconnect={() => handleCodexReconnect('device_code')} />
) : null}
) : null}
{canOpenExistingTeam ? ( ) : null}
setProviderSettingsProviderId(providerId)} providers={effectiveCliStatus?.providers ?? []} projectPath={effectiveCwd || null} disabled={isSubmitting} onProviderRuntimeChanged={invalidatePrepareProvider} />
); };