import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { HotkeyButton } from '@/components/ui/hotkey-button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { CategoryAutocomplete } from '@/components/ui/category-autocomplete'; import { DescriptionImageDropZone, FeatureImagePath as DescriptionImagePath, FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; import { MessageSquare, Settings2, SlidersHorizontal, Sparkles, ChevronDown, Play, } from 'lucide-react'; import { toast } from 'sonner'; import { getElectronAPI } from '@/lib/electron'; import { modelSupportsThinking } from '@/lib/utils'; import { useAppStore, AgentModel, ThinkingLevel, FeatureImage, AIProfile, PlanningMode, Feature, } from '@/store/app-store'; import { ModelSelector, ThinkingLevelSelector, ProfileQuickSelect, TestingTabContent, PrioritySelector, BranchSelector, PlanningModeSelector, AncestorContextSection, } from '../shared'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useNavigate } from '@tanstack/react-router'; import { getAncestors, formatAncestorContextForPrompt, type AncestorContext, } from '@automaker/dependency-resolver'; type FeatureData = { title: string; category: string; description: string; images: FeatureImage[]; imagePaths: DescriptionImagePath[]; textFilePaths: DescriptionTextFilePath[]; skipTests: boolean; model: AgentModel; thinkingLevel: ThinkingLevel; branchName: string; // Can be empty string to use current branch priority: number; planningMode: PlanningMode; requirePlanApproval: boolean; dependencies?: string[]; }; interface AddFeatureDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onAdd: (feature: FeatureData) => void; onAddAndStart?: (feature: FeatureData) => void; categorySuggestions: string[]; branchSuggestions: string[]; branchCardCounts?: Record; // Map of branch name to unarchived card count defaultSkipTests: boolean; defaultBranch?: string; currentBranch?: string; isMaximized: boolean; showProfilesOnly: boolean; aiProfiles: AIProfile[]; // Spawn task mode props parentFeature?: Feature | null; allFeatures?: Feature[]; } export function AddFeatureDialog({ open, onOpenChange, onAdd, onAddAndStart, categorySuggestions, branchSuggestions, branchCardCounts, defaultSkipTests, defaultBranch = 'main', currentBranch, isMaximized, showProfilesOnly, aiProfiles, parentFeature = null, allFeatures = [], }: AddFeatureDialogProps) { const isSpawnMode = !!parentFeature; const navigate = useNavigate(); const [useCurrentBranch, setUseCurrentBranch] = useState(true); const [newFeature, setNewFeature] = useState({ title: '', category: '', description: '', images: [] as FeatureImage[], imagePaths: [] as DescriptionImagePath[], textFilePaths: [] as DescriptionTextFilePath[], skipTests: false, model: 'opus' as AgentModel, thinkingLevel: 'none' as ThinkingLevel, branchName: '', priority: 2 as number, // Default to medium priority }); const [newFeaturePreviewMap, setNewFeaturePreviewMap] = useState( () => new Map() ); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [descriptionError, setDescriptionError] = useState(false); const [isEnhancing, setIsEnhancing] = useState(false); const [enhancementMode, setEnhancementMode] = useState< 'improve' | 'technical' | 'simplify' | 'acceptance' >('improve'); const [planningMode, setPlanningMode] = useState('skip'); const [requirePlanApproval, setRequirePlanApproval] = useState(false); // Spawn mode state const [ancestors, setAncestors] = useState([]); const [selectedAncestorIds, setSelectedAncestorIds] = useState>(new Set()); // Get enhancement model, planning mode defaults, and worktrees setting from store const { enhancementModel, defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, useWorktrees, } = useAppStore(); // Sync defaults when dialog opens useEffect(() => { if (open) { // Find the default profile if one is set const defaultProfile = defaultAIProfileId ? aiProfiles.find((p) => p.id === defaultAIProfileId) : null; setNewFeature((prev) => ({ ...prev, skipTests: defaultSkipTests, branchName: defaultBranch || '', // Use default profile's model/thinkingLevel if set, else fallback to defaults model: defaultProfile?.model ?? 'opus', thinkingLevel: defaultProfile?.thinkingLevel ?? 'none', })); setUseCurrentBranch(true); setPlanningMode(defaultPlanningMode); setRequirePlanApproval(defaultRequirePlanApproval); // Initialize ancestors for spawn mode if (parentFeature) { const ancestorList = getAncestors(parentFeature, allFeatures); setAncestors(ancestorList); // Only select parent by default - ancestors are optional context setSelectedAncestorIds(new Set([parentFeature.id])); } else { setAncestors([]); setSelectedAncestorIds(new Set()); } } }, [ open, defaultSkipTests, defaultBranch, defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, aiProfiles, parentFeature, allFeatures, ]); const buildFeatureData = (): FeatureData | null => { if (!newFeature.description.trim()) { setDescriptionError(true); return null; } // Validate branch selection when "other branch" is selected if (useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()) { toast.error('Please select a branch name'); return null; } const category = newFeature.category || 'Uncategorized'; const selectedModel = newFeature.model; const normalizedThinking = modelSupportsThinking(selectedModel) ? newFeature.thinkingLevel : 'none'; // Use current branch if toggle is on // If currentBranch is provided (non-primary worktree), use it // Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary) const finalBranchName = useCurrentBranch ? currentBranch || '' : newFeature.branchName || ''; // Build final description - prepend ancestor context in spawn mode let finalDescription = newFeature.description; if (isSpawnMode && parentFeature && selectedAncestorIds.size > 0) { // Create parent context as an AncestorContext const parentContext: AncestorContext = { id: parentFeature.id, title: parentFeature.title, description: parentFeature.description, spec: parentFeature.spec, summary: parentFeature.summary, depth: -1, }; const allAncestorsWithParent = [parentContext, ...ancestors]; const contextText = formatAncestorContextForPrompt( allAncestorsWithParent, selectedAncestorIds ); if (contextText) { finalDescription = `${contextText}\n\n---\n\n## Task Description\n\n${newFeature.description}`; } } return { title: newFeature.title, category, description: finalDescription, images: newFeature.images, imagePaths: newFeature.imagePaths, textFilePaths: newFeature.textFilePaths, skipTests: newFeature.skipTests, model: selectedModel, thinkingLevel: normalizedThinking, branchName: finalBranchName, priority: newFeature.priority, planningMode, requirePlanApproval, // In spawn mode, automatically add parent as dependency dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined, }; }; const resetForm = () => { setNewFeature({ title: '', category: '', description: '', images: [], imagePaths: [], textFilePaths: [], skipTests: defaultSkipTests, model: 'opus', priority: 2, thinkingLevel: 'none', branchName: '', }); setUseCurrentBranch(true); setPlanningMode(defaultPlanningMode); setRequirePlanApproval(defaultRequirePlanApproval); setNewFeaturePreviewMap(new Map()); setShowAdvancedOptions(false); setDescriptionError(false); onOpenChange(false); }; const handleAction = (actionFn?: (data: FeatureData) => void) => { if (!actionFn) return; const featureData = buildFeatureData(); if (!featureData) return; actionFn(featureData); resetForm(); }; const handleAdd = () => handleAction(onAdd); const handleAddAndStart = () => handleAction(onAddAndStart); const handleDialogClose = (open: boolean) => { onOpenChange(open); if (!open) { setNewFeaturePreviewMap(new Map()); setShowAdvancedOptions(false); setDescriptionError(false); } }; const handleEnhanceDescription = async () => { if (!newFeature.description.trim() || isEnhancing) return; setIsEnhancing(true); try { const api = getElectronAPI(); const result = await api.enhancePrompt?.enhance( newFeature.description, enhancementMode, enhancementModel ); if (result?.success && result.enhancedText) { const enhancedText = result.enhancedText; setNewFeature((prev) => ({ ...prev, description: enhancedText })); toast.success('Description enhanced!'); } else { toast.error(result?.error || 'Failed to enhance description'); } } catch (error) { console.error('Enhancement failed:', error); toast.error('Failed to enhance description'); } finally { setIsEnhancing(false); } }; const handleModelSelect = (model: AgentModel) => { setNewFeature({ ...newFeature, model, thinkingLevel: modelSupportsThinking(model) ? newFeature.thinkingLevel : 'none', }); }; const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => { setNewFeature({ ...newFeature, model, thinkingLevel, }); }; const newModelAllowsThinking = modelSupportsThinking(newFeature.model); return ( { const target = e.target as HTMLElement; if (target.closest('[data-testid="category-autocomplete-list"]')) { e.preventDefault(); } }} onInteractOutside={(e: CustomEvent) => { const target = e.target as HTMLElement; if (target.closest('[data-testid="category-autocomplete-list"]')) { e.preventDefault(); } }} > {isSpawnMode ? 'Spawn Sub-Task' : 'Add New Feature'} {isSpawnMode ? `Create a sub-task that depends on "${parentFeature?.title || parentFeature?.description.slice(0, 50)}..."` : 'Create a new feature card for the Kanban board.'} Prompt Model Options {/* Prompt Tab */} {/* Ancestor Context Section - only in spawn mode */} {isSpawnMode && parentFeature && ( )}
{ setNewFeature({ ...newFeature, description: value }); if (value.trim()) { setDescriptionError(false); } }} images={newFeature.imagePaths} onImagesChange={(images) => setNewFeature({ ...newFeature, imagePaths: images })} textFiles={newFeature.textFilePaths} onTextFilesChange={(textFiles) => setNewFeature({ ...newFeature, textFilePaths: textFiles }) } placeholder="Describe the feature..." previewMap={newFeaturePreviewMap} onPreviewMapChange={setNewFeaturePreviewMap} autoFocus error={descriptionError} />
setNewFeature({ ...newFeature, title: e.target.value })} placeholder="Leave blank to auto-generate" />
setEnhancementMode('improve')}> Improve Clarity setEnhancementMode('technical')}> Add Technical Details setEnhancementMode('simplify')}> Simplify setEnhancementMode('acceptance')}> Add Acceptance Criteria
setNewFeature({ ...newFeature, category: value })} suggestions={categorySuggestions} placeholder="e.g., Core, UI, API" data-testid="feature-category-input" />
{useWorktrees && ( setNewFeature({ ...newFeature, branchName: value })} branchSuggestions={branchSuggestions} branchCardCounts={branchCardCounts} currentBranch={currentBranch} testIdPrefix="feature" /> )} {/* Priority Selector */} setNewFeature({ ...newFeature, priority })} testIdPrefix="priority" />
{/* Model Tab */} {/* Show Advanced Options Toggle */} {showProfilesOnly && (

Simple Mode Active

Only showing AI profiles. Advanced model tweaking is hidden.

)} {/* Quick Select Profile Section */} { onOpenChange(false); navigate({ to: '/profiles' }); }} /> {/* Separator */} {aiProfiles.length > 0 && (!showProfilesOnly || showAdvancedOptions) && (
)} {/* Claude Models Section */} {(!showProfilesOnly || showAdvancedOptions) && ( <> {newModelAllowsThinking && ( setNewFeature({ ...newFeature, thinkingLevel: level }) } /> )} )} {/* Options Tab */} {/* Planning Mode Section */}
{/* Testing Section */} setNewFeature({ ...newFeature, skipTests })} /> {onAddAndStart && ( )} {isSpawnMode ? 'Spawn Task' : 'Add Feature'}
); }