diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index cfa063fd..0541de9f 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -60,9 +60,6 @@ import { // Stable empty array to avoid infinite loop in selector const EMPTY_WORKTREES: ReturnType['getWorktrees']> = []; -/** Delay before starting a newly created feature to allow state to settle */ -const FEATURE_CREATION_SETTLE_DELAY_MS = 500; - export function BoardView() { const { currentProject, @@ -461,23 +458,22 @@ export function BoardView() { requirePlanApproval: false, }; + // Capture existing feature IDs before adding + const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id)); await handleAddFeature(featureData); - // Find the newly created feature and start it - // We need to wait a moment for the feature to be created - setTimeout(async () => { - const latestFeatures = useAppStore.getState().features; - const newFeature = latestFeatures.find( - (f) => - f.branchName === worktree.branch && - f.status === 'backlog' && - f.description.includes(`PR #${prNumber}`) - ); + // Find the newly created feature by looking for an ID that wasn't in the original set + const latestFeatures = useAppStore.getState().features; + const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id)); - if (newFeature) { - await handleStartImplementation(newFeature); - } - }, FEATURE_CREATION_SETTLE_DELAY_MS); + if (newFeature) { + await handleStartImplementation(newFeature); + } else { + console.error('Could not find newly created feature to start it automatically.'); + toast.error('Failed to auto-start feature', { + description: 'The feature was created but could not be started automatically.', + }); + } }, [handleAddFeature, handleStartImplementation, defaultSkipTests] ); @@ -503,26 +499,49 @@ export function BoardView() { requirePlanApproval: false, }; + // Capture existing feature IDs before adding + const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id)); await handleAddFeature(featureData); - // Find the newly created feature and start it - setTimeout(async () => { - const latestFeatures = useAppStore.getState().features; - const newFeature = latestFeatures.find( - (f) => - f.branchName === worktree.branch && - f.status === 'backlog' && - f.description.includes('Pull latest from origin/main') - ); + // Find the newly created feature by looking for an ID that wasn't in the original set + const latestFeatures = useAppStore.getState().features; + const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id)); - if (newFeature) { - await handleStartImplementation(newFeature); - } - }, FEATURE_CREATION_SETTLE_DELAY_MS); + if (newFeature) { + await handleStartImplementation(newFeature); + } else { + console.error('Could not find newly created feature to start it automatically.'); + toast.error('Failed to auto-start feature', { + description: 'The feature was created but could not be started automatically.', + }); + } }, [handleAddFeature, handleStartImplementation, defaultSkipTests] ); + // Handler for "Make" button - creates a feature and immediately starts it + const handleAddAndStartFeature = useCallback( + async (featureData: Parameters[0]) => { + // Capture existing feature IDs before adding + const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id)); + await handleAddFeature(featureData); + + // Find the newly created feature by looking for an ID that wasn't in the original set + const latestFeatures = useAppStore.getState().features; + const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id)); + + if (newFeature) { + await handleStartImplementation(newFeature); + } else { + console.error('Could not find newly created feature to start it automatically.'); + toast.error('Failed to auto-start feature', { + description: 'The feature was created but could not be started automatically.', + }); + } + }, + [handleAddFeature, handleStartImplementation] + ); + // Client-side auto mode: periodically check for backlog items and move them to in-progress // Use a ref to track the latest auto mode state so async operations always check the current value const autoModeRunningRef = useRef(autoMode.isRunning); @@ -1137,6 +1156,7 @@ export function BoardView() { } }} onAdd={handleAddFeature} + onAddAndStart={handleAddAndStartFeature} categorySuggestions={categorySuggestions} branchSuggestions={branchSuggestions} branchCardCounts={branchCardCounts} diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx index a5eea2c5..35a95e91 100644 --- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -19,7 +19,14 @@ import { FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; -import { MessageSquare, Settings2, SlidersHorizontal, Sparkles, ChevronDown } from 'lucide-react'; +import { + MessageSquare, + Settings2, + SlidersHorizontal, + Sparkles, + ChevronDown, + Play, +} from 'lucide-react'; import { toast } from 'sonner'; import { getElectronAPI } from '@/lib/electron'; import { modelSupportsThinking } from '@/lib/utils'; @@ -55,25 +62,28 @@ import { 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: { - 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[]; - }) => void; + onAdd: (feature: FeatureData) => void; + onAddAndStart?: (feature: FeatureData) => void; categorySuggestions: string[]; branchSuggestions: string[]; branchCardCounts?: Record; // Map of branch name to unarchived card count @@ -92,6 +102,7 @@ export function AddFeatureDialog({ open, onOpenChange, onAdd, + onAddAndStart, categorySuggestions, branchSuggestions, branchCardCounts, @@ -188,16 +199,16 @@ export function AddFeatureDialog({ allFeatures, ]); - const handleAdd = () => { + const buildFeatureData = (): FeatureData | null => { if (!newFeature.description.trim()) { setDescriptionError(true); - return; + return null; } // Validate branch selection when "other branch" is selected if (useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()) { toast.error('Please select a branch name'); - return; + return null; } const category = newFeature.category || 'Uncategorized'; @@ -235,7 +246,7 @@ export function AddFeatureDialog({ } } - onAdd({ + return { title: newFeature.title, category, description: finalDescription, @@ -251,9 +262,10 @@ export function AddFeatureDialog({ requirePlanApproval, // In spawn mode, automatically add parent as dependency dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined, - }); + }; + }; - // Reset form + const resetForm = () => { setNewFeature({ title: '', category: '', @@ -276,6 +288,20 @@ export function AddFeatureDialog({ 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) { @@ -575,6 +601,17 @@ export function AddFeatureDialog({ + {onAddAndStart && ( + + )}