import React, { useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { ExtendedContextCheckbox } from '@renderer/components/team/dialogs/ExtendedContextCheckbox'; import { SkipPermissionsCheckbox } from '@renderer/components/team/dialogs/SkipPermissionsCheckbox'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Label } from '@renderer/components/ui/label'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { useStore } from '@renderer/store'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { normalizePath } from '@renderer/utils/pathNormalize'; import { AlertTriangle, CheckCircle2, Loader2, RotateCcw, X } from 'lucide-react'; import { EffortLevelSelector } from './EffortLevelSelector'; import { ProjectPathSelector } from './ProjectPathSelector'; import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector'; import type { ActiveTeamRef } from './CreateTeamDialog'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { EffortLevel, Project, ResolvedTeamMember, TeamLaunchRequest, TeamProvisioningPrepareResult, } from '@shared/types'; interface LaunchTeamDialogProps { open: boolean; teamName: string; members: ResolvedTeamMember[]; defaultProjectPath?: string; provisioningError: string | null; clearProvisioningError?: () => void; activeTeams?: ActiveTeamRef[]; onClose: () => void; onLaunch: (request: TeamLaunchRequest) => Promise; } export const LaunchTeamDialog = ({ open, teamName, members, defaultProjectPath, provisioningError, clearProvisioningError, activeTeams, onClose, onLaunch, }: LaunchTeamDialogProps): React.JSX.Element => { const [cwdMode, setCwdMode] = useState<'project' | 'custom'>('project'); const [selectedProjectPath, setSelectedProjectPath] = useState(''); const [customCwd, setCustomCwd] = useState(''); const promptDraft = useDraftPersistence({ key: `launchTeam:${teamName}:prompt` }); const chipDraft = useChipDraftPersistence(`launchTeam:${teamName}:chips`); const [projects, setProjects] = useState([]); const [projectsLoading, setProjectsLoading] = useState(false); const [projectsError, setProjectsError] = useState(null); const [localError, setLocalError] = useState(null); const [prepareState, setPrepareState] = useState<'idle' | 'loading' | 'ready' | 'failed'>('idle'); const [prepareMessage, setPrepareMessage] = useState(null); const [prepareWarnings, setPrepareWarnings] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [selectedModel, setSelectedModelRaw] = useState(() => { const stored = localStorage.getItem('team:lastSelectedModel') ?? ''; return stored === '__default__' ? '' : stored; }); const [extendedContext, setExtendedContextRaw] = useState( () => localStorage.getItem('team:lastExtendedContext') === 'true' ); const [skipPermissions, setSkipPermissionsRaw] = useState( () => localStorage.getItem('team:lastSkipPermissions') !== 'false' ); const [selectedEffort, setSelectedEffortRaw] = useState( () => localStorage.getItem('team:lastSelectedEffort') ?? '' ); const [clearContext, setClearContext] = useState(false); const [conflictDismissed, setConflictDismissed] = useState(false); const setSelectedModel = (value: string): void => { setSelectedModelRaw(value); localStorage.setItem('team:lastSelectedModel', value); }; const setExtendedContext = (value: boolean): void => { setExtendedContextRaw(value); localStorage.setItem('team:lastExtendedContext', 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 resetFormState = (): void => { setLocalError(null); setIsSubmitting(false); setPrepareState('idle'); setPrepareMessage(null); setPrepareWarnings([]); setCwdMode('project'); setSelectedProjectPath(''); setCustomCwd(''); setClearContext(false); setConflictDismissed(false); chipDraft.clearChipDraft(); }; // Clear stale provisioning error when dialog opens useEffect(() => { if (open) { clearProvisioningError?.(); } }, [open, clearProvisioningError]); // Warm up CLI on open useEffect(() => { if (!open) { return; } if (typeof api.teams.prepareProvisioning !== 'function') { setPrepareState('failed'); setPrepareWarnings([]); setPrepareMessage( 'Current preload version does not support team:prepareProvisioning. Restart the dev app.' ); return; } let cancelled = false; setPrepareState('loading'); setPrepareMessage('Warming up CLI environment...'); setPrepareWarnings([]); void (async () => { try { const prepResult: TeamProvisioningPrepareResult = await api.teams.prepareProvisioning(); if (cancelled) { return; } setPrepareState(prepResult.ready ? 'ready' : 'failed'); setPrepareMessage(prepResult.message); setPrepareWarnings(prepResult.warnings ?? []); } catch (error) { if (cancelled) { return; } setPrepareState('failed'); setPrepareWarnings([]); setPrepareMessage( error instanceof Error ? error.message : 'Failed to warm up Claude CLI environment' ); } })(); return () => { cancelled = true; }; }, [open]); const repositoryGroups = useStore((s) => s.repositoryGroups); // Fetch projects on open, merging with repositoryGroups from store useEffect(() => { if (!open) { return; } setProjectsLoading(true); setProjectsError(null); let cancelled = false; void (async () => { try { const apiProjects = await api.getProjects(); if (cancelled) { return; } // Merge repositoryGroups (may include synthetic folders without sessions) const pathSet = new Set(apiProjects.map((p) => p.path)); const extras: Project[] = []; for (const repo of repositoryGroups) { for (const wt of repo.worktrees) { if (!pathSet.has(wt.path)) { pathSet.add(wt.path); extras.push({ id: wt.id, path: wt.path, name: wt.name, sessions: [], totalSessions: 0, createdAt: wt.createdAt ?? Date.now(), }); } } } setProjects([...apiProjects, ...extras]); } 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]); // Pre-select defaultProjectPath when projects loaded (only while dialog is open) useEffect(() => { if (!open) return; if (cwdMode !== 'project') { return; } if (selectedProjectPath || projects.length === 0) { return; } if (defaultProjectPath) { const match = projects.find((p) => p.path === defaultProjectPath); if (match) { setSelectedProjectPath(match.path); return; } } setSelectedProjectPath(projects[0].path); }, [open, cwdMode, projects, selectedProjectPath, defaultProjectPath]); const effectiveCwd = cwdMode === 'project' ? selectedProjectPath.trim() : customCwd.trim(); // Pre-warm file list cache so @-mention file search is instant useFileListCacheWarmer(effectiveCwd || null); const conflictingTeam = useMemo(() => { if (!activeTeams?.length || !effectiveCwd) return null; const norm = normalizePath(effectiveCwd); return ( activeTeams.find((t) => t.teamName !== teamName && normalizePath(t.projectPath) === norm) ?? null ); }, [activeTeams, effectiveCwd, teamName]); // Reset dismiss when conflict target changes (different path or different conflicting team) useEffect(() => { setConflictDismissed(false); }, [conflictingTeam?.teamName, effectiveCwd]); const colorMap = useMemo(() => buildMemberColorMap(members), [members]); const mentionSuggestions = useMemo( () => members.map((m) => ({ id: m.name, name: m.name, subtitle: formatAgentRole(m.role) ?? formatAgentRole(m.agentType) ?? undefined, color: colorMap.get(m.name), })), [members, colorMap] ); const activeError = localError ?? provisioningError; const handleSubmit = (): void => { if (!effectiveCwd) { setLocalError('Select working directory (cwd)'); return; } setLocalError(null); setIsSubmitting(true); void (async () => { try { await onLaunch({ teamName, cwd: effectiveCwd, prompt: promptDraft.value.trim() || undefined, model: computeEffectiveTeamModel(selectedModel, extendedContext), effort: (selectedEffort as EffortLevel) || undefined, clearContext: clearContext || undefined, skipPermissions, }); resetFormState(); onClose(); } catch { // error is shown via provisioningError prop } finally { setIsSubmitting(false); } })(); }; return ( { if (!nextOpen) { resetFormState(); onClose(); } }} > Launch Team Start team {teamName} via local Claude CLI. {conflictingTeam && !conflictDismissed ? (

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

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

Working directory: {effectiveCwd}

) : null} {prepareState === 'failed' ? (

CLI environment is not available — launch is blocked

{prepareMessage ?? 'Failed to prepare environment'}

{prepareWarnings.length > 0 ? (
{prepareWarnings.map((warning) => (

{warning}

))}
) : null}

Make sure claude CLI is installed and available in PATH, then reopen this dialog.

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

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

)}
{activeError ? (

{activeError}

) : null}
{prepareState === 'idle' || prepareState === 'loading' ? (
{prepareMessage ?? (prepareState === 'idle' ? 'Warming up CLI environment...' : 'Preparing environment...')}
) : null} {prepareState === 'ready' ? (
{prepareWarnings.length > 0 ? 'CLI environment ready (with notes)' : 'CLI environment ready'}
{prepareMessage ? (

{prepareMessage}

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

{warning}

))}
) : null}
) : null} {prepareState === 'failed' ?
: null}
); };