import { useEffect, useState, useCallback } from 'react'; 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'; /** * Extract model string from PhaseModelEntry or string */ function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModelId { if (typeof entry === 'string') { return entry as ModelAlias | CursorModelId; } return entry.model; } 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; } type DialogMode = 'input' | 'review' | 'applying'; export function BacklogPlanDialog({ open, onClose, projectPath, onPlanApplied, pendingPlanResult, setPendingPlanResult, isGeneratingPlan, setIsGeneratingPlan, }: BacklogPlanDialogProps) { 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) { toast.error('API not available'); return; } // Start generation in background setIsGeneratingPlan(true); // Use model override if set, otherwise use global default (extract model string from PhaseModelEntry) const effectiveModel = modelOverride || extractModel(phaseModels.backlogPlanningModel); const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel); if (!result.success) { setIsGeneratingPlan(false); toast.error(result.error || 'Failed to start plan generation'); return; } // Show toast and close dialog - generation runs in background 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); 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, ]); const handleDiscard = useCallback(() => { setPendingPlanResult(null); setMode('input'); }, [setPendingPlanResult]); 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 to your backlog. The AI will analyze your current features and propose additions, updates, or deletions.