import { useEffect, useState, useCallback } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { Loader2, Wand2, Check, Plus, Pencil, Trash2, ChevronDown, ChevronRight, } from 'lucide-react'; import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import type { BacklogPlanResult, BacklogChange, ModelAlias, CursorModelId, PhaseModelEntry, } from '@automaker/types'; import { ModelOverrideTrigger } from '@/components/shared/model-override-trigger'; import { useAppStore } from '@/store/app-store'; /** * Normalize PhaseModelEntry or string to PhaseModelEntry */ function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry { if (typeof entry === 'string') { return { model: entry as ModelAlias | CursorModelId }; } return entry; } interface BacklogPlanDialogProps { open: boolean; onClose: () => void; projectPath: string; onPlanApplied?: () => void; // Props for background generation pendingPlanResult: BacklogPlanResult | null; setPendingPlanResult: (result: BacklogPlanResult | null) => void; isGeneratingPlan: boolean; setIsGeneratingPlan: (generating: boolean) => void; // Branch to use for created features (defaults to 'main' when applying) currentBranch?: string; } type DialogMode = 'input' | 'review' | 'applying'; export function BacklogPlanDialog({ open, onClose, projectPath, onPlanApplied, pendingPlanResult, setPendingPlanResult, isGeneratingPlan, setIsGeneratingPlan, currentBranch, }: BacklogPlanDialogProps) { const logger = createLogger('BacklogPlanDialog'); const [mode, setMode] = useState('input'); const [prompt, setPrompt] = useState(''); const [expandedChanges, setExpandedChanges] = useState>(new Set()); const [selectedChanges, setSelectedChanges] = useState>(new Set()); const [modelOverride, setModelOverride] = useState(null); const { phaseModels } = useAppStore(); // Set mode based on whether we have a pending result useEffect(() => { if (open) { if (pendingPlanResult) { setMode('review'); // Select all changes by default setSelectedChanges(new Set(pendingPlanResult.changes.map((_, i) => i))); setExpandedChanges(new Set()); } else { setMode('input'); } } }, [open, pendingPlanResult]); const handleGenerate = useCallback(async () => { if (!prompt.trim()) { toast.error('Please enter a prompt describing the changes you want'); return; } const api = getElectronAPI(); if (!api?.backlogPlan) { logger.warn('Backlog plan API not available'); toast.error('API not available'); return; } // Start generation in background logger.debug('Starting backlog plan generation', { projectPath, promptLength: prompt.length, hasModelOverride: Boolean(modelOverride), }); setIsGeneratingPlan(true); // Use model override if set, otherwise use global default (extract model string from PhaseModelEntry) const effectiveModelEntry = modelOverride || normalizeEntry(phaseModels.backlogPlanningModel); const effectiveModel = effectiveModelEntry.model; const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel); if (!result.success) { logger.error('Backlog plan generation failed to start', { error: result.error, projectPath, }); setIsGeneratingPlan(false); toast.error(result.error || 'Failed to start plan generation'); return; } // Show toast and close dialog - generation runs in background logger.debug('Backlog plan generation started', { projectPath, model: effectiveModel, }); toast.info('Generating plan... This will be ready soon!', { duration: 3000, }); setPrompt(''); onClose(); }, [projectPath, prompt, modelOverride, phaseModels, setIsGeneratingPlan, onClose]); const handleApply = useCallback(async () => { if (!pendingPlanResult) return; // Filter to only selected changes const selectedChangesList = pendingPlanResult.changes.filter((_, index) => selectedChanges.has(index) ); if (selectedChangesList.length === 0) { toast.error('Please select at least one change to apply'); return; } const api = getElectronAPI(); if (!api?.backlogPlan) { toast.error('API not available'); return; } setMode('applying'); // Create a filtered plan result with only selected changes const filteredPlanResult: BacklogPlanResult = { ...pendingPlanResult, changes: selectedChangesList, // Filter dependency updates to only include those for selected features dependencyUpdates: pendingPlanResult.dependencyUpdates?.filter((update) => { const isDeleting = selectedChangesList.some( (c) => c.type === 'delete' && c.featureId === update.featureId ); return !isDeleting; }) || [], }; const result = await api.backlogPlan.apply( projectPath, filteredPlanResult, currentBranch ?? 'main' ); if (result.success) { toast.success(`Applied ${result.appliedChanges?.length || 0} changes`); setPendingPlanResult(null); onPlanApplied?.(); onClose(); } else { toast.error(result.error || 'Failed to apply plan'); setMode('review'); } }, [ projectPath, pendingPlanResult, selectedChanges, setPendingPlanResult, onPlanApplied, onClose, currentBranch, ]); const handleDiscard = useCallback(async () => { setPendingPlanResult(null); setMode('input'); const api = getElectronAPI(); if (api?.backlogPlan) { await api.backlogPlan.clear(projectPath); } }, [setPendingPlanResult, projectPath]); const toggleChangeExpanded = (index: number) => { setExpandedChanges((prev) => { const next = new Set(prev); if (next.has(index)) { next.delete(index); } else { next.add(index); } return next; }); }; const toggleChangeSelected = (index: number) => { setSelectedChanges((prev) => { const next = new Set(prev); if (next.has(index)) { next.delete(index); } else { next.add(index); } return next; }); }; const toggleAllChanges = () => { if (!pendingPlanResult) return; if (selectedChanges.size === pendingPlanResult.changes.length) { setSelectedChanges(new Set()); } else { setSelectedChanges(new Set(pendingPlanResult.changes.map((_, i) => i))); } }; const getChangeIcon = (type: BacklogChange['type']) => { switch (type) { case 'add': return ; case 'update': return ; case 'delete': return ; } }; const getChangeLabel = (change: BacklogChange) => { switch (change.type) { case 'add': return change.feature?.title || 'New Feature'; case 'update': return `Update: ${change.featureId}`; case 'delete': return `Delete: ${change.featureId}`; } }; const renderContent = () => { switch (mode) { case 'input': return (
Describe the changes you want to make across your features. The AI will analyze your current feature list and propose additions, updates, deletions, or restructuring.