agent-ecosystem/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
777genius e9cebe64ff feat: improve provider status startup hydration
Keep connected provider details visible while refreshes are in flight, restore reusable provider status UI, and separate fast startup summaries from heavier provider hydration. Replace the fixed 30s startup wait with an idle-aware scheduler with a 30s safety cap and cover the Electron timer binding crash.
2026-05-24 00:23:04 +03:00

3231 lines
123 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
reconcileAnthropicRuntimeSelections,
resolveAnthropicFastMode,
resolveAnthropicRuntimeSelection,
} from '@features/anthropic-runtime-profile/renderer';
import {
mergeCodexCliStatusWithSnapshot,
useCodexAccountSnapshot,
} from '@features/codex-account/renderer';
import {
buildCodexFastModeArgs,
reconcileCodexRuntimeSelections,
resolveCodexFastMode,
resolveCodexRuntimeSelection,
} from '@features/codex-runtime-profile/renderer';
import { api } from '@renderer/api';
import { ProviderActivityStatusStrip } from '@renderer/components/common/ProviderActivityStatusStrip';
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 { 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 { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
import {
getTeamModelSelectionError,
normalizeExplicitTeamModelForUi,
} from '@renderer/utils/teamModelAvailability';
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
import { 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,
ExternalLink,
Info,
Loader2,
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 {
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 {
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 {
CliProviderId,
CreateScheduleInput,
EffortLevel,
ResolvedTeamMember,
Schedule,
ScheduleLaunchConfig,
TeamCreateRequest,
TeamFastMode,
TeamLaunchRequest,
TeamProviderId,
TeamProvisioningModelCheckRequest,
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<void>;
}
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<void>;
}
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';
const ANTHROPIC_AGENT_SDK_CREDIT_ARTICLE_URL =
'https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan';
// =============================================================================
// 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<MemberDraft, 'providerId' | 'model' | 'effort'>,
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<ResolvedTeamMember, 'providerId' | 'model' | 'effort'>,
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<string, string> {
const paths: Record<string, string> = {};
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 cliProviderStatusLoading = useStore((s) => s.cliProviderStatusLoading);
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 codexSnapshotPending =
codexAccount.loading &&
Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')) &&
!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<ProjectPathProject[]>([]);
const [projectsLoading, setProjectsLoading] = useState(false);
const [projectsError, setProjectsError] = useState<string | null>(null);
const [localError, setLocalError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedProviderId, setSelectedProviderIdRaw] = useState<TeamProviderId>(() =>
isLaunchMode
? normalizeLeadProviderForMode(getStoredTeamProvider(), multimodelEnabled)
: normalizeOneShotProviderForMode(getStoredTeamProvider(), multimodelEnabled)
);
const [selectedModel, setSelectedModelRaw] = useState(() =>
getStoredTeamModel(
isLaunchMode
? normalizeLeadProviderForMode(getStoredTeamProvider(), multimodelEnabled)
: normalizeOneShotProviderForMode(getStoredTeamProvider(), multimodelEnabled)
)
);
const [membersDrafts, setMembersDrafts] = useState<MemberDraft[]>([]);
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 ? '' : stored;
});
const [selectedFastMode, setSelectedFastModeRaw] = useState<TeamFastMode>(getStoredTeamFastMode);
const [anthropicRuntimeNotice, setAnthropicRuntimeNotice] = useState<string | null>(null);
// ---------------------------------------------------------------------------
// Launch-only state
// ---------------------------------------------------------------------------
const [limitContext, setLimitContextRaw] = useState(
() => localStorage.getItem('team:lastLimitContext') === 'true'
);
const [conflictDismissed, setConflictDismissed] = useState(false);
const [prepareState, setPrepareState] = useState<'idle' | 'loading' | 'ready' | 'failed'>('idle');
const [prepareMessage, setPrepareMessage] = useState<string | null>(null);
const [prepareWarnings, setPrepareWarnings] = useState<string[]>([]);
const [prepareChecks, setPrepareChecks] = useState<ProvisioningProviderCheck[]>([]);
const [prepareProviderInvalidationEpochById, setPrepareProviderInvalidationEpochById] = useState<
Partial<Record<TeamProviderId, number>>
>({});
const [providerSettingsProviderId, setProviderSettingsProviderId] =
useState<TeamProviderId | null>(null);
const prepareRequestSeqRef = useRef(0);
const appliedDefaultProjectPathRef = useRef<string | null>(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<TeamProviderId | null>(null);
const [savedLaunchProviderBackendId, setSavedLaunchProviderBackendId] = useState<string | null>(
null
);
useEffect(() => {
if (!open) {
setProviderSettingsProviderId(null);
}
}, [open]);
// 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<string | null>(null);
const [worktreePathByMemberName, setWorktreePathByMemberName] = useState<Record<string, string>>(
{}
);
const effectiveMemberDrafts = useMemo(
() => (syncModelsWithLead ? membersDrafts.map(clearMemberModelOverrides) : membersDrafts),
[membersDrafts, syncModelsWithLead]
);
const tmuxRuntime = useTmuxRuntimeReadiness(open && isLaunchMode);
const selectedMemberProviders = useMemo<TeamProviderId[]>(
() =>
!multimodelEnabled
? ['anthropic']
: Array.from(
new Set([
selectedProviderId,
...effectiveMemberDrafts.flatMap((member) =>
!member.removedAt && isTeamProviderId(member.providerId) ? [member.providerId] : []
),
])
),
[effectiveMemberDrafts, multimodelEnabled, selectedProviderId]
);
const hasSelectedAnthropicRuntime = isLaunchMode && selectedMemberProviders.includes('anthropic');
const effectiveAnthropicRuntimeLimitContext =
hasSelectedAnthropicRuntime && !isSchedule ? 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<TeamProviderId, string | null>(entries);
}, [effectiveCliStatus?.providers]);
const runtimeBackendSummaryByProviderRef = useRef(runtimeBackendSummaryByProvider);
const prepareChecksRef = useRef<ProvisioningProviderCheck[]>([]);
const prepareMessageRef = useRef<string | null>(null);
const prepareModelResultsCacheRef = useRef(
new Map<string, Record<string, ProviderPrepareDiagnosticsModelResult>>()
);
const lastPrepareProviderSignatureByIdRef = useRef(new Map<TeamProviderId, string>());
const prepareProviderRequestSeqByIdRef = useRef(new Map<TeamProviderId, number>());
const prepareWarningsByProviderIdRef = useRef(new Map<TeamProviderId, string[]>());
useEffect(() => {
runtimeBackendSummaryByProviderRef.current = runtimeBackendSummaryByProvider;
}, [runtimeBackendSummaryByProvider]);
useEffect(() => {
prepareChecksRef.current = prepareChecks;
}, [prepareChecks]);
useEffect(() => {
prepareMessageRef.current = prepareMessage;
}, [prepareMessage]);
const invalidatePrepareProvider = useCallback((providerId: CliProviderId): void => {
if (!isTeamProviderId(providerId)) {
return;
}
lastPrepareProviderSignatureByIdRef.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();
prepareProviderRequestSeqByIdRef.current.clear();
prepareWarningsByProviderIdRef.current.clear();
}
}, [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);
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('');
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('');
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 ? '' : 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<TeamProviderId | null>(() => {
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 effectiveLeadRuntimeModel = useMemo(
() =>
computeEffectiveTeamModel(
selectedModel,
effectiveAnthropicRuntimeLimitContext,
selectedProviderId,
runtimeProviderStatusById.get(selectedProviderId)
) ?? '',
[
effectiveAnthropicRuntimeLimitContext,
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 teammateRuntimeProviderNoticeById:
| Partial<Record<TeamProviderId, React.ReactNode>>
| undefined = teammateRuntimeCompatibility.providerNoticeProviderId
? {
[teammateRuntimeCompatibility.providerNoticeProviderId]: (
<TeammateRuntimeCompatibilityNotice
analysis={teammateRuntimeCompatibility}
onOpenDashboard={() => {
closeDialog();
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')) ??
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]
);
const selectedEffortForCurrentSelection = useMemo(
() =>
getAvailableTeamEffortValue({
providerId: selectedProviderId,
model: selectedModel,
limitContext: effectiveAnthropicRuntimeLimitContext,
providerStatus: runtimeProviderStatusById.get(selectedProviderId),
value: selectedEffort,
}),
[
effectiveAnthropicRuntimeLimitContext,
runtimeProviderStatusById,
selectedEffort,
selectedModel,
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: 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')
) ??
migrateProviderBackendId(
'codex',
previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId
) ??
undefined,
},
selectedModel,
}),
selectedFastMode,
}),
};
const notices: string[] = [];
if (selectedEffortForCurrentSelection !== selectedEffort) {
setSelectedEffortRaw(selectedEffortForCurrentSelection);
localStorage.setItem('team:lastSelectedEffort', selectedEffortForCurrentSelection);
}
if (reconciliation.nextEffort !== selectedEffortForCurrentSelection) {
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,
selectedEffortForCurrentSelection,
selectedFastMode,
selectedModel,
selectedProviderId,
schedule,
scheduleHydrationKey,
isSchedule,
]);
const selectedModelChecksByProvider = useMemo(() => {
const modelsByProvider = new Map<TeamProviderId, TeamProvisioningModelCheckRequest[]>();
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);
}
};
if (selectedModel.trim()) {
addModel(selectedProviderId, effectiveLeadRuntimeModel, 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;
}, [
effectiveLeadRuntimeModel,
effectiveMemberDrafts,
runtimeProviderStatusById,
selectedEffortForCurrentSelection,
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 ?? '') !==
((selectedEffortForCurrentSelection as EffortLevel | '') || ''))
) {
notes.push({
key: 'lead',
memberName: 'lead',
message: `${formatTeamModelSummary(
selectedProviderId,
currentLeadDisplayModel,
(selectedEffortForCurrentSelection 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,
(selectedEffortForCurrentSelection 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,
selectedEffortForCurrentSelection,
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<string, string> = {};
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<string, string> = { ...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<string, string> = {};
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 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 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 selectedModelChecksByProviderSignature = useMemo(
() => buildProviderPrepareModelChecksSignature(selectedModelChecksByProvider),
[selectedModelChecksByProvider]
);
const shortLivedModelIssueReasons = useMemo(() => {
void prepareChecks;
void selectedModelChecksByProviderSignature;
const modelAdvisoryReasonByProvider: Partial<Record<TeamProviderId, Record<string, string>>> =
{};
const modelIssueReasonByProvider: Partial<Record<TeamProviderId, Record<string, string>>> = {};
const modelUnavailableReasonByProvider: Partial<
Record<TeamProviderId, Record<string, string>>
> = {};
if (!isLaunchMode) {
return {
modelAdvisoryReasonByProvider,
modelIssueReasonByProvider,
modelUnavailableReasonByProvider,
};
}
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,
isLaunchMode,
prepareChecks,
runtimeBackendSummaryByProvider,
runtimeProviderStatusById,
selectedModelChecksByProvider,
selectedModelChecksByProviderSignature,
selectedMemberProviders,
]);
// 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;
lastPrepareProviderSignatureByIdRef.current.clear();
prepareProviderRequestSeqByIdRef.current.clear();
prepareWarningsByProviderIdRef.current.clear();
return;
}
if (typeof api.teams.prepareProvisioning !== 'function') {
prepareRequestSeqRef.current += 1;
lastPrepareProviderSignatureByIdRef.current.clear();
prepareProviderRequestSeqByIdRef.current.clear();
prepareWarningsByProviderIdRef.current.clear();
setPrepareState('failed');
setPrepareWarnings([]);
setPrepareChecks([]);
setPrepareMessage(
'Current preload version does not support team:prepareProvisioning. Restart the dev app.'
);
return;
}
if (!effectiveCwd) {
prepareRequestSeqRef.current += 1;
lastPrepareProviderSignatureByIdRef.current.clear();
prepareProviderRequestSeqByIdRef.current.clear();
prepareWarningsByProviderIdRef.current.clear();
setPrepareState('idle');
setPrepareWarnings([]);
setPrepareChecks([]);
setPrepareMessage('Select a working directory to validate the launch environment.');
return;
}
const selectedProviderIdSet = new Set(selectedMemberProviders);
for (const providerId of Array.from(lastPrepareProviderSignatureByIdRef.current.keys())) {
if (!selectedProviderIdSet.has(providerId)) {
lastPrepareProviderSignatureByIdRef.current.delete(providerId);
prepareProviderRequestSeqByIdRef.current.delete(providerId);
prepareWarningsByProviderIdRef.current.delete(providerId);
}
}
const providerPlans = buildProviderPreparePlans({
cwd: effectiveCwd,
providerIds: selectedMemberProviders,
selectedModelChecksByProvider,
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
limitContext: effectiveAnthropicRuntimeLimitContext,
runtimeProviderStatusById,
cachedModelResultsByCacheKey: prepareModelResultsCacheRef.current,
});
const changedPlans = providerPlans.filter(
(plan) =>
lastPrepareProviderSignatureByIdRef.current.get(plan.providerId) !== plan.requestSignature
);
const loadingMessage = getProvisioningProviderProgressMessage(
changedPlans.map((plan) => plan.providerId),
selectedMemberProviders.length
);
const getSelectedWarnings = (): string[] =>
selectedMemberProviders.flatMap(
(providerId) => prepareWarningsByProviderIdRef.current.get(providerId) ?? []
);
const commitChecks = (nextChecks: ProvisioningProviderCheck[]): void => {
prepareChecksRef.current = nextChecks;
setPrepareChecks(nextChecks);
};
const applyPrepareOutcome = (
nextChecks: ProvisioningProviderCheck[],
pendingMessage: string | null
): void => {
const selectedWarnings = getSelectedWarnings();
setPrepareWarnings(selectedWarnings);
if (nextChecks.some((check) => check.status === 'pending' || check.status === 'checking')) {
setPrepareState('loading');
setPrepareMessage(pendingMessage);
return;
}
const anyFailure = nextChecks.some((check) => check.status === 'failed');
const anyNotes =
selectedWarnings.length > 0 || nextChecks.some((check) => check.status === 'notes');
const failureMessage =
getPrimaryProvisioningFailureDetail(nextChecks) ??
'Some selected providers need attention.';
setPrepareState(anyFailure ? 'failed' : 'ready');
setPrepareMessage(
anyFailure
? failureMessage
: anyNotes
? 'All selected providers are ready, with notes.'
: 'All selected providers are ready.'
);
};
let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders);
for (const plan of changedPlans) {
checks = updateProviderCheck(checks, plan.providerId, {
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
backendSummary: plan.backendSummary,
details: plan.cachedSnapshot.details,
supportDiagnostics: undefined,
});
prepareWarningsByProviderIdRef.current.delete(plan.providerId);
}
commitChecks(checks);
applyPrepareOutcome(
checks,
changedPlans.length > 0
? loadingMessage
: (prepareMessageRef.current ??
getProvisioningProviderProgressMessage([], selectedMemberProviders.length))
);
if (changedPlans.length === 0) {
return;
}
const generation = prepareRequestSeqRef.current;
const runningPlans = changedPlans.map((plan) => {
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 };
});
const isPlanCurrent = (plan: ProviderPreparePlan & { requestSeq: number }): boolean =>
prepareRequestSeqRef.current === generation &&
lastPrepareProviderSignatureByIdRef.current.get(plan.providerId) === plan.requestSignature &&
prepareProviderRequestSeqByIdRef.current.get(plan.providerId) === plan.requestSeq;
void (async () => {
await Promise.all(
runningPlans.map(async (plan) => {
try {
const prepResult = await runProviderPrepareDiagnostics({
cwd: effectiveCwd,
providerId: plan.providerId,
selectedModelIds: plan.selectedModelIds,
selectedModelChecks: plan.selectedModelChecks,
prepareProvisioning: api.teams.prepareProvisioning,
limitContext: effectiveAnthropicRuntimeLimitContext,
cachedModelResultsById: plan.cachedModelResultsById,
onModelProgress: ({ status, details }) => {
if (!isPlanCurrent(plan)) {
return;
}
const nextChecks = updateProviderCheck(prepareChecksRef.current, plan.providerId, {
status,
backendSummary: plan.backendSummary,
details,
supportDiagnostics: undefined,
});
commitChecks(nextChecks);
applyPrepareOutcome(nextChecks, loadingMessage);
},
});
if (!isPlanCurrent(plan)) {
return;
}
prepareWarningsByProviderIdRef.current.set(
plan.providerId,
prepResult.warnings.map(
(warning) => `${getProviderLabel(plan.providerId)}: ${warning}`
)
);
prepareModelResultsCacheRef.current.set(
plan.cacheKey,
mergeReusableProviderPrepareModelResults(
prepareModelResultsCacheRef.current.get(plan.cacheKey),
prepResult.modelResultsById
)
);
storeShortLivedProviderPrepareModelResults({
providerId: plan.providerId,
cacheKey: plan.cacheKey,
modelResultsById: prepResult.modelResultsById,
});
const nextChecks = updateProviderCheck(prepareChecksRef.current, plan.providerId, {
status: prepResult.status,
backendSummary: plan.backendSummary,
details: prepResult.details,
supportDiagnostics: prepResult.supportDiagnostics,
});
commitChecks(nextChecks);
applyPrepareOutcome(nextChecks, loadingMessage);
} catch (error) {
if (!isPlanCurrent(plan)) {
return;
}
const failureMessage =
error instanceof Error ? error.message : 'Failed to prepare selected providers';
const nextChecks = updateProviderCheck(prepareChecksRef.current, plan.providerId, {
status: 'failed',
backendSummary: plan.backendSummary,
details: [failureMessage],
supportDiagnostics: undefined,
});
prepareWarningsByProviderIdRef.current.delete(plan.providerId);
commitChecks(nextChecks);
applyPrepareOutcome(nextChecks, failureMessage);
}
})
);
})();
}, [
open,
isLaunchMode,
effectiveCwd,
effectiveAnthropicRuntimeLimitContext,
prepareProviderInvalidationEpochById,
runtimeProviderStatusById,
selectedMemberProviders,
selectedModelChecksByProvider,
selectedModelChecksByProviderSignature,
]);
// ---------------------------------------------------------------------------
// 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(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]);
// 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<MentionSuggestion[]>(
() => 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', '<auto>', '--disallowedTools', APP_TEAM_RUNTIME_DISALLOWED_TOOLS);
if (skipPermissions) args.push('--dangerously-skip-permissions');
const model = computeEffectiveTeamModel(
selectedModel,
effectiveAnthropicRuntimeLimitContext,
selectedProviderId,
runtimeProviderStatusById.get(selectedProviderId)
);
if (model) args.push('--model', model);
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,
isLaunchMode,
skipPermissions,
selectedModel,
effectiveAnthropicRuntimeLimitContext,
selectedEffortForCurrentSelection,
selectedProviderId,
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 (selectedEffortForCurrentSelection) {
summary.push(`Effort: ${selectedEffortForCurrentSelection}`);
}
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 (skipPermissions) summary.push('Auto-approve tools');
summary.push('Fresh lead 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,
selectedEffortForCurrentSelection,
selectedFastMode,
anthropicProviderFastModeDefault,
effectiveAnthropicRuntimeLimitContext,
skipPermissions,
worktreeEnabled,
worktreeName,
customArgs,
]);
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
const validationErrors = useMemo(() => {
const errors: string[] = [];
if (selectedProjectPathDeleted) {
errors.push('Project folder no longer exists');
} else 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,
selectedProjectPathDeleted,
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<string, string> = {};
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, {
inheritedProviderId: selectedProviderId,
});
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,
effectiveAnthropicRuntimeLimitContext,
selectedProviderId,
runtimeProviderStatusById.get(selectedProviderId)
),
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,
};
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 = selectedEffortForCurrentSelection
? (selectedEffortForCurrentSelection 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 <span className="font-mono font-medium">{effectiveTeamName}</span>{' '}
and start it again via local Claude CLI.
</>
) : (
<>
Start team <span className="font-mono font-medium">{effectiveTeamName}</span> 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 (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
closeDialog();
}
}}
>
<DialogContent
className={isSchedule ? 'max-h-[90vh] max-w-[52rem] overflow-y-auto' : 'max-w-[52rem]'}
>
<DialogHeader>
<DialogTitle className="text-sm">{dialogTitle}</DialogTitle>
<DialogDescription className="text-xs">{dialogDescription}</DialogDescription>
</DialogHeader>
{isRelaunch ? (
<div
className="rounded-md border p-3 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1 space-y-1">
<p className="font-medium">Relaunch will restart the current team run</p>
<p className="opacity-80">
Saving these settings will stop the current team process, persist the updated
roster, and launch the team again with the new runtime.
</p>
</div>
</div>
</div>
) : null}
{/* Launch-only: Conflict warning */}
{isLaunchMode && conflictingTeam && !conflictDismissed ? (
<div
className="rounded-md border p-3 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1 space-y-1">
<p className="font-medium">
Another team &ldquo;{conflictingTeam.displayName}&rdquo; is already running for
this working directory
</p>
<p className="opacity-80">
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.
</p>
<p className="text-[11px] opacity-70">
Working directory: <span className="font-mono">{effectiveCwd}</span>
</p>
</div>
<button
type="button"
className="shrink-0 rounded p-0.5 opacity-60 transition-colors hover:opacity-100"
onClick={() => setConflictDismissed(true)}
>
<X className="size-3.5" />
</button>
</div>
</div>
) : null}
<div className="space-y-4">
{/* ═══════════════════════════════════════════════════════════════════
Schedule-only: Team selector (standalone mode)
═══════════════════════════════════════════════════════════════════ */}
{needsTeamSelector ? (
<div className="space-y-1.5">
<Label className="text-xs">Team</Label>
<Combobox
options={teamOptions}
value={selectedTeamName}
onValueChange={setSelectedTeamName}
placeholder="Select a team..."
searchPlaceholder="Search teams..."
emptyMessage={
teamOptions.length === 0
? 'No teams available. Create a team first.'
: 'No teams match your search.'
}
disabled={teamOptions.length === 0}
renderOption={(option, isSelected) => {
const colorName = option.meta?.color as string | undefined;
const colorSet = colorName
? getTeamColorSet(colorName)
: nameColorSet(option.label);
return (
<>
{isSelected ? (
<Check className="mr-2 size-3.5 shrink-0 text-[var(--color-text)]" />
) : (
<span
className="mr-2 size-3.5 shrink-0 rounded-full"
style={{ backgroundColor: colorSet.text }}
/>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
{isSelected ? (
<span
className="size-2 shrink-0 rounded-full"
style={{ backgroundColor: colorSet.text }}
/>
) : null}
<p className="truncate font-medium text-[var(--color-text)]">
{option.label}
</p>
</div>
{option.description ? (
<p className="truncate text-[var(--color-text-muted)]">
{option.description}
</p>
) : null}
</div>
</>
);
}}
/>
</div>
) : null}
{/* ═══════════════════════════════════════════════════════════════════
Schedule-only: Schedule configuration section
═══════════════════════════════════════════════════════════════════ */}
{isSchedule ? (
<div
className="rounded-lg border border-[var(--color-border-emphasis)] shadow-sm"
style={{
backgroundColor: isLight
? 'color-mix(in srgb, var(--color-surface-overlay) 24%, white 76%)'
: 'var(--color-surface-overlay)',
}}
>
<button
type="button"
className="flex w-full items-center gap-1.5 px-3 py-2 text-left"
onClick={() => setSchedExpanded((v) => !v)}
>
{schedExpanded ? (
<ChevronDown className="size-3.5 shrink-0 text-[var(--color-text-muted)]" />
) : (
<ChevronRight className="size-3.5 shrink-0 text-[var(--color-text-muted)]" />
)}
<span className="text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
Schedule
</span>
{!schedExpanded && (schedLabel || cronExpression) ? (
<span className="ml-auto truncate text-[11px] text-[var(--color-text-muted)] opacity-70">
{schedLabel || cronExpression}
</span>
) : null}
</button>
{schedExpanded ? (
<div className="space-y-3 border-t border-[var(--color-border)] px-3 pb-3 pt-2">
{/* Label */}
<div className="space-y-1.5">
<Label htmlFor="schedule-label" className="label-optional">
Label (optional)
</Label>
<Input
id="schedule-label"
className="h-8 text-xs"
value={schedLabel}
onChange={(e) => setSchedLabel(e.target.value)}
placeholder="e.g., Daily code review, Nightly tests..."
/>
</div>
{/* Cron + Timezone + Warmup */}
<CronScheduleInput
cronExpression={cronExpression}
onCronExpressionChange={setCronExpression}
timezone={timezone}
onTimezoneChange={setTimezone}
warmUpMinutes={warmUpMinutes}
onWarmUpMinutesChange={setWarmUpMinutes}
/>
</div>
) : null}
</div>
) : null}
{/* ═══════════════════════════════════════════════════════════════════
Shared: Working directory
═══════════════════════════════════════════════════════════════════ */}
<ProjectPathSelector
cwdMode={cwdMode}
onCwdModeChange={setCwdMode}
selectedProjectPath={selectedProjectPath}
onSelectedProjectPathChange={setSelectedProjectPath}
customCwd={customCwd}
onCustomCwdChange={setCustomCwd}
projects={projects}
projectsLoading={projectsLoading}
projectsError={projectsError}
/>
{/* ═══════════════════════════════════════════════════════════════════
Launch: optional settings
Schedule: prompt + execution defaults
═══════════════════════════════════════════════════════════════════ */}
{isLaunchMode ? (
<OptionalSettingsSection
title={isRelaunch ? 'Relaunch settings' : 'Optional launch settings'}
description={
isRelaunch
? 'Review the roster and lead runtime before restarting the team.'
: 'Keep the launch flow focused on the project path and only expand this when you want extra control.'
}
summary={launchOptionalSummary}
>
<div className="space-y-4">
{selectedProviderId === 'anthropic' ? (
<div className="space-y-2">
<AnthropicFastModeSelector
value={selectedFastMode}
onValueChange={setSelectedFastMode}
providerFastModeDefault={anthropicProviderFastModeDefault}
model={selectedModel}
limitContext={effectiveAnthropicRuntimeLimitContext}
id="launch-fast-mode"
/>
{anthropicRuntimeNotice ? (
<div className="bg-amber-500/8 flex items-start gap-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
<Info className="mt-0.5 size-3.5 shrink-0 text-amber-300" />
<p>{anthropicRuntimeNotice}</p>
</div>
) : null}
</div>
) : null}
{selectedProviderId === 'codex' ? (
<div className="space-y-2">
<CodexFastModeSelector
value={selectedFastMode}
onValueChange={setSelectedFastMode}
model={selectedModel}
providerBackendId={
resolveUiOwnedProviderBackendId(
'codex',
runtimeProviderStatusById.get('codex')
) ??
migrateProviderBackendId(
'codex',
previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId
) ??
undefined
}
id="launch-fast-mode"
/>
{anthropicRuntimeNotice ? (
<div className="bg-amber-500/8 flex items-start gap-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
<Info className="mt-0.5 size-3.5 shrink-0 text-amber-300" />
<p>{anthropicRuntimeNotice}</p>
</div>
) : null}
</div>
) : null}
<TeamRosterEditorSection
members={membersDrafts}
onMembersChange={setMembersDrafts}
validateMemberName={validateMemberNameInline}
showWorkflow
showJsonEditor
draftKeyPrefix={`launchTeam:${effectiveTeamName}`}
projectPath={effectiveCwd || null}
taskSuggestions={taskSuggestions}
teamSuggestions={teamMentionSuggestions}
existingMembers={members}
defaultProviderId={selectedProviderId}
inheritedProviderId={selectedProviderId}
inheritedModel={selectedModel}
inheritedEffort={(selectedEffortForCurrentSelection as EffortLevel) || undefined}
inheritModelSettingsByDefault
lockProviderModel={syncModelsWithLead}
forceInheritedModelSettings={syncModelsWithLead}
modelLockReason="This teammate is synced with the lead model. Turn off sync to set a custom provider, model, or effort."
providerId={selectedProviderId}
model={selectedModel}
effort={(selectedEffortForCurrentSelection as EffortLevel) || undefined}
limitContext={effectiveAnthropicRuntimeLimitContext}
leadProviderNoticeById={teammateRuntimeProviderNoticeById}
onProviderChange={setSelectedProviderId}
onModelChange={setSelectedModel}
onEffortChange={setSelectedEffort}
onLimitContextChange={setLimitContext}
syncModelsWithTeammates={syncModelsWithLead}
onSyncModelsWithTeammatesChange={setSyncModelsWithLead}
showWorktreeIsolationControls
teammateWorktreeDefault={teammateWorktreeDefault}
worktreeIsolationDisabledReason={worktreeIsolationDisabledReason}
onTeammateWorktreeDefaultChange={setTeammateWorktreeDefault}
leadWarningText={leadRuntimeWarningText}
memberWarningById={combinedMemberRuntimeWarningById}
memberInfoById={memberWorktreeContinuationInfoById}
leadModelIssueText={leadModelIssueText}
memberModelIssueById={memberModelIssueById}
modelAdvisoryReasonByProvider={
shortLivedModelIssueReasons.modelAdvisoryReasonByProvider
}
modelIssueReasonByProvider={
shortLivedModelIssueReasons.modelIssueReasonByProvider
}
modelUnavailableReasonByProvider={
shortLivedModelIssueReasons.modelUnavailableReasonByProvider
}
softDeleteMembers
disableGeminiOption={isGeminiUiFrozen()}
headerBottom={
showRosterTeammateRuntimeCompatibility || hasSelectedWorktreeIsolation ? (
<div className="space-y-2">
{showRosterTeammateRuntimeCompatibility ? (
<TeammateRuntimeCompatibilityNotice
analysis={teammateRuntimeCompatibility}
onOpenDashboard={() => {
closeDialog();
openDashboard();
}}
/>
) : null}
{hasSelectedWorktreeIsolation ? (
<WorktreeGitReadinessBanner state={worktreeGitReadiness} />
) : null}
</div>
) : null
}
/>
<div className="space-y-1.5">
<Label htmlFor="dialog-prompt" className="label-optional">
Prompt for team lead (optional)
</Label>
<MentionableTextarea
id="dialog-prompt"
className="min-h-[100px] text-xs"
minRows={4}
maxRows={12}
value={promptDraft.value}
onValueChange={promptDraft.setValue}
suggestions={mentionSuggestions}
projectPath={effectiveCwd || null}
chips={chipDraft.chips}
onChipRemove={chipDraft.removeChip}
onFileChipInsert={chipDraft.addChip}
placeholder="Instructions for team lead..."
footerRight={
promptDraft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null
}
/>
</div>
<div>
<SkipPermissionsCheckbox
id="dialog-skip-permissions"
checked={skipPermissions}
onCheckedChange={setSkipPermissions}
/>
</div>
<div className="space-y-2">
{providerChangeForcesFreshLeadContext ? (
<div
className="rounded-md border px-3 py-2 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<p>
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.
</p>
</div>
</div>
) : null}
<div
className="rounded-md border px-3 py-2 text-xs"
style={{
backgroundColor: 'var(--warning-bg)',
borderColor: 'var(--warning-border)',
color: 'var(--warning-text)',
}}
>
<div className="flex items-start gap-2">
<Info className="mt-0.5 size-3.5 shrink-0" />
<p>
Team relaunch starts a fresh lead session. Durable team state, task board,
and member configuration are rehydrated into the launch prompt.
</p>
</div>
</div>
</div>
<AdvancedCliSection
teamName={effectiveTeamName}
internalArgs={internalArgs}
worktreeEnabled={worktreeEnabled}
onWorktreeEnabledChange={setWorktreeEnabled}
worktreeName={worktreeName}
onWorktreeNameChange={setWorktreeName}
customArgs={customArgs}
onCustomArgsChange={setCustomArgs}
/>
</div>
</OptionalSettingsSection>
) : (
<>
<div className="space-y-1.5">
<Label htmlFor="dialog-prompt">Prompt</Label>
<MentionableTextarea
id="dialog-prompt"
className="min-h-[100px] text-xs"
minRows={4}
maxRows={12}
value={promptDraft.value}
onValueChange={promptDraft.setValue}
suggestions={mentionSuggestions}
projectPath={effectiveCwd || null}
chips={chipDraft.chips}
onChipRemove={chipDraft.removeChip}
onFileChipInsert={chipDraft.addChip}
placeholder="Instructions for Claude to execute on schedule..."
footerRight={
promptDraft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null
}
/>
<p className="text-[11px] text-[var(--color-text-muted)]">
This prompt will be passed to <code className="font-mono">claude -p</code> for
one-shot execution
</p>
{selectedProviderId === 'anthropic' ? (
<div className="flex gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 p-2 text-[11px] leading-relaxed text-amber-100">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<p>
Starting June 15, 2026, Anthropic bills <code>claude -p</code> and Agent SDK
usage from the monthly Agent SDK credit, separate from interactive Claude Code
limits. The credit resets each billing cycle and unused credit does not roll
over.{' '}
<a
href={ANTHROPIC_AGENT_SDK_CREDIT_ARTICLE_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 font-medium underline underline-offset-2 hover:text-white"
>
Read Anthropic article
<ExternalLink className="size-3" />
</a>
.
</p>
</div>
) : null}
</div>
<div>
<TeamModelSelector
providerId={selectedProviderId}
onProviderChange={setSelectedProviderId}
value={selectedModel}
onValueChange={setSelectedModel}
id="dialog-model"
disableGeminiOption={isGeminiUiFrozen()}
providerDisabledReasonById={{
opencode: OPENCODE_ONE_SHOT_DISABLED_REASON,
}}
providerDisabledBadgeLabelById={{
opencode: OPENCODE_ONE_SHOT_DISABLED_BADGE_LABEL,
}}
/>
<EffortLevelSelector
value={selectedEffortForCurrentSelection}
onValueChange={setSelectedEffort}
id="dialog-effort"
providerId={selectedProviderId}
model={selectedModel}
limitContext={effectiveAnthropicRuntimeLimitContext}
/>
{selectedProviderId === 'anthropic' ? (
<div className="mt-2">
<AnthropicFastModeSelector
value={selectedFastMode}
onValueChange={setSelectedFastMode}
providerFastModeDefault={anthropicProviderFastModeDefault}
model={selectedModel}
limitContext={effectiveAnthropicRuntimeLimitContext}
id="dialog-fast-mode"
/>
{anthropicRuntimeNotice ? (
<div className="bg-amber-500/8 mt-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
{anthropicRuntimeNotice}
</div>
) : null}
</div>
) : null}
{selectedProviderId === 'codex' ? (
<div className="mt-2">
<CodexFastModeSelector
value={selectedFastMode}
onValueChange={setSelectedFastMode}
model={selectedModel}
providerBackendId={
resolveUiOwnedProviderBackendId(
'codex',
runtimeProviderStatusById.get('codex')
) ??
migrateProviderBackendId(
'codex',
previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId
) ??
undefined
}
id="dialog-fast-mode"
/>
{anthropicRuntimeNotice ? (
<div className="bg-amber-500/8 mt-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
{anthropicRuntimeNotice}
</div>
) : null}
</div>
) : null}
<SkipPermissionsCheckbox
id="dialog-skip-permissions"
checked={skipPermissions}
onCheckedChange={setSkipPermissions}
/>
</div>
</>
)}
{/* ═══════════════════════════════════════════════════════════════════
Schedule-only: Execution limits
═══════════════════════════════════════════════════════════════════ */}
{isSchedule ? (
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label
htmlFor="schedule-max-turns"
className="text-[11px] text-[var(--color-text-muted)]"
>
Max turns
</Label>
<Input
id="schedule-max-turns"
type="number"
min={1}
max={500}
className="h-8 text-xs"
value={maxTurns}
onChange={(e) => setMaxTurns(Math.max(1, parseInt(e.target.value) || 50))}
/>
</div>
<div className="space-y-1">
<Label
htmlFor="schedule-max-budget"
className="text-[11px] text-[var(--color-text-muted)]"
>
Max budget (USD)
</Label>
<Input
id="schedule-max-budget"
type="number"
min={0}
step={0.5}
className="h-8 text-xs"
value={maxBudgetUsd}
onChange={(e) => setMaxBudgetUsd(e.target.value)}
placeholder="No limit"
/>
</div>
</div>
) : null}
</div>
{/* Error display */}
{activeError ? (
<div className="flex items-start gap-2 rounded border border-red-500/40 bg-red-500/10 p-2 text-xs text-red-300">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>{activeError}</span>
</div>
) : null}
<DialogFooter className={isLaunchMode ? 'pt-4 sm:justify-between' : 'pt-4'}>
{/* Launch-only: CLI warm-up status */}
{isLaunchMode ? (
<div className="min-w-0">
<ProviderActivityStatusStrip
cliStatus={effectiveCliStatus}
sourceCliStatus={loadingCliStatus}
cliStatusLoading={cliStatusLoading}
cliProviderStatusLoading={cliProviderStatusLoading}
multimodelEnabled={multimodelEnabled}
codexSnapshotPending={codexSnapshotPending}
providerIds={selectedMemberProviders}
className="mb-2"
/>
{effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading' ? (
<>
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
<span className="inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
<div>
<span>
{effectivePrepare.message ??
(effectivePrepare.state === 'idle'
? 'Checking selected providers...'
: 'Preparing environment...')}
</span>
<p className="mt-0.5 flex items-center gap-1.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
<span>
Pre-flight check to catch errors before{' '}
{isRelaunch ? 'relaunch' : 'launch'}
</span>
</p>
</div>
</div>
<ProvisioningProviderStatusList
checks={prepareChecks}
className="mt-2"
onOpenProviderSettings={(providerId) =>
setProviderSettingsProviderId(providerId)
}
/>
</>
) : null}
{effectivePrepare.state === 'ready' ? (
<div>
<div className="flex items-center gap-1.5 text-xs font-medium text-emerald-400">
<CheckCircle2 className="size-3.5 shrink-0" />
<span>
{prepareChecks.some((check) => check.status === 'notes') ||
prepareWarnings.length > 0
? 'Selected providers ready (with notes)'
: 'Selected providers ready'}
</span>
</div>
{effectivePrepare.message ? (
<p className="mt-0.5 pl-5 text-[11px] text-[var(--color-text-muted)]">
{effectivePrepare.message}
</p>
) : null}
<ProvisioningProviderStatusList
checks={prepareChecks}
className="mt-1"
onOpenProviderSettings={(providerId) =>
setProviderSettingsProviderId(providerId)
}
/>
{prepareWarnings.length > 0 && prepareChecks.length === 0 ? (
<div className="mt-0.5 space-y-0.5 pl-5">
{prepareWarnings.map((warning, index) => (
<p key={`${index}:${warning}`} className="text-[11px] text-sky-300">
{warning}
</p>
))}
</div>
) : null}
</div>
) : null}
{effectivePrepare.state === 'failed' ? (
<div className="text-xs">
<div className="flex items-start gap-2 text-red-300">
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<div className="min-w-0">
<p className="font-medium">
Runtime environment is not available - {isRelaunch ? 'relaunch' : 'launch'}{' '}
is blocked
</p>
<p className="mt-0.5 text-red-300/80">
{effectivePrepare.message ?? 'Failed to prepare environment'}
</p>
<p className="mt-0.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
Pre-flight check to catch errors before {isRelaunch ? 'relaunch' : 'launch'}
</p>
</div>
</div>
{!shouldHideProvisioningProviderStatusList(
prepareChecks,
effectivePrepare.message
) ? (
<ProvisioningProviderStatusList
checks={prepareChecks}
className="mt-2"
suppressDetailsMatching={effectivePrepare.message}
onOpenProviderSettings={(providerId) =>
setProviderSettingsProviderId(providerId)
}
/>
) : null}
{prepareWarnings.length > 0 && prepareChecks.length === 0 ? (
<div className="mt-1 space-y-0.5 pl-6">
{prepareWarnings.map((warning, index) => (
<p
key={`${index}:${warning}`}
className="text-[11px]"
style={{ color: 'var(--warning-text)' }}
>
{warning}
</p>
))}
</div>
) : null}
<div className="mt-1 flex items-center gap-2 pl-6">
<p className="text-[11px] text-[var(--color-text-muted)]">
{getProvisioningFailureHint(effectivePrepare.message, prepareChecks)}
</p>
{(effectivePrepare.message ?? '').toLowerCase().includes('spawn ') ||
prepareChecks.some((check) =>
check.details.some((detail) => detail.toLowerCase().includes('spawn '))
) ? (
<button
type="button"
className="shrink-0 rounded bg-blue-600 px-2 py-0.5 text-[11px] font-medium text-white transition-colors hover:bg-blue-500"
onClick={() => {
closeDialog();
openDashboard();
}}
>
Go to Dashboard
</button>
) : null}
</div>
{showCodexReconnectPrompt ? (
<div className="pl-6">
<CodexReconnectPrompt
authUrl={codexAccount.snapshot?.login.authUrl ?? null}
userCode={codexAccount.snapshot?.login.userCode ?? null}
reconnectBusy={codexAccount.loading}
onReconnect={() => handleCodexReconnect('browser')}
onDeviceCodeReconnect={() => handleCodexReconnect('device_code')}
/>
</div>
) : null}
</div>
) : null}
</div>
) : null}
<div className="flex shrink-0 items-center gap-2">
<Button
size="sm"
className="bg-emerald-600 text-white hover:bg-emerald-700"
disabled={isDisabled}
onClick={handleSubmit}
>
{isSubmitting || launchInFlight ? (
<>
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
{submittingLabel}
</>
) : (
submitLabel
)}
</Button>
</div>
</DialogFooter>
</DialogContent>
<ProvisioningProviderRuntimeSettingsDialog
openProviderId={providerSettingsProviderId}
onOpenProviderIdChange={(providerId) => setProviderSettingsProviderId(providerId)}
providers={effectiveCliStatus?.providers ?? []}
projectPath={effectiveCwd || null}
disabled={isSubmitting || launchInFlight}
onProviderRuntimeChanged={invalidatePrepareProvider}
/>
</Dialog>
);
};