// @ts-nocheck import { useState, useEffect, useRef } 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 { HotkeyButton } from '@/components/ui/hotkey-button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { CategoryAutocomplete } from '@/components/ui/category-autocomplete'; import { DependencySelector } from '@/components/ui/dependency-selector'; import { DescriptionImageDropZone, FeatureImagePath as DescriptionImagePath, FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; import { Play, Cpu, FolderKanban, Settings2 } from 'lucide-react'; import { useNavigate } from '@tanstack/react-router'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils'; import { useAppStore, ModelAlias, ThinkingLevel, FeatureImage, PlanningMode, Feature, } from '@/store/app-store'; import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types'; import { supportsReasoningEffort, isClaudeModel } from '@automaker/types'; import { TestingTabContent, PrioritySelector, WorkModeSelector, PlanningModeSelect, AncestorContextSection, EnhanceWithAI, EnhancementHistoryButton, PipelineExclusionControls, type BaseHistoryEntry, } from '../shared'; import type { WorkMode } from '../shared'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { getAncestors, formatAncestorContextForPrompt, type AncestorContext, } from '@automaker/dependency-resolver'; const logger = createLogger('AddFeatureDialog'); /** * Determines the default work mode based on global settings and current worktree selection. * * Priority: * 1. If forceCurrentBranchMode is true, always defaults to 'current' (work on current branch) * 2. If a non-main worktree is selected in the board header, defaults to 'custom' (use that branch) * 3. If useWorktrees global setting is enabled, defaults to 'auto' (automatic worktree creation) * 4. Otherwise, defaults to 'current' (work on current branch without isolation) */ const getDefaultWorkMode = ( useWorktrees: boolean, selectedNonMainWorktreeBranch?: string, forceCurrentBranchMode?: boolean ): WorkMode => { // If force current branch mode is enabled (worktree setting is off), always use 'current' if (forceCurrentBranchMode) { return 'current'; } // If a non-main worktree is selected, default to 'custom' mode with that branch if (selectedNonMainWorktreeBranch) { return 'custom'; } // Otherwise, respect the global worktree setting return useWorktrees ? 'auto' : 'current'; }; type FeatureData = { title: string; category: string; description: string; images: FeatureImage[]; imagePaths: DescriptionImagePath[]; textFilePaths: DescriptionTextFilePath[]; skipTests: boolean; model: AgentModel; thinkingLevel: ThinkingLevel; reasoningEffort: ReasoningEffort; branchName: string; priority: number; planningMode: PlanningMode; requirePlanApproval: boolean; dependencies?: string[]; childDependencies?: string[]; // Feature IDs that should depend on this feature excludedPipelineSteps?: string[]; // Pipeline step IDs to skip for this feature workMode: WorkMode; }; interface AddFeatureDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onAdd: (feature: FeatureData) => void; onAddAndStart?: (feature: FeatureData) => void; categorySuggestions: string[]; branchSuggestions: string[]; branchCardCounts?: Record; defaultSkipTests: boolean; defaultBranch?: string; currentBranch?: string; isMaximized: boolean; parentFeature?: Feature | null; allFeatures?: Feature[]; /** * Path to the current project for loading pipeline config. */ projectPath?: string; /** * When a non-main worktree is selected in the board header, this will be set to that worktree's branch. * When set, the dialog will default to 'custom' work mode with this branch pre-filled. */ selectedNonMainWorktreeBranch?: string; /** * When true, forces the dialog to default to 'current' work mode (work on current branch). * This is used when the "Default to worktree mode" setting is disabled. */ forceCurrentBranchMode?: boolean; } /** * A single entry in the description history */ interface DescriptionHistoryEntry extends BaseHistoryEntry { description: string; } export function AddFeatureDialog({ open, onOpenChange, onAdd, onAddAndStart, categorySuggestions, branchSuggestions, branchCardCounts, defaultSkipTests, defaultBranch = 'main', currentBranch, isMaximized, parentFeature = null, allFeatures = [], projectPath, selectedNonMainWorktreeBranch, forceCurrentBranchMode, }: AddFeatureDialogProps) { const isSpawnMode = !!parentFeature; const navigate = useNavigate(); const [workMode, setWorkMode] = useState('current'); // Form state const [title, setTitle] = useState(''); const [category, setCategory] = useState(''); const [description, setDescription] = useState(''); const [images, setImages] = useState([]); const [imagePaths, setImagePaths] = useState([]); const [textFilePaths, setTextFilePaths] = useState([]); const [skipTests, setSkipTests] = useState(false); const [branchName, setBranchName] = useState(''); const [priority, setPriority] = useState(2); // Model selection state const [modelEntry, setModelEntry] = useState({ model: 'claude-opus' }); // Check if current model supports planning mode (Claude/Anthropic only) const modelSupportsPlanningMode = isClaudeModel(modelEntry.model); // Planning mode state const [planningMode, setPlanningMode] = useState('skip'); const [requirePlanApproval, setRequirePlanApproval] = useState(false); // UI state const [previewMap, setPreviewMap] = useState(() => new Map()); const [descriptionError, setDescriptionError] = useState(false); // Description history state const [descriptionHistory, setDescriptionHistory] = useState([]); // Spawn mode state const [ancestors, setAncestors] = useState([]); const [selectedAncestorIds, setSelectedAncestorIds] = useState>(new Set()); // Dependency selection state (not in spawn mode) const [parentDependencies, setParentDependencies] = useState([]); const [childDependencies, setChildDependencies] = useState([]); // Pipeline exclusion state const [excludedPipelineSteps, setExcludedPipelineSteps] = useState([]); // Get defaults from store const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel, currentProject, } = useAppStore(); // Use project-level default feature model if set, otherwise fall back to global const effectiveDefaultFeatureModel = currentProject?.defaultFeatureModel ?? defaultFeatureModel; // Track previous open state to detect when dialog opens const wasOpenRef = useRef(false); // Sync defaults only when dialog opens (transitions from closed to open) useEffect(() => { const justOpened = open && !wasOpenRef.current; wasOpenRef.current = open; if (justOpened) { setSkipTests(defaultSkipTests); // When a non-main worktree is selected, use its branch name for custom mode // Otherwise, use the default branch setBranchName(selectedNonMainWorktreeBranch || defaultBranch || ''); setWorkMode( getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode) ); setPlanningMode(defaultPlanningMode); setRequirePlanApproval(defaultRequirePlanApproval); setModelEntry(effectiveDefaultFeatureModel); // Initialize description history (empty for new feature) setDescriptionHistory([]); // Initialize ancestors for spawn mode if (parentFeature) { const ancestorList = getAncestors(parentFeature, allFeatures); setAncestors(ancestorList); setSelectedAncestorIds(new Set([parentFeature.id])); } else { setAncestors([]); setSelectedAncestorIds(new Set()); } // Reset dependency selections setParentDependencies([]); setChildDependencies([]); // Reset pipeline exclusions (all pipelines enabled by default) setExcludedPipelineSteps([]); } }, [ open, defaultSkipTests, defaultBranch, defaultPlanningMode, defaultRequirePlanApproval, effectiveDefaultFeatureModel, useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode, parentFeature, allFeatures, ]); const handleModelChange = (entry: PhaseModelEntry) => { setModelEntry(entry); }; const buildFeatureData = (): FeatureData | null => { if (!description.trim()) { setDescriptionError(true); return null; } if (workMode === 'custom' && !branchName.trim()) { toast.error('Please select a branch name'); return null; } const finalCategory = category || 'Uncategorized'; const selectedModel = modelEntry.model; const normalizedThinking = modelSupportsThinking(selectedModel) ? modelEntry.thinkingLevel || 'none' : 'none'; const normalizedReasoning = supportsReasoningEffort(selectedModel) ? modelEntry.reasoningEffort || 'none' : 'none'; // For 'current' mode, use empty string (work on current branch) // For 'auto' mode, use empty string (will be auto-generated in use-board-actions) // For 'custom' mode, use the specified branch name const finalBranchName = workMode === 'custom' ? branchName || '' : ''; // Build final description with ancestor context in spawn mode let finalDescription = description; if (isSpawnMode && parentFeature && selectedAncestorIds.size > 0) { 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${description}`; } } // Determine final dependencies // In spawn mode, use parent feature as dependency // Otherwise, use manually selected parent dependencies const finalDependencies = isSpawnMode && parentFeature ? [parentFeature.id] : parentDependencies.length > 0 ? parentDependencies : undefined; return { title, category: finalCategory, description: finalDescription, images, imagePaths, textFilePaths, skipTests, model: selectedModel, thinkingLevel: normalizedThinking, reasoningEffort: normalizedReasoning, branchName: finalBranchName, priority, planningMode, requirePlanApproval, dependencies: finalDependencies, childDependencies: childDependencies.length > 0 ? childDependencies : undefined, excludedPipelineSteps: excludedPipelineSteps.length > 0 ? excludedPipelineSteps : undefined, workMode, }; }; const resetForm = () => { setTitle(''); setCategory(''); setDescription(''); setImages([]); setImagePaths([]); setTextFilePaths([]); setSkipTests(defaultSkipTests); // When a non-main worktree is selected, use its branch name for custom mode setBranchName(selectedNonMainWorktreeBranch || ''); setPriority(2); setModelEntry(effectiveDefaultFeatureModel); setWorkMode( getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode) ); setPlanningMode(defaultPlanningMode); setRequirePlanApproval(defaultRequirePlanApproval); setPreviewMap(new Map()); setDescriptionError(false); setDescriptionHistory([]); setParentDependencies([]); setChildDependencies([]); setExcludedPipelineSteps([]); 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) { setPreviewMap(new Map()); setDescriptionError(false); } }; // Shared card styling const cardClass = 'rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3'; const sectionHeaderClass = 'flex items-center gap-2 text-sm font-medium text-foreground'; 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.'}
{/* Ancestor Context Section - only in spawn mode */} {isSpawnMode && parentFeature && ( )} {/* Task Details Section */}
{/* Version History Button */} entry.description} title="Version History" restoreMessage="Description restored from history" />
{ setDescription(value); if (value.trim()) setDescriptionError(false); }} images={imagePaths} onImagesChange={setImagePaths} textFiles={textFilePaths} onTextFilesChange={setTextFilePaths} placeholder="Describe the feature..." previewMap={previewMap} onPreviewMapChange={setPreviewMap} autoFocus error={descriptionError} />
setTitle(e.target.value)} placeholder="Leave blank to auto-generate" />
{/* Enhancement Section */} { const timestamp = new Date().toISOString(); setDescriptionHistory((prev) => { const newHistory = [...prev]; // Add original text first (so user can restore to pre-enhancement state) // Only add if it's different from the last entry to avoid duplicates const lastEntry = prev[prev.length - 1]; if (!lastEntry || lastEntry.description !== originalText) { newHistory.push({ description: originalText, timestamp, source: prev.length === 0 ? 'initial' : 'edit', }); } // Add enhanced text newHistory.push({ description: enhancedText, timestamp, source: 'enhance', enhancementMode: mode, }); return newHistory; }); }} />
{/* AI & Execution Section */}
AI & Execution

Change default model and planning settings for new features

{modelSupportsPlanningMode ? ( ) : (
{}} testIdPrefix="add-feature-planning" compact disabled />

Planning modes are only available for Claude Provider

)}
setSkipTests(!checked)} data-testid="add-feature-skip-tests-checkbox" />
setRequirePlanApproval(!!checked)} disabled={ !modelSupportsPlanningMode || planningMode === 'skip' || planningMode === 'lite' } data-testid="add-feature-require-approval-checkbox" />
{/* Organization Section */}
Organization
{/* Work Mode Selector */}
{/* Dependencies - only show when not in spawn mode */} {!isSpawnMode && allFeatures.length > 0 && (
)} {/* Pipeline Exclusion Controls */}
{onAddAndStart && ( )} {isSpawnMode ? 'Spawn Task' : 'Add Feature'}
); }