import React, { 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 { SkipPermissionsCheckbox } from '@renderer/components/team/dialogs/SkipPermissionsCheckbox'; import { buildMemberDraftColorMap, buildMemberDraftSuggestions, buildMembersFromDrafts, clearMemberModelOverrides, createMemberDraftsFromInputs, filterEditableMemberInputs, normalizeLeadProviderForMode, normalizeMemberDraftForProviderMode, normalizeProviderForMode, validateMemberNameInline, } from '@renderer/components/team/members/MembersEditorSection'; import { TeamRosterEditorSection } from '@renderer/components/team/members/TeamRosterEditorSection'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Combobox } from '@renderer/components/ui/combobox'; 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 } from '@renderer/constants/teamColors'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; 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 { useStore } from '@renderer/store'; import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; import { isTeamProvisioningActive, selectResolvedMembersForTeamName, } from '@renderer/store/slices/teamSlice'; import { isGeminiUiFrozen, normalizeCreateLaunchProviderForUi, } from '@renderer/utils/geminiUiFreeze'; import { normalizePath } from '@renderer/utils/pathNormalize'; import { nameColorSet } from '@renderer/utils/projectColor'; import { resolveUiOwnedProviderBackendId } from '@renderer/utils/providerBackendIdentity'; import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus'; import { getTeamModelSelectionError, normalizeExplicitTeamModelForUi, } from '@renderer/utils/teamModelAvailability'; import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog'; import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath'; import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection'; import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { AlertTriangle, Check, CheckCircle2, ChevronDown, ChevronRight, Info, Loader2, RotateCcw, X, } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { CronScheduleInput } from '../schedule/CronScheduleInput'; import { AdvancedCliSection } from './AdvancedCliSection'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; import { CodexFastModeSelector } from './CodexFastModeSelector'; import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt'; import { EffortLevelSelector } from './EffortLevelSelector'; import { resolveLaunchDialogPrefill } from './launchDialogPrefill'; import { clearInheritedMemberModelsUnavailableForProvider, resolveProviderScopedMemberModel, } from './memberModelScope'; import { OptionalSettingsSection } from './OptionalSettingsSection'; import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects'; import { ProjectPathSelector } from './ProjectPathSelector'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { buildReusableProviderPrepareModelResults, getProviderPrepareCachedSnapshot, type ProviderPrepareDiagnosticsModelResult, runProviderPrepareDiagnostics, } from './providerPrepareDiagnostics'; import { buildProviderPrepareModelChecksSignature, buildProviderPrepareRequestSignature, buildProviderPrepareRuntimeStatusSignature, } from './providerPrepareRequestSignature'; import { getShortLivedProviderPrepareModelResults, storeShortLivedProviderPrepareModelResults, } from './providerPrepareShortLivedCache'; import { getProvisioningModelIssue } from './provisioningModelIssues'; import { deriveEffectiveProvisioningPrepareState, failIncompleteProviderChecks, getPrimaryProvisioningFailureDetail, getProvisioningFailureHint, getProvisioningProviderBackendSummary, type ProvisioningProviderCheck, ProvisioningProviderStatusList, shouldHideProvisioningProviderStatusList, updateProviderCheck, } from './ProvisioningProviderStatusList'; import { analyzeTeammateRuntimeCompatibility, useTmuxRuntimeReadiness, } from './teammateRuntimeCompatibility'; import { TeammateRuntimeCompatibilityNotice } from './TeammateRuntimeCompatibilityNotice'; import { computeEffectiveTeamModel, formatTeamModelSummary, OPENCODE_ONE_SHOT_DISABLED_BADGE_LABEL, OPENCODE_ONE_SHOT_DISABLED_REASON, TeamModelSelector, } from './TeamModelSelector'; import { getWorktreeGitBlockingMessage, getWorktreeGitControlDisabledReason, useWorktreeGitReadiness, WorktreeGitReadinessBanner, } from './WorktreeGitReadinessBanner'; import type { ActiveTeamRef } from './CreateTeamDialog'; import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { CreateScheduleInput, EffortLevel, ResolvedTeamMember, Schedule, ScheduleLaunchConfig, TeamCreateRequest, TeamFastMode, TeamLaunchRequest, TeamProviderId, UpdateSchedulePatch, } from '@shared/types'; 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: [], } ); } // ============================================================================= // Props — discriminated union // ============================================================================= interface LaunchDialogBase { open: boolean; teamName: string; onClose: () => void; } export type TeamLaunchDialogMode = 'launch' | 'relaunch'; interface LaunchDialogLaunchMode extends LaunchDialogBase { mode: 'launch'; members: ResolvedTeamMember[]; defaultProjectPath?: string; provisioningError: string | null; clearProvisioningError?: (teamName?: string) => void; activeTeams?: ActiveTeamRef[]; onLaunch: (request: TeamLaunchRequest) => Promise; } interface LaunchDialogRelaunchMode extends LaunchDialogBase { mode: 'relaunch'; members: ResolvedTeamMember[]; defaultProjectPath?: string; provisioningError: string | null; clearProvisioningError?: (teamName?: string) => void; activeTeams?: ActiveTeamRef[]; onRelaunch: (request: TeamLaunchRequest, members: TeamCreateRequest['members']) => Promise; } interface LaunchDialogScheduleMode { mode: 'schedule'; open: boolean; /** Team name — optional when creating from standalone schedules page */ teamName?: string; onClose: () => void; /** When provided → edit mode; null/undefined → create mode */ schedule?: Schedule | null; } export type LaunchTeamDialogProps = | LaunchDialogLaunchMode | LaunchDialogRelaunchMode | LaunchDialogScheduleMode; const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate'; // ============================================================================= // Helpers // ============================================================================= function getLocalTimezone(): string { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return 'UTC'; } } function getStoredTeamProvider(): TeamProviderId { const stored = localStorage.getItem('team:lastSelectedProvider'); return normalizeCreateLaunchProviderForUi(normalizeOptionalTeamProviderId(stored), true); } function normalizeOneShotProviderForMode( providerId: TeamProviderId | undefined, multimodelEnabled: boolean ): TeamProviderId { const normalizedProviderId = normalizeProviderForMode(providerId, multimodelEnabled); return normalizedProviderId === 'opencode' ? 'anthropic' : normalizedProviderId; } function getStoredTeamModel(providerId: TeamProviderId): string { const stored = localStorage.getItem(`team:lastSelectedModel:${providerId}`); if (stored === null) { return providerId === 'anthropic' ? 'opus' : ''; } return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored); } function getStoredTeamFastMode(): TeamFastMode { const stored = localStorage.getItem('team:lastSelectedFastMode'); return stored === 'on' || stored === 'off' || stored === 'inherit' ? stored : 'inherit'; } function getProviderLabel(providerId: TeamProviderId): string { return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic'; } function resolveMemberDraftRuntime( member: Pick, inheritedProviderId: TeamProviderId, inheritedModel: string, inheritedEffort: EffortLevel | undefined ): { providerId: TeamProviderId; model: string; effort: EffortLevel | undefined } { return { providerId: member.providerId ?? inheritedProviderId, model: member.model?.trim() || inheritedModel, effort: member.effort ?? inheritedEffort, }; } function resolveResolvedMemberRuntime( member: Pick, inheritedProviderId: TeamProviderId, inheritedModel: string, inheritedEffort: EffortLevel | undefined ): { providerId: TeamProviderId; model: string; effort: EffortLevel | undefined } { return { providerId: normalizeOptionalTeamProviderId(member.providerId) ?? inheritedProviderId, model: member.model?.trim() || inheritedModel, effort: member.effort ?? inheritedEffort, }; } function deriveTeammateWorktreeDefault( members: readonly { name: string; isolation?: 'worktree'; removedAt?: number | string | null; }[] ): boolean { const activeTeammates = members.filter( (member) => !member.removedAt && member.name.trim().toLowerCase() !== 'team-lead' ); return ( activeTeammates.length > 0 && activeTeammates.every((member) => member.isolation === 'worktree') ); } function buildWorktreePathByMemberName( members: readonly { name: string; isolation?: 'worktree'; cwd?: string; removedAt?: number | string | null; }[] ): Record { const paths: Record = {}; for (const member of members) { const name = member.name.trim().toLowerCase(); const cwd = member.cwd?.trim(); if (!name || member.removedAt || member.isolation !== 'worktree' || !cwd) { continue; } paths[name] = cwd; } return paths; } // ============================================================================= // Component // ============================================================================= export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Element => { const { open, onClose } = props; const { isLight } = useTheme(); const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const anthropicProviderFastModeDefault = useStore( (s) => s.appConfig?.providerConnections?.anthropic.fastModeDefault ?? false ); const cliStatus = useStore((s) => s.cliStatus); const cliStatusLoading = useStore((s) => s.cliStatusLoading); const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus); const fetchCliStatus = useStore((s) => s.fetchCliStatus); const isLaunchMode = props.mode === 'launch' || props.mode === 'relaunch'; const isRelaunch = props.mode === 'relaunch'; 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 isSchedule = props.mode === 'schedule'; const schedule = isSchedule ? (props.schedule ?? null) : null; const isEditing = isSchedule && !!schedule; // Team name: always present for launch mode, may be absent in schedule mode (standalone page) const propsTeamName = props.teamName ?? ''; const [selectedTeamName, setSelectedTeamName] = useState(''); const { teamByName, openDashboard } = useStore( useShallow((s) => ({ teamByName: s.teamByName, openDashboard: s.openDashboard, })) ); const openTeamTab = useStore((s) => s.openTeamTab); const teamOptions = useMemo( () => Object.values(teamByName) .sort((a, b) => a.teamName.localeCompare(b.teamName)) .map((team) => ({ value: team.teamName, label: team.displayName || team.teamName, description: team.description || undefined, meta: { color: team.color }, })), [teamByName] ); // Effective team name: from props if provided, otherwise from local selection const effectiveTeamName = propsTeamName || selectedTeamName; const needsTeamSelector = isSchedule && !propsTeamName; // --------------------------------------------------------------------------- // Shared form state // --------------------------------------------------------------------------- const [cwdMode, setCwdMode] = useState<'project' | 'custom'>('project'); const [selectedProjectPath, setSelectedProjectPath] = useState(''); const [customCwd, setCustomCwd] = useState(''); const promptDraft = useDraftPersistence({ key: `launchTeam:${effectiveTeamName || 'standalone'}:${props.mode}:prompt`, }); const chipDraft = useChipDraftPersistence( `launchTeam:${effectiveTeamName || 'standalone'}:${props.mode}:chips` ); const [projects, setProjects] = useState([]); const [projectsLoading, setProjectsLoading] = useState(false); const [projectsError, setProjectsError] = useState(null); const [localError, setLocalError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [selectedProviderId, setSelectedProviderIdRaw] = useState(() => isLaunchMode ? normalizeLeadProviderForMode(getStoredTeamProvider(), multimodelEnabled) : normalizeOneShotProviderForMode(getStoredTeamProvider(), multimodelEnabled) ); const [selectedModel, setSelectedModelRaw] = useState(() => getStoredTeamModel( isLaunchMode ? normalizeLeadProviderForMode(getStoredTeamProvider(), multimodelEnabled) : normalizeOneShotProviderForMode(getStoredTeamProvider(), multimodelEnabled) ) ); const [membersDrafts, setMembersDrafts] = useState([]); const [teammateWorktreeDefault, setTeammateWorktreeDefault] = useState(false); const [syncModelsWithLead, setSyncModelsWithLead] = useState(false); const [skipPermissions, setSkipPermissionsRaw] = useState( () => localStorage.getItem('team:lastSkipPermissions') !== 'false' ); const [selectedEffort, setSelectedEffortRaw] = useState(() => { const stored = localStorage.getItem('team:lastSelectedEffort'); return stored === null ? 'medium' : stored; }); const [selectedFastMode, setSelectedFastModeRaw] = useState(getStoredTeamFastMode); const [anthropicRuntimeNotice, setAnthropicRuntimeNotice] = useState(null); // --------------------------------------------------------------------------- // Launch-only state // --------------------------------------------------------------------------- const [limitContext, setLimitContextRaw] = useState( () => localStorage.getItem('team:lastLimitContext') === 'true' ); const [clearContext, setClearContext] = useState(false); const [conflictDismissed, setConflictDismissed] = useState(false); const [prepareState, setPrepareState] = useState<'idle' | 'loading' | 'ready' | 'failed'>('idle'); const [prepareMessage, setPrepareMessage] = useState(null); const [prepareWarnings, setPrepareWarnings] = useState([]); const [prepareChecks, setPrepareChecks] = useState([]); const prepareRequestSeqRef = useRef(0); const appliedDefaultProjectPathRef = useRef(null); const storeMembers = useStore((s) => selectResolvedMembersForTeamName(s, s.selectedTeamName)); const previousLaunchParams = useStore((s) => effectiveTeamName ? s.launchParamsByTeam[effectiveTeamName] : undefined ); const members = isLaunchMode ? props.members : storeMembers; const [savedLaunchProviderId, setSavedLaunchProviderId] = useState(null); const [savedLaunchProviderBackendId, setSavedLaunchProviderBackendId] = useState( null ); // Advanced CLI section state (with localStorage persistence) const [worktreeEnabled, setWorktreeEnabledRaw] = useState( () => localStorage.getItem(`team:lastWorktreeEnabled:${effectiveTeamName}`) === 'true' && Boolean(localStorage.getItem(`team:lastWorktreeName:${effectiveTeamName}`)) ); const [worktreeName, setWorktreeNameRaw] = useState( () => localStorage.getItem(`team:lastWorktreeName:${effectiveTeamName}`) ?? '' ); const [customArgs, setCustomArgsRaw] = useState( () => localStorage.getItem(`team:lastCustomArgs:${effectiveTeamName}`) ?? '' ); // --------------------------------------------------------------------------- // Schedule-only state // --------------------------------------------------------------------------- const [schedLabel, setSchedLabel] = useState(''); const [schedExpanded, setSchedExpanded] = useState(true); const [cronExpression, setCronExpression] = useState('0 9 * * 1-5'); const [timezone, setTimezone] = useState(getLocalTimezone); const [warmUpMinutes, setWarmUpMinutes] = useState(15); const [maxTurns, setMaxTurns] = useState(50); const [maxBudgetUsd, setMaxBudgetUsd] = useState(''); const [scheduleHydrationKey, setScheduleHydrationKey] = useState(null); const [worktreePathByMemberName, setWorktreePathByMemberName] = useState>( {} ); const effectiveMemberDrafts = useMemo( () => (syncModelsWithLead ? membersDrafts.map(clearMemberModelOverrides) : membersDrafts), [membersDrafts, syncModelsWithLead] ); const tmuxRuntime = useTmuxRuntimeReadiness(open && isLaunchMode); const selectedMemberProviders = useMemo( () => !multimodelEnabled ? ['anthropic'] : Array.from( new Set([ selectedProviderId, ...effectiveMemberDrafts.flatMap((member) => !member.removedAt && isTeamProviderId(member.providerId) ? [member.providerId] : [] ), ]) ), [effectiveMemberDrafts, multimodelEnabled, selectedProviderId] ); 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 runtimeBackendSummaryByProviderRef = useRef(runtimeBackendSummaryByProvider); const prepareChecksRef = useRef([]); const prepareModelResultsCacheRef = useRef( new Map>() ); const lastPrepareRequestSignatureRef = useRef(null); useEffect(() => { runtimeBackendSummaryByProviderRef.current = runtimeBackendSummaryByProvider; }, [runtimeBackendSummaryByProvider]); useEffect(() => { prepareChecksRef.current = prepareChecks; }, [prepareChecks]); useEffect(() => { if (!open) { lastPrepareRequestSignatureRef.current = null; } }, [open]); const runtimeProviderStatusById = useMemo( () => new Map( (effectiveCliStatus?.providers ?? []).map( (provider) => [provider.providerId, provider] as const ) ), [effectiveCliStatus?.providers] ); useEffect(() => { if (!open) { return; } setMembersDrafts((prev) => { const sanitized = clearInheritedMemberModelsUnavailableForProvider({ members: prev, selectedProviderId, runtimeProviderStatusById, }); return sanitized.changed ? sanitized.members : prev; }); }, [membersDrafts, open, runtimeProviderStatusById, selectedProviderId]); useEffect(() => { if (multimodelEnabled) { return; } if (selectedProviderId !== 'anthropic') { setSelectedProviderIdRaw('anthropic'); setSelectedModelRaw(getStoredTeamModel('anthropic')); } setMembersDrafts((prev) => { let changed = false; const next = prev.map((member) => { const normalized = normalizeMemberDraftForProviderMode(member, false); if (normalized !== member) changed = true; return normalized; }); return changed ? next : prev; }); }, [multimodelEnabled, selectedProviderId]); useEffect(() => { if (!open || cliStatus || cliStatusLoading) { return; } void refreshCliStatusForCurrentMode({ multimodelEnabled, bootstrapCliStatus, fetchCliStatus, }); }, [bootstrapCliStatus, cliStatus, cliStatusLoading, fetchCliStatus, multimodelEnabled, open]); const handleCodexReconnect = React.useCallback( (mode: 'browser' | 'device_code' = 'browser') => { void (async () => { await codexAccount.startChatgptLogin(mode); })(); }, [codexAccount] ); // Schedule store actions const createSchedule = useStore((s) => s.createSchedule); const updateSchedule = useStore((s) => s.updateSchedule); // --------------------------------------------------------------------------- // localStorage persistence wrappers // --------------------------------------------------------------------------- const setWorktreeEnabled = (value: boolean): void => { setWorktreeEnabledRaw(value); localStorage.setItem(`team:lastWorktreeEnabled:${effectiveTeamName}`, String(value)); if (!value) { setWorktreeNameRaw(''); localStorage.setItem(`team:lastWorktreeName:${effectiveTeamName}`, ''); } }; const setWorktreeName = (value: string): void => { setWorktreeNameRaw(value); localStorage.setItem(`team:lastWorktreeName:${effectiveTeamName}`, value); }; const setCustomArgs = (value: string): void => { setCustomArgsRaw(value); localStorage.setItem(`team:lastCustomArgs:${effectiveTeamName}`, value); }; const setSelectedProviderId = (value: TeamProviderId): void => { const normalizedValue = isLaunchMode ? normalizeLeadProviderForMode(value, multimodelEnabled) : normalizeOneShotProviderForMode(value, multimodelEnabled); setSelectedProviderIdRaw(normalizedValue); localStorage.setItem('team:lastSelectedProvider', normalizedValue); if (normalizedValue !== 'anthropic') { setLimitContextRaw(false); localStorage.setItem('team:lastLimitContext', 'false'); } setSelectedModelRaw(getStoredTeamModel(normalizedValue)); }; const setSelectedModel = (value: string): void => { const normalizedValue = normalizeExplicitTeamModelForUi(selectedProviderId, value); setSelectedModelRaw(normalizedValue); localStorage.setItem(`team:lastSelectedModel:${selectedProviderId}`, normalizedValue); }; const setLimitContext = (value: boolean): void => { setLimitContextRaw(value); localStorage.setItem('team:lastLimitContext', String(value)); }; const setSkipPermissions = (value: boolean): void => { setSkipPermissionsRaw(value); localStorage.setItem('team:lastSkipPermissions', String(value)); }; const setSelectedEffort = (value: string): void => { setSelectedEffortRaw(value); localStorage.setItem('team:lastSelectedEffort', value); }; const setSelectedFastMode = (value: TeamFastMode): void => { setSelectedFastModeRaw(value); localStorage.setItem('team:lastSelectedFastMode', value); }; // --------------------------------------------------------------------------- // localStorage migration: schedule → team namespace (one-time) // --------------------------------------------------------------------------- useEffect(() => { const legacyTeamModel = localStorage.getItem('team:lastSelectedModel'); if ( legacyTeamModel != null && localStorage.getItem('team:lastSelectedModel:anthropic') == null ) { localStorage.setItem('team:lastSelectedModel:anthropic', legacyTeamModel); } localStorage.removeItem('team:lastSelectedModel'); for (const suffix of ['lastSelectedModel', 'lastSelectedEffort']) { const schedKey = `schedule:${suffix}`; const teamKey = suffix === 'lastSelectedModel' ? 'team:lastSelectedModel:anthropic' : `team:${suffix}`; const schedVal = localStorage.getItem(schedKey); if (schedVal != null && localStorage.getItem(teamKey) == null) { localStorage.setItem(teamKey, schedVal); } localStorage.removeItem(schedKey); } }, []); // --------------------------------------------------------------------------- // Form reset / populate // --------------------------------------------------------------------------- const resetFormState = (): void => { setLocalError(null); setIsSubmitting(false); setPrepareState('idle'); setPrepareMessage(null); setPrepareWarnings([]); setPrepareChecks([]); setCwdMode('project'); setSelectedProjectPath(''); setCustomCwd(''); setClearContext(false); setConflictDismissed(false); setMembersDrafts([]); setSyncModelsWithLead(false); chipDraft.clearChipDraft(); // Schedule fields setSelectedTeamName(''); setSchedLabel(''); setCronExpression('0 9 * * 1-5'); setTimezone(getLocalTimezone()); setWarmUpMinutes(15); setMaxTurns(50); setMaxBudgetUsd(''); }; const closeDialog = (): void => { if (isLaunchMode) { resetFormState(); } onClose(); }; // Populate form in schedule edit mode useEffect(() => { if (!open || !isSchedule) return; if (schedule) { // Edit mode — populate from existing schedule setSchedLabel(schedule.label ?? ''); setCronExpression(schedule.cronExpression); setTimezone(schedule.timezone); setWarmUpMinutes(schedule.warmUpMinutes); setMaxTurns(schedule.maxTurns); setMaxBudgetUsd(schedule.maxBudgetUsd != null ? String(schedule.maxBudgetUsd) : ''); promptDraft.setValue(schedule.launchConfig.prompt); setCustomCwd(schedule.launchConfig.cwd); setCwdMode('custom'); const scheduleProviderId = normalizeOneShotProviderForMode( schedule.launchConfig.providerId, multimodelEnabled ); const scheduleSourceProviderId = normalizeOptionalTeamProviderId( schedule.launchConfig.providerId ); setSelectedProviderIdRaw(scheduleProviderId); setSelectedModelRaw( scheduleSourceProviderId !== 'gemini' && scheduleSourceProviderId !== 'opencode' && scheduleProviderId === normalizeOneShotProviderForMode(schedule.launchConfig.providerId, true) ? (schedule.launchConfig.model ?? '') : getStoredTeamModel('anthropic') ); setSkipPermissionsRaw(schedule.launchConfig.skipPermissions !== false); setSelectedEffortRaw(schedule.launchConfig.effort ?? ''); setSelectedFastModeRaw(schedule.launchConfig.fastMode ?? getStoredTeamFastMode()); setSavedLaunchProviderBackendId(schedule.launchConfig.providerBackendId ?? null); setScheduleHydrationKey(`${schedule.id}:${schedule.updatedAt ?? ''}`); } else { // Create mode — reset to defaults setSchedLabel(''); setCronExpression('0 9 * * 1-5'); setTimezone(getLocalTimezone()); setWarmUpMinutes(15); setMaxTurns(50); setMaxBudgetUsd(''); promptDraft.setValue(''); setCwdMode('project'); setSelectedProjectPath(''); setCustomCwd(''); const storedProviderId = normalizeOneShotProviderForMode( getStoredTeamProvider(), multimodelEnabled ); setSelectedProviderIdRaw(storedProviderId); setSelectedModelRaw(getStoredTeamModel(storedProviderId)); setSelectedEffortRaw('medium'); setSelectedFastModeRaw(getStoredTeamFastMode()); setSavedLaunchProviderBackendId(null); setScheduleHydrationKey(null); } setLocalError(null); setIsSubmitting(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, isSchedule, schedule?.id]); useEffect(() => { if (!open || !isLaunchMode) return; let cancelled = false; void (async () => { let savedRequest = null; try { savedRequest = effectiveTeamName ? await api.teams.getSavedRequest(effectiveTeamName) : null; } catch { savedRequest = null; } if (cancelled) return; const nextMembersSource = members.length > 0 ? members : savedRequest?.members && savedRequest.members.length > 0 ? savedRequest.members : []; const editableMembersSource = filterEditableMemberInputs(nextMembersSource); const storedEffort = localStorage.getItem('team:lastSelectedEffort'); const savedProviderId = normalizeOptionalTeamProviderId(savedRequest?.providerId) ?? null; const savedProviderBackendId = typeof savedRequest?.providerBackendId === 'string' && savedRequest.providerBackendId.trim().length > 0 ? savedRequest.providerBackendId.trim() : null; const storedProviderId = normalizeLeadProviderForMode( getStoredTeamProvider(), multimodelEnabled ); const launchPrefill = resolveLaunchDialogPrefill({ members, savedRequest, previousLaunchParams, multimodelEnabled, storedProviderId, storedEffort: storedEffort === null ? 'medium' : storedEffort, storedFastMode: getStoredTeamFastMode(), storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true', getStoredModel: getStoredTeamModel, }); setSavedLaunchProviderId(savedProviderId); setSavedLaunchProviderBackendId( launchPrefill.providerBackendId ?? savedProviderBackendId ?? null ); setMembersDrafts( createMemberDraftsFromInputs(editableMembersSource).map((member) => normalizeMemberDraftForProviderMode(member, multimodelEnabled) ) ); setWorktreePathByMemberName(buildWorktreePathByMemberName(editableMembersSource)); setTeammateWorktreeDefault(deriveTeammateWorktreeDefault(editableMembersSource)); setSyncModelsWithLead( !editableMembersSource.some((member) => member.providerId || member.model || member.effort) ); const leadProviderId = normalizeLeadProviderForMode( launchPrefill.providerId, multimodelEnabled ); setSelectedProviderIdRaw(leadProviderId); setSelectedModelRaw(leadProviderId === launchPrefill.providerId ? launchPrefill.model : ''); setSelectedEffortRaw(launchPrefill.effort); setSelectedFastModeRaw(launchPrefill.fastMode); setLimitContextRaw(launchPrefill.limitContext); setSkipPermissionsRaw( savedRequest?.skipPermissions ?? localStorage.getItem('team:lastSkipPermissions') !== 'false' ); })(); return () => { cancelled = true; }; }, [open, isLaunchMode, effectiveTeamName, members, multimodelEnabled, previousLaunchParams]); const previousProviderId = useMemo(() => { if (!isLaunchMode) { return null; } return ( normalizeOptionalTeamProviderId(previousLaunchParams?.providerId) ?? savedLaunchProviderId ); }, [isLaunchMode, previousLaunchParams?.providerId, savedLaunchProviderId]); const providerChangeForcesFreshLeadContext = useMemo(() => { if (!isLaunchMode || !previousProviderId) { return false; } return previousProviderId !== selectedProviderId; }, [isLaunchMode, previousProviderId, selectedProviderId]); const effectiveAnthropicRuntimeLimitContext = isSchedule ? false : limitContext; const effectiveLeadRuntimeModel = useMemo( () => computeEffectiveTeamModel( selectedModel, limitContext, selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ) ?? '', [limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId] ); const selectedProviderBackendId = useMemo( () => resolveUiOwnedProviderBackendId( selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ) ?? migrateProviderBackendId( selectedProviderId, previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId ) ?? undefined, [ previousLaunchParams?.providerBackendId, runtimeProviderStatusById, savedLaunchProviderBackendId, selectedProviderId, ] ); const teammateRuntimeCompatibility = useMemo( () => analyzeTeammateRuntimeCompatibility({ leadProviderId: selectedProviderId, leadProviderBackendId: selectedProviderBackendId, members: isLaunchMode ? effectiveMemberDrafts : [], extraCliArgs: isLaunchMode ? customArgs : undefined, tmuxStatus: tmuxRuntime.status, tmuxStatusLoading: tmuxRuntime.loading, tmuxStatusError: tmuxRuntime.error, }), [ customArgs, effectiveMemberDrafts, isLaunchMode, selectedProviderBackendId, selectedProviderId, tmuxRuntime.error, tmuxRuntime.loading, tmuxRuntime.status, ] ); 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')) ?? migrateProviderBackendId( 'codex', previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId ) ?? undefined, }, selectedModel, }) : null, [ previousLaunchParams?.providerBackendId, runtimeProviderStatusById, savedLaunchProviderBackendId, selectedModel, selectedProviderId, ] ); const codexFastModeResolution = useMemo( () => selectedProviderId === 'codex' && codexRuntimeSelection ? resolveCodexFastMode({ selection: codexRuntimeSelection, selectedFastMode, }) : null, [codexRuntimeSelection, selectedFastMode, selectedProviderId] ); useEffect(() => { if (isSchedule && schedule) { const nextHydrationKey = `${schedule.id}:${schedule.updatedAt ?? ''}`; if (scheduleHydrationKey !== nextHydrationKey) { return; } } 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, selectedFastMode, providerFastModeDefault: anthropicProviderFastModeDefault, }) : { nextEffort: selectedEffort, effortResetReason: null, ...reconcileCodexRuntimeSelections({ selection: codexRuntimeSelection ?? resolveCodexRuntimeSelection({ source: { providerStatus: runtimeProviderStatusById.get('codex'), providerBackendId: resolveUiOwnedProviderBackendId( 'codex', runtimeProviderStatusById.get('codex') ) ?? migrateProviderBackendId( 'codex', previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId ) ?? undefined, }, selectedModel, }), selectedFastMode, }), }; const notices: string[] = []; if (reconciliation.nextEffort !== selectedEffort) { setSelectedEffortRaw(reconciliation.nextEffort); localStorage.setItem('team:lastSelectedEffort', reconciliation.nextEffort); if (reconciliation.effortResetReason) { notices.push(reconciliation.effortResetReason); } } if (reconciliation.nextFastMode !== selectedFastMode) { setSelectedFastModeRaw(reconciliation.nextFastMode); localStorage.setItem('team:lastSelectedFastMode', reconciliation.nextFastMode); if (reconciliation.fastModeResetReason) { notices.push(reconciliation.fastModeResetReason); } } setAnthropicRuntimeNotice(notices.length > 0 ? notices.join(' ') : null); }, [ anthropicProviderFastModeDefault, anthropicRuntimeSelection, codexRuntimeSelection, effectiveAnthropicRuntimeLimitContext, previousLaunchParams?.providerBackendId, runtimeProviderStatusById, savedLaunchProviderBackendId, selectedEffort, selectedFastMode, selectedModel, selectedProviderId, schedule, scheduleHydrationKey, isSchedule, ]); const selectedModelChecksByProvider = useMemo(() => { const modelsByProvider = new Map(); const defaultSelectionByProvider = new Map(); const addModel = (providerId: TeamProviderId, model: string | undefined): void => { const trimmed = model?.trim() ?? ''; if (!trimmed) { return; } const existing = modelsByProvider.get(providerId) ?? []; if (!existing.includes(trimmed)) { modelsByProvider.set(providerId, [...existing, trimmed]); } }; const addDefaultSelection = (providerId: TeamProviderId): void => { if ( providerId === 'codex' || providerId === 'gemini' || (providerId === 'anthropic' && selectedProviderId === 'anthropic') ) { defaultSelectionByProvider.set(providerId, true); } }; if (selectedModel.trim()) { addModel(selectedProviderId, effectiveLeadRuntimeModel); } else { addDefaultSelection(selectedProviderId); } for (const member of effectiveMemberDrafts) { if (member.removedAt) { continue; } const scopedModel = resolveProviderScopedMemberModel({ memberProviderId: member.providerId, memberModel: member.model, selectedProviderId, runtimeProviderStatusById, }); if (scopedModel.model) { addModel(scopedModel.providerId, scopedModel.model); } else { addDefaultSelection(scopedModel.providerId); } } for (const providerId of defaultSelectionByProvider.keys()) { addModel(providerId, DEFAULT_PROVIDER_MODEL_SELECTION); } return modelsByProvider; }, [ effectiveLeadRuntimeModel, effectiveMemberDrafts, runtimeProviderStatusById, selectedModel, selectedProviderId, ]); const runtimeChangeNotes = useMemo(() => { if (!isLaunchMode) { return [] as { key: string; memberName: string; message: string }[]; } const notes: { key: string; memberName: string; message: string }[] = []; const previousLeadModel = previousLaunchParams?.model?.trim() || ''; const previousLeadEffort = previousLaunchParams?.effort; const currentLeadDisplayModel = selectedModel.trim() || effectiveLeadRuntimeModel; if ( previousProviderId && (previousProviderId !== selectedProviderId || previousLeadModel !== currentLeadDisplayModel || (previousLeadEffort ?? '') !== ((selectedEffort as EffortLevel | '') || '')) ) { notes.push({ key: 'lead', memberName: 'lead', message: `${formatTeamModelSummary( selectedProviderId, currentLeadDisplayModel, (selectedEffort as EffortLevel) || undefined )} instead of ${formatTeamModelSummary( previousProviderId, previousLeadModel, previousLeadEffort )}`, }); } const previousMembersByName = new Map( members.map((member) => [member.name.trim().toLowerCase(), member] as const) ); for (const member of effectiveMemberDrafts) { if (member.removedAt) { continue; } const name = member.name.trim(); if (!name) { continue; } const previousMember = previousMembersByName.get(name.toLowerCase()); if (!previousMember) { continue; } const { providerId: currentProviderId, model: currentModel, effort: currentEffort, } = resolveMemberDraftRuntime( member, selectedProviderId, currentLeadDisplayModel, (selectedEffort as EffortLevel) || undefined ); const { providerId: previousProvider, model: previousModel, effort: previousEffort, } = resolveResolvedMemberRuntime( previousMember, previousProviderId ?? 'anthropic', previousLeadModel, previousLeadEffort ); if ( previousProvider === currentProviderId && previousModel === currentModel && (previousEffort ?? '') === (currentEffort ?? '') && (previousMember.isolation ?? '') === (member.isolation ?? '') ) { continue; } const runtimeMessage = previousProvider !== currentProviderId || previousModel !== currentModel || (previousEffort ?? '') !== (currentEffort ?? '') ? `${formatTeamModelSummary( currentProviderId, currentModel, currentEffort )} instead of ${formatTeamModelSummary(previousProvider, previousModel, previousEffort)}` : null; const isolationMessage = previousMember.isolation !== member.isolation ? `${member.isolation === 'worktree' ? 'separate worktree' : 'shared workspace'} instead of ${ previousMember.isolation === 'worktree' ? 'separate worktree' : 'shared workspace' }` : null; notes.push({ key: `member:${name.toLowerCase()}`, memberName: name, message: [runtimeMessage, isolationMessage] .filter((part): part is string => Boolean(part)) .join('; '), }); } return notes; }, [ isLaunchMode, previousLaunchParams?.effort, previousLaunchParams?.model, previousProviderId, selectedProviderId, selectedModel, effectiveLeadRuntimeModel, selectedEffort, members, effectiveMemberDrafts, ]); const runtimeChangeNoteByKey = useMemo( () => new Map(runtimeChangeNotes.map((note) => [note.key, note.message] as const)), [runtimeChangeNotes] ); const leadRuntimeWarningText = useMemo(() => { const parts: string[] = []; if (providerChangeForcesFreshLeadContext && previousProviderId) { parts.push( `Provider changed from ${getProviderLabel(previousProviderId)} to ${getProviderLabel(selectedProviderId)}. The previous lead session will not be resumed and lead will start with a fresh context.` ); } const runtimeChange = runtimeChangeNoteByKey.get('lead'); if (runtimeChange) { parts.push(`Next launch will use ${runtimeChange}.`); } return parts.length > 0 ? parts.join(' ') : null; }, [ providerChangeForcesFreshLeadContext, previousProviderId, selectedProviderId, runtimeChangeNoteByKey, ]); const memberRuntimeWarningById = useMemo(() => { const warnings: Record = {}; for (const member of effectiveMemberDrafts) { const name = member.name.trim(); if (!name || member.removedAt) { continue; } const note = runtimeChangeNoteByKey.get(`member:${name.toLowerCase()}`); if (note) { warnings[member.id] = `Next launch will use ${note}.`; } } return warnings; }, [effectiveMemberDrafts, runtimeChangeNoteByKey]); const combinedMemberRuntimeWarningById = useMemo(() => { const warnings: Record = { ...memberRuntimeWarningById }; for (const [memberId, warning] of Object.entries( teammateRuntimeCompatibility.memberWarningById )) { warnings[memberId] = warnings[memberId] ? `${warnings[memberId]} ${warning}` : warning; } return warnings; }, [memberRuntimeWarningById, teammateRuntimeCompatibility.memberWarningById]); const memberWorktreeContinuationInfoById = useMemo(() => { if (!isLaunchMode) { return {}; } const info: Record = {}; for (const member of effectiveMemberDrafts) { if (member.removedAt || member.isolation !== 'worktree') { continue; } const lookupName = (member.originalName?.trim() || member.name.trim()).toLowerCase(); if (!lookupName) { continue; } const previousWorktreePath = worktreePathByMemberName[lookupName]; if (!previousWorktreePath) { continue; } info[member.id] = `This teammate will continue from its existing worktree: ${previousWorktreePath}`; } return info; }, [effectiveMemberDrafts, isLaunchMode, worktreePathByMemberName]); // --------------------------------------------------------------------------- // Launch-only effects // --------------------------------------------------------------------------- const selectedProjectCwd = isEphemeralProjectPath(selectedProjectPath) ? '' : selectedProjectPath.trim(); const effectiveCwd = cwdMode === 'project' ? selectedProjectCwd : customCwd.trim(); const hasSelectedWorktreeIsolation = isLaunchMode && effectiveMemberDrafts.some((member) => !member.removedAt && member.isolation === 'worktree'); const worktreeGitReadiness = useWorktreeGitReadiness( effectiveCwd || null, open && hasSelectedWorktreeIsolation ); const worktreeIsolationDisabledReason = isLaunchMode ? getWorktreeGitControlDisabledReason(worktreeGitReadiness) : null; const worktreeGitBlockingMessage = getWorktreeGitBlockingMessage( worktreeGitReadiness, hasSelectedWorktreeIsolation ); const prepareRuntimeStatusSignature = useMemo( () => buildProviderPrepareRuntimeStatusSignature( selectedMemberProviders, runtimeProviderStatusById ), [runtimeProviderStatusById, selectedMemberProviders] ); const selectedModelChecksByProviderSignature = useMemo( () => buildProviderPrepareModelChecksSignature(selectedModelChecksByProvider), [selectedModelChecksByProvider] ); const prepareRequestSignature = useMemo( () => buildProviderPrepareRequestSignature({ cwd: effectiveCwd, selectedProviderId, selectedModel, selectedMemberProviders, limitContext, runtimeStatusSignature: prepareRuntimeStatusSignature, modelChecksSignature: selectedModelChecksByProviderSignature, }), [ effectiveCwd, limitContext, prepareRuntimeStatusSignature, selectedMemberProviders, selectedModel, selectedModelChecksByProviderSignature, selectedProviderId, ] ); // Clear stale provisioning error when dialog opens useEffect(() => { if (!open || !isLaunchMode) return; props.clearProvisioningError?.(effectiveTeamName); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, isLaunchMode, effectiveTeamName]); // Warm up CLI for the currently selected working directory (launch mode only). useEffect(() => { if (!open || !isLaunchMode) { prepareRequestSeqRef.current += 1; lastPrepareRequestSignatureRef.current = null; return; } if (typeof api.teams.prepareProvisioning !== 'function') { prepareRequestSeqRef.current += 1; lastPrepareRequestSignatureRef.current = null; setPrepareState('failed'); setPrepareWarnings([]); setPrepareChecks([]); setPrepareMessage( 'Current preload version does not support team:prepareProvisioning. Restart the dev app.' ); return; } if (!effectiveCwd) { prepareRequestSeqRef.current += 1; lastPrepareRequestSignatureRef.current = null; setPrepareState('idle'); setPrepareWarnings([]); setPrepareChecks([]); setPrepareMessage('Select a working directory to validate the launch environment.'); return; } if (lastPrepareRequestSignatureRef.current === prepareRequestSignature) { return; } lastPrepareRequestSignatureRef.current = prepareRequestSignature; const requestSeq = ++prepareRequestSeqRef.current; const initialChecks = alignProvisioningChecks( prepareChecksRef.current, selectedMemberProviders ); setPrepareState('loading'); setPrepareMessage('Checking selected providers in parallel...'); setPrepareWarnings([]); setPrepareChecks(initialChecks); void (async () => { let checks = initialChecks; const providerPlans = selectedMemberProviders.map((providerId) => { const selectedModelChecks = selectedModelChecksByProvider.get(providerId) ?? []; const backendSummary = runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null; const cacheKey = buildProviderPrepareModelCacheKey({ cwd: effectiveCwd, providerId, backendSummary, limitContext, runtimeStatusSignature: prepareRuntimeStatusSignature, }); const cachedModelResultsById = { ...getShortLivedProviderPrepareModelResults({ providerId, cacheKey, }), ...(prepareModelResultsCacheRef.current.get(cacheKey) ?? {}), }; const cachedSnapshot = getProviderPrepareCachedSnapshot({ providerId, selectedModelIds: selectedModelChecks, cachedModelResultsById, }); return { providerId, selectedModelChecks, backendSummary, cacheKey, cachedModelResultsById, cachedSnapshot, }; }); try { for (const plan of providerPlans) { checks = updateProviderCheck(checks, plan.providerId, { status: plan.selectedModelChecks.length > 0 ? plan.cachedSnapshot.status : 'checking', backendSummary: plan.backendSummary, details: plan.cachedSnapshot.details, }); } if (prepareRequestSeqRef.current === requestSeq) { setPrepareChecks(checks); } const providerResults = await Promise.all( providerPlans.map(async (plan) => { const prepResult = await runProviderPrepareDiagnostics({ cwd: effectiveCwd, providerId: plan.providerId, selectedModelIds: plan.selectedModelChecks, prepareProvisioning: api.teams.prepareProvisioning, limitContext, cachedModelResultsById: plan.cachedModelResultsById, onModelProgress: ({ status, details }) => { checks = updateProviderCheck(checks, plan.providerId, { status, backendSummary: plan.backendSummary, details, }); if (prepareRequestSeqRef.current === requestSeq) { setPrepareChecks(checks); } }, }); return { ...plan, prepResult }; }) ); let anyFailure = false; let anyNotes = false; const collectedWarnings: string[] = []; for (const plan of providerResults) { if (plan.prepResult.warnings.length > 0) { anyNotes = true; collectedWarnings.push( ...plan.prepResult.warnings.map( (warning) => `${getProviderLabel(plan.providerId)}: ${warning}` ) ); } if (plan.prepResult.status === 'failed') { anyFailure = true; } else if (plan.prepResult.status === 'notes') { anyNotes = true; } if (prepareRequestSeqRef.current === requestSeq) { const reusableModelResults = buildReusableProviderPrepareModelResults( plan.prepResult.modelResultsById ); prepareModelResultsCacheRef.current.set(plan.cacheKey, reusableModelResults); storeShortLivedProviderPrepareModelResults({ providerId: plan.providerId, cacheKey: plan.cacheKey, modelResultsById: plan.prepResult.modelResultsById, }); } checks = updateProviderCheck(checks, plan.providerId, { status: plan.prepResult.status, backendSummary: plan.backendSummary, details: plan.prepResult.details, }); } if (prepareRequestSeqRef.current === requestSeq) { setPrepareChecks(checks); } if (prepareRequestSeqRef.current !== requestSeq) return; const failureMessage = getPrimaryProvisioningFailureDetail(checks) ?? 'Some selected providers need attention.'; setPrepareState(anyFailure ? 'failed' : 'ready'); setPrepareMessage( anyFailure ? failureMessage : anyNotes ? 'Selected providers are ready with notes.' : 'Selected providers are ready.' ); setPrepareWarnings(collectedWarnings); } catch (error) { if (prepareRequestSeqRef.current !== requestSeq) return; const failureMessage = error instanceof Error ? error.message : 'Failed to warm up Claude CLI environment'; setPrepareState('failed'); setPrepareWarnings([]); setPrepareChecks(failIncompleteProviderChecks(checks, failureMessage)); setPrepareMessage(failureMessage); } })(); }, [ open, isLaunchMode, effectiveCwd, prepareRequestSignature, selectedProviderId, selectedMemberProviders, selectedModelChecksByProvider, ]); // --------------------------------------------------------------------------- // Shared effects: projects // --------------------------------------------------------------------------- const repositoryGroups = useStore(useShallow((s) => s.repositoryGroups)); const defaultProjectPath = isLaunchMode ? props.defaultProjectPath : undefined; useEffect(() => { if (!open) return; setProjectsLoading(true); setProjectsError(null); let cancelled = false; void (async () => { try { const nextProjects = await loadProjectPathProjects({ defaultProjectPath, repositoryGroups, }); 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, repositoryGroups, defaultProjectPath]); // Pre-select defaultProjectPath (launch mode) or first project useEffect(() => { if (!open) { appliedDefaultProjectPathRef.current = null; return; } if (cwdMode !== 'project') return; const selectableProjects = projects.filter((project) => !isEphemeralProjectPath(project.path)); 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]); useEffect(() => { if (!open || cwdMode !== 'project' || !selectedProjectPath) { return; } if (!isEphemeralProjectPath(selectedProjectPath)) { return; } setSelectedProjectPath(''); }, [open, cwdMode, selectedProjectPath, setSelectedProjectPath]); // Pre-warm file list cache so @-mention file search is instant useFileListCacheWarmer(effectiveCwd || null); // --------------------------------------------------------------------------- // Launch-only: conflict detection // --------------------------------------------------------------------------- const activeTeams = isLaunchMode ? props.activeTeams : undefined; const conflictingTeam = useMemo(() => { if (!isLaunchMode || !activeTeams?.length || !effectiveCwd) return null; const norm = normalizePath(effectiveCwd); return ( activeTeams.find( (t) => t.teamName !== effectiveTeamName && normalizePath(t.projectPath) === norm ) ?? null ); }, [isLaunchMode, activeTeams, effectiveCwd, effectiveTeamName]); useEffect(() => { setConflictDismissed(false); }, [conflictingTeam?.teamName, effectiveCwd]); // --------------------------------------------------------------------------- // Mention suggestions (shared — from props in launch, from store in schedule) // --------------------------------------------------------------------------- const { suggestions: taskSuggestions } = useTaskSuggestions(null); const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null); const memberColorMap = useMemo( () => buildMemberDraftColorMap(membersDrafts, members), [membersDrafts, members] ); const mentionSuggestions = useMemo( () => buildMemberDraftSuggestions(membersDrafts, memberColorMap), [memberColorMap, membersDrafts] ); // --------------------------------------------------------------------------- // Launch-only: internal args preview // --------------------------------------------------------------------------- const internalArgs = useMemo(() => { if (!isLaunchMode) return []; 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'); const model = computeEffectiveTeamModel( selectedModel, limitContext, selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ); if (model) args.push('--model', model); const effectiveEffort = selectedProviderId === 'anthropic' ? selectedEffort || anthropicRuntimeSelection?.defaultEffort || '' : selectedEffort; if (effectiveEffort) args.push('--effort', effectiveEffort); if (selectedProviderId === 'anthropic') { const fastSettings = anthropicFastModeResolution?.resolvedFastMode ? { fastMode: true, fastModePerSessionOptIn: false } : { fastMode: false }; args.push('--settings', JSON.stringify(fastSettings)); } else if (selectedProviderId === 'codex') { args.push(...buildCodexFastModeArgs(codexFastModeResolution?.resolvedFastMode)); } if (!clearContext) args.push('--resume', ''); return args; }, [ anthropicFastModeResolution?.resolvedFastMode, anthropicRuntimeSelection?.defaultEffort, codexFastModeResolution?.resolvedFastMode, isLaunchMode, skipPermissions, selectedModel, limitContext, selectedEffort, selectedProviderId, clearContext, runtimeProviderStatusById, ]); const launchOptionalSummary = useMemo(() => { if (!isLaunchMode) return []; const summary: string[] = []; if (promptDraft.value.trim()) summary.push('Lead prompt'); const worktreeMemberCount = effectiveMemberDrafts.filter( (member) => !member.removedAt && member.isolation === 'worktree' ).length; if (worktreeMemberCount > 0) { summary.push( `${worktreeMemberCount} teammate worktree${worktreeMemberCount === 1 ? '' : 's'}` ); } summary.push(`Provider: ${getProviderLabel(selectedProviderId)}`); if (selectedModel) summary.push(`Model: ${selectedModel}`); if (selectedEffort) summary.push(`Effort: ${selectedEffort}`); 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 (selectedProviderId === 'anthropic' && limitContext) summary.push('Limited to 200K context'); if (skipPermissions) summary.push('Auto-approve tools'); if (clearContext) summary.push('Fresh session'); if (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`); if (customArgs.trim()) summary.push('Custom CLI args'); return summary; }, [ isLaunchMode, effectiveMemberDrafts, promptDraft.value, selectedModel, selectedProviderId, selectedEffort, selectedFastMode, anthropicProviderFastModeDefault, limitContext, skipPermissions, clearContext, worktreeEnabled, worktreeName, customArgs, ]); // --------------------------------------------------------------------------- // Validation // --------------------------------------------------------------------------- const validationErrors = useMemo(() => { const errors: string[] = []; if (!effectiveCwd) errors.push('Working directory is required'); if (worktreeGitBlockingMessage) errors.push(worktreeGitBlockingMessage); if (isSchedule) { if (!effectiveTeamName) errors.push('Team is required'); if (!promptDraft.value.trim()) errors.push('Prompt is required'); if (!cronExpression.trim()) errors.push('Cron expression is required'); } return errors; }, [ effectiveCwd, worktreeGitBlockingMessage, isSchedule, effectiveTeamName, promptDraft.value, cronExpression, ]); const modelValidationError = useMemo(() => { if (isLaunchMode && selectedProviderId === 'opencode') { if (!selectedModel.trim()) { return 'OpenCode lead requires a selected model.'; } const activeMemberCount = 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; } if (!isLaunchMode) { return null; } 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, isLaunchMode, runtimeProviderStatusById, selectedModel, selectedProviderId, ]); const leadModelIssueText = useMemo(() => { const issue = getProvisioningModelIssue( prepareChecks, selectedProviderId, effectiveLeadRuntimeModel || selectedModel ); return issue?.reason ?? issue?.detail ?? null; }, [effectiveLeadRuntimeModel, prepareChecks, selectedModel, selectedProviderId]); const memberModelIssueById = useMemo(() => { const next: Record = {}; if (!isLaunchMode) { return next; } 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, isLaunchMode, leadModelIssueText, prepareChecks, selectedProviderId, syncModelsWithLead, ]); const hasInvalidLaunchMemberNames = useMemo( () => isLaunchMode && membersDrafts.some( (member) => !member.name.trim() || validateMemberNameInline(member.name.trim()) !== null ), [isLaunchMode, membersDrafts] ); const hasDuplicateLaunchMemberNames = useMemo(() => { if (!isLaunchMode) return false; const activeNames = membersDrafts .map((member) => member.name.trim().toLowerCase()) .filter(Boolean); return new Set(activeNames).size !== activeNames.length; }, [isLaunchMode, membersDrafts]); // --------------------------------------------------------------------------- // Error // --------------------------------------------------------------------------- const provisioningError = isLaunchMode ? props.provisioningError : null; const activeError = localError ?? modelValidationError ?? provisioningError; 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 launchInFlight = useStore((s) => isLaunchMode && effectiveTeamName ? isTeamProvisioningActive(s, effectiveTeamName) : false ); useEffect(() => { if (!open || !isLaunchMode || !effectiveTeamName || !launchInFlight) { return; } openTeamTab(effectiveTeamName, effectiveCwd || defaultProjectPath); closeDialog(); }, [ closeDialog, defaultProjectPath, effectiveCwd, effectiveTeamName, isLaunchMode, launchInFlight, open, openTeamTab, ]); // --------------------------------------------------------------------------- // Submit // --------------------------------------------------------------------------- const handleSubmit = (): void => { if (validationErrors.length > 0) { setLocalError(validationErrors[0]); return; } if (modelValidationError) { setLocalError(modelValidationError); return; } if (isLaunchMode && teammateRuntimeCompatibility.blocksSubmission) { setLocalError(teammateRuntimeCompatibility.message); return; } if (isLaunchMode && !effectiveCwd) { setLocalError('Select working directory (cwd)'); return; } if ( isLaunchMode && membersDrafts.some( (member) => !member.name.trim() || validateMemberNameInline(member.name.trim()) !== null ) ) { setLocalError('Fix member names before launch'); return; } if (isLaunchMode) { const activeNames = membersDrafts .map((member) => member.name.trim().toLowerCase()) .filter(Boolean); if (new Set(activeNames).size !== activeNames.length) { setLocalError('Member names must be unique before launch'); return; } } setLocalError(null); setIsSubmitting(true); void (async () => { try { if (isLaunchMode) { const nextMembers = buildMembersFromDrafts(effectiveMemberDrafts); const launchRequest: TeamLaunchRequest = { teamName: effectiveTeamName, cwd: effectiveCwd, prompt: promptDraft.value.trim() || undefined, providerId: selectedProviderId, providerBackendId: resolveUiOwnedProviderBackendId( selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ) ?? selectedProviderBackendId ?? undefined, model: computeEffectiveTeamModel( selectedModel, limitContext, selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ), effort: (selectedEffort as EffortLevel) || undefined, fastMode: selectedProviderId === 'anthropic' || selectedProviderId === 'codex' ? selectedFastMode : undefined, limitContext, clearContext: clearContext || undefined, skipPermissions, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, extraCliArgs: customArgs.trim() || undefined, }; if (isRelaunch) { await props.onRelaunch(launchRequest, nextMembers); } else { await api.teams.replaceMembers(effectiveTeamName, { members: nextMembers, }); await props.onLaunch(launchRequest); } openTeamTab(effectiveTeamName, effectiveCwd || defaultProjectPath); closeDialog(); } else { // Schedule mode: create or update const parsedBudget = maxBudgetUsd ? parseFloat(maxBudgetUsd) : undefined; const scheduleProviderBackendId = resolveUiOwnedProviderBackendId( selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ) ?? selectedProviderBackendId ?? undefined; const scheduleModel = computeEffectiveTeamModel( selectedModel, false, selectedProviderId, runtimeProviderStatusById.get(selectedProviderId) ); const explicitScheduleEffort = selectedEffort ? (selectedEffort as EffortLevel) : undefined; const scheduleEffort = selectedProviderId === 'anthropic' ? (explicitScheduleEffort ?? anthropicRuntimeSelection?.defaultEffort ?? undefined) : explicitScheduleEffort; const launchConfig: ScheduleLaunchConfig = { cwd: effectiveCwd, prompt: promptDraft.value.trim(), providerId: selectedProviderId, providerBackendId: scheduleProviderBackendId, model: scheduleModel, effort: scheduleEffort, fastMode: selectedProviderId === 'anthropic' || selectedProviderId === 'codex' ? selectedFastMode : undefined, resolvedFastMode: selectedProviderId === 'anthropic' ? (anthropicFastModeResolution?.resolvedFastMode ?? false) : selectedProviderId === 'codex' ? (codexFastModeResolution?.resolvedFastMode ?? false) : undefined, skipPermissions, }; if (isEditing && schedule) { const patch: UpdateSchedulePatch = { label: schedLabel.trim() || undefined, cronExpression: cronExpression.trim(), timezone, warmUpMinutes, maxTurns, maxBudgetUsd: parsedBudget, launchConfig, }; await updateSchedule(schedule.id, patch); } else { const input: CreateScheduleInput = { teamName: effectiveTeamName, label: schedLabel.trim() || undefined, cronExpression: cronExpression.trim(), timezone, warmUpMinutes, maxTurns, maxBudgetUsd: parsedBudget, launchConfig, }; await createSchedule(input); } closeDialog(); } } catch (err) { const message = err instanceof Error ? err.message : isSchedule ? 'Failed to save schedule' : isRelaunch ? 'Failed to relaunch team' : 'Failed to launch team'; setLocalError(message); if (isLaunchMode) { console.error( isRelaunch ? 'Failed to relaunch team from dialog:' : 'Failed to launch team from dialog:', err ); } } finally { setIsSubmitting(false); } })(); }; // --------------------------------------------------------------------------- // Disabled state // --------------------------------------------------------------------------- const isDisabled = isLaunchMode ? isSubmitting || launchInFlight || validationErrors.length > 0 || !!modelValidationError || hasInvalidLaunchMemberNames || hasDuplicateLaunchMemberNames || teammateRuntimeCompatibility.blocksSubmission : isSubmitting || validationErrors.length > 0 || !!modelValidationError; // --------------------------------------------------------------------------- // Dynamic labels // --------------------------------------------------------------------------- const dialogTitle = isLaunchMode ? isRelaunch ? 'Relaunch Team' : 'Launch Team' : isEditing ? 'Edit Schedule' : 'Create Schedule'; const dialogDescription = isLaunchMode ? ( isRelaunch ? ( <> Stop the current run for {effectiveTeamName}{' '} and start it again via local Claude CLI. ) : ( <> Start team {effectiveTeamName} via local Claude CLI. ) ) : isEditing ? ( `Editing schedule for team "${effectiveTeamName}"` ) : effectiveTeamName ? ( `Schedule automatic runs for team "${effectiveTeamName}"` ) : ( 'Schedule automatic Claude task execution' ); const submitLabel = isLaunchMode ? isRelaunch ? 'Relaunch team' : 'Launch team' : isEditing ? 'Save Changes' : 'Create Schedule'; const submittingLabel = isLaunchMode ? isRelaunch ? 'Relaunching...' : 'Launching...' : isEditing ? 'Saving...' : 'Creating...'; // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- return ( { if (!nextOpen) { closeDialog(); } }} > {dialogTitle} {dialogDescription} {isRelaunch ? (

Relaunch will restart the current team run

Saving these settings will stop the current team process, persist the updated roster, and launch the team again with the new runtime.

) : null} {/* Launch-only: Conflict warning */} {isLaunchMode && 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} {isLaunchMode ? ( { closeDialog(); openDashboard(); }} /> ) : null}
{/* ═══════════════════════════════════════════════════════════════════ Schedule-only: Team selector (standalone mode) ═══════════════════════════════════════════════════════════════════ */} {needsTeamSelector ? (
{ const colorName = option.meta?.color as string | undefined; const colorSet = colorName ? getTeamColorSet(colorName) : nameColorSet(option.label); return ( <> {isSelected ? ( ) : ( )}
{isSelected ? ( ) : null}

{option.label}

{option.description ? (

{option.description}

) : null}
); }} />
) : null} {/* ═══════════════════════════════════════════════════════════════════ Schedule-only: Schedule configuration section ═══════════════════════════════════════════════════════════════════ */} {isSchedule ? (
{schedExpanded ? (
{/* Label */}
setSchedLabel(e.target.value)} placeholder="e.g., Daily code review, Nightly tests..." />
{/* Cron + Timezone + Warmup */}
) : null}
) : null} {/* ═══════════════════════════════════════════════════════════════════ Shared: Working directory ═══════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════ Launch: optional settings Schedule: prompt + execution defaults ═══════════════════════════════════════════════════════════════════ */} {isLaunchMode ? (
{selectedProviderId === 'anthropic' ? (
{anthropicRuntimeNotice ? (

{anthropicRuntimeNotice}

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

{anthropicRuntimeNotice}

) : null}
) : null} ) : null } />
Saved ) : null } />
{providerChangeForcesFreshLeadContext ? (

Provider changed from {getProviderLabel(previousProviderId!)} to{' '} {getProviderLabel(selectedProviderId)}. The previous lead session will not be resumed, and the lead will start with fresh context so the new runtime is applied correctly.

) : null}
setClearContext(checked === true)} />
{clearContext && (

The team lead will start a new session without resuming previous context. All accumulated session memory and conversation history will not be available.

)}
) : ( <>
Saved ) : null } />

This prompt will be passed to claude -p for one-shot execution

{selectedProviderId === 'anthropic' ? (
{anthropicRuntimeNotice ? (
{anthropicRuntimeNotice}
) : null}
) : null} {selectedProviderId === 'codex' ? (
{anthropicRuntimeNotice ? (
{anthropicRuntimeNotice}
) : null}
) : null}
)} {/* ═══════════════════════════════════════════════════════════════════ Schedule-only: Execution limits ═══════════════════════════════════════════════════════════════════ */} {isSchedule ? (
setMaxTurns(Math.max(1, parseInt(e.target.value) || 50))} />
setMaxBudgetUsd(e.target.value)} placeholder="No limit" />
) : null}
{/* Error display */} {activeError ? (
{activeError}
) : null} {/* Launch-only: CLI warm-up status */} {isLaunchMode ? (
{effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading' ? ( <>
{effectivePrepare.message ?? (effectivePrepare.state === 'idle' ? 'Warming up CLI environment...' : 'Preparing environment...')}

Pre-flight check to catch errors before{' '} {isRelaunch ? 'relaunch' : 'launch'}

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

{effectivePrepare.message}

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

{warning}

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

CLI environment is not available - {isRelaunch ? 'relaunch' : 'launch'} is blocked

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

Pre-flight check to catch errors before {isRelaunch ? 'relaunch' : 'launch'}

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

{warning}

))}
) : null}

{getProvisioningFailureHint(effectivePrepare.message, prepareChecks)}

{(effectivePrepare.message ?? '').toLowerCase().includes('spawn ') || prepareChecks.some((check) => check.details.some((detail) => detail.toLowerCase().includes('spawn ')) ) ? ( ) : null}
{showCodexReconnectPrompt ? (
handleCodexReconnect('browser')} onDeviceCodeReconnect={() => handleCodexReconnect('device_code')} />
) : null}
) : null}
) : null}
); };