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 { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { ProviderActivityStatusStrip } from '@renderer/components/common/ProviderActivityStatusStrip'; 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 { 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, isTeamProviderRuntimeStatusLoading, 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, t: ReturnType['t'] ): string | null { const trimmed = name.trim(); if (!trimmed) return null; const sanitized = sanitizeTeamName(trimmed); if (!sanitized) { return t('create.validation.nameMustContainLetterOrDigit'); } if (sanitized.length > 128) { return t('create.validation.nameTooLong'); } 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, t: ReturnType['t'], options?: { requireCwd?: boolean } ): ValidationResult { const requireCwd = options?.requireCwd ?? true; const sanitized = sanitizeTeamName(request.teamName); if (!sanitized) { return { valid: false, errors: { teamName: t('create.validation.nameMustContainLetterOrDigit'), }, }; } if (sanitized.length > 128) { return { valid: false, errors: { teamName: t('create.validation.nameTooLong'), }, }; } if (requireCwd && !request.cwd.trim()) { return { valid: false, errors: { cwd: t('create.validation.selectWorkingDirectory'), }, }; } if (request.members.some((member) => !member.name.trim())) { return { valid: false, errors: { members: t('create.validation.memberNameRequired'), }, }; } if (request.members.some((member) => validateMemberNameInline(member.name.trim()) !== null)) { return { valid: false, errors: { members: t('create.validation.memberNameInvalid'), }, }; } const uniqueNames = new Set(request.members.map((member) => member.name.trim().toLowerCase())); if (uniqueNames.size !== request.members.length) { return { valid: false, errors: { members: t('create.validation.memberNamesUnique'), }, }; } 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 { t } = useAppTranslation('team'); const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const anthropicProviderFastModeDefault = useStore( (s) => s.appConfig?.providerConnections?.anthropic.fastModeDefault ?? false ); const { cliStatus, cliStatusLoading, cliProviderStatusLoading } = useStore( useShallow((s) => ({ cliStatus: s.cliStatus, cliStatusLoading: s.cliStatusLoading, cliProviderStatusLoading: s.cliProviderStatusLoading, })) ); 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] ); const codexSnapshotPending = codexAccount.loading && Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')) && !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 [workflowMentionSuggestionsEnabled, setWorkflowMentionSuggestionsEnabled] = useState(false); 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 enableWorkflowMentionSuggestions = useCallback((): void => { setWorkflowMentionSuggestionsEnabled(true); }, []); 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 runtimeProviderLoadingById = useMemo( () => new Map( selectedMemberProviders.map( (providerId) => [ providerId, isTeamProviderRuntimeStatusLoading( providerId, runtimeProviderStatusById.get(providerId), cliProviderStatusLoading[providerId] === true || (providerId === 'codex' && codexSnapshotPending) ), ] as const ) ), [ cliProviderStatusLoading, codexSnapshotPending, runtimeProviderStatusById, selectedMemberProviders, ] ); 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(t('create.prepare.unsupportedPreload')); 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(t('create.prepare.selectWorkingDirectory')); 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 loadingProviderIds = selectedMemberProviders.filter((providerId) => runtimeProviderLoadingById.get(providerId) ); const readyProviderIds = selectedMemberProviders.filter( (providerId) => !runtimeProviderLoadingById.get(providerId) ); const providerPlans = buildProviderPreparePlans({ cwd: effectiveCwd, providerIds: readyProviderIds, 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( [...loadingProviderIds, ...changedPlans.map((plan) => plan.providerId)], selectedMemberProviders.length, t ); 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) ?? t('create.prepare.someProvidersNeedAttention'); setPrepareState(anyFailure ? 'failed' : 'ready'); setPrepareMessage( anyFailure ? failureMessage : anyNotes ? t('create.prepare.readyWithNotes') : t('create.prepare.ready') ); }; let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders); for (const providerId of loadingProviderIds) { lastPrepareProviderSignatureByIdRef.current.delete(providerId); pendingPrepareProviderSignatureByIdRef.current.delete(providerId); prepareProviderRequestSeqByIdRef.current.delete(providerId); prepareWarningsByProviderIdRef.current.delete(providerId); checks = updateProviderCheck(checks, providerId, { status: 'checking', backendSummary: runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null, details: [ `${getProviderLabel(providerId)} provider status is still loading. Model checks will start automatically.`, ], supportDiagnostics: undefined, }); } 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, t)) ); 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 : t('create.prepare.failed'); 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, runtimeProviderLoadingById, selectedModel, selectedModelChecksByProvider, selectedModelChecksByProviderSignature, selectedProviderId, selectedMemberProviders, t, ]); useEffect(() => { if (!open) { setWorkflowMentionSuggestionsEnabled(false); 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 : t('create.errors.loadProjectsFailed') ); setProjects([]); } finally { if (!cancelled) { setProjectsLoading(false); } } })(); return () => { cancelled = true; }; }, [open, defaultProjectPath, t]); 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]); const { suggestions: taskSuggestions } = useTaskSuggestions(null, { enabled: workflowMentionSuggestionsEnabled, }); const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null, { enabled: workflowMentionSuggestionsEnabled, }); 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, t); 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, t, { requireCwd: launchTeam }), [request, launchTeam, t] ); const modelValidationError = useMemo(() => { if (selectedProviderId === 'opencode') { if (!selectedModel.trim()) { return t('create.validation.openCodeLeadModelRequired'); } const activeMemberCount = soloTeam ? 0 : effectiveMemberDrafts.filter((member) => !member.removedAt && member.name.trim()).length; if (activeMemberCount === 0) { return t('create.validation.openCodeTeammateRequired'); } } if (!runtimeProviderLoadingById.get(selectedProviderId)) { 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; if (runtimeProviderLoadingById.get(providerId)) { continue; } 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, runtimeProviderLoadingById, selectedModel, selectedProviderId, soloTeam, t, ]); 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, t, }), [prepareChecks, prepareMessage, prepareState, prepareWarnings, t] ); 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 ? t('create.validation.teamLaunching') : t('create.validation.teamNameExists'); setFieldErrors({ teamName: msg }); setLocalError(msg); return; } const validation = validateRequest(request, t, { requireCwd: launchTeam }); if (!validation.valid) { const errors = validation.errors ?? {}; setFieldErrors(errors); const messages = Object.values(errors).filter(Boolean); setLocalError(messages.join(' · ') || t('create.validation.checkFormFields')); 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 : t('create.errors.createConfigFailed') ); } 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, t] ); const rosterHeaderBottom = useMemo( () => showRosterTeammateRuntimeCompatibility || soloTeam || (canCreate && hasSelectedWorktreeIsolation) ? (
{showRosterTeammateRuntimeCompatibility ? ( { onClose(); openDashboard(); }} /> ) : null} {soloTeam ? (

{t('create.solo.description')}

) : null} {canCreate && hasSelectedWorktreeIsolation ? ( ) : null}
) : null, [ canCreate, hasSelectedWorktreeIsolation, onClose, openDashboard, showRosterTeammateRuntimeCompatibility, soloTeam, teammateRuntimeCompatibility, t, worktreeGitReadiness, ] ); return ( { if (!nextOpen) { resetUIState(); onClose(); } }} > {initialData ? t('create.title.copy') : t('create.title.create')} {initialData ? t('create.description.copy') : t('create.description.create')} {conflictingTeam && !conflictDismissed ? (

{t('create.conflict.title', { team: conflictingTeam.displayName })}

{t('create.conflict.description')}

{t('create.conflict.workingDirectory')}{' '} {effectiveCwd}

) : null} {!canCreate ? (

{t('create.localOnly')}

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

{t('create.errors.nameExists')}

) : teamNameInlineError ? (

{teamNameInlineError}

) : isNameProvisioning ? (

{t('create.errors.nameLaunching')}

) : fieldErrors.teamName ? (

{fieldErrors.teamName}

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

{t('create.onDisk')} {sanitizedTeamName}

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

{t('create.launchAfterCreate.description')}

{launchTeam ? (
{ if (isOpen) { enableWorkflowMentionSuggestions(); } }} >
{selectedProviderId === 'anthropic' ? (
{anthropicRuntimeNotice ? (

{anthropicRuntimeNotice}

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

{anthropicRuntimeNotice}

) : null}
) : null}
{t('create.saved')} ) : null } />
) : null}
descriptionDraft.setValue(event.target.value)} placeholder={t('create.placeholders.description')} /> {descriptionDraft.isSaved ? ( {t('create.saved')} ) : null}
{TEAM_COLOR_NAMES.map((colorName) => { const colorSet = getTeamColorSet(colorName); const isSelected = teamColor === colorName; return ( ); })}
{activeError ? (

{activeError}

) : null}
{canCreate && launchTeam ? ( ) : null} {canCreate && launchTeam && (effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading') ? ( <>
{effectivePrepare.message ?? (effectivePrepare.state === 'idle' ? t('create.prepare.checkingProviders') : t('create.prepare.preparingEnvironment'))}

{t('launch.prepare.preflight', { action: t('launch.prepare.action.launch'), })}

setProviderSettingsProviderId(providerId)} /> ) : null} {canCreate && launchTeam && effectivePrepare.state === 'ready' ? (
{prepareChecks.some((check) => check.status === 'notes') || prepareWarnings.length > 0 ? t('create.prepare.selectedProvidersReadyWithNotes') : t('create.prepare.selectedProvidersReady')}
{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' ? (

{t('launch.prepare.blocked', { action: t('launch.prepare.action.launch'), })}

{effectivePrepare.message ?? t('launch.prepare.failed')}

{t('launch.prepare.preflight', { action: t('launch.prepare.action.launch'), })}

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

{warning}

))}
) : null}

{getProvisioningFailureHint(effectivePrepare.message, prepareChecks, t)}

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