import { useState, useEffect, useMemo } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { AlertCircle } from 'lucide-react'; import { modelSupportsThinking } from '@/lib/utils'; import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store'; import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector, PipelineExclusionControls, } from '../shared'; import type { WorkMode } from '../shared'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface MassEditDialogProps { open: boolean; onClose: () => void; selectedFeatures: Feature[]; onApply: (updates: Partial, workMode: WorkMode) => Promise; branchSuggestions: string[]; branchCardCounts?: Record; currentBranch?: string; projectPath?: string; } interface ApplyState { model: boolean; thinkingLevel: boolean; planningMode: boolean; requirePlanApproval: boolean; priority: boolean; skipTests: boolean; branchName: boolean; excludedPipelineSteps: boolean; } function getMixedValues(features: Feature[]): Record { if (features.length === 0) return {}; const first = features[0]; const firstExcludedSteps = JSON.stringify(first.excludedPipelineSteps || []); return { model: !features.every((f) => f.model === first.model), thinkingLevel: !features.every((f) => f.thinkingLevel === first.thinkingLevel), planningMode: !features.every((f) => f.planningMode === first.planningMode), requirePlanApproval: !features.every( (f) => f.requirePlanApproval === first.requirePlanApproval ), priority: !features.every((f) => f.priority === first.priority), skipTests: !features.every((f) => f.skipTests === first.skipTests), branchName: !features.every((f) => f.branchName === first.branchName), excludedPipelineSteps: !features.every( (f) => JSON.stringify(f.excludedPipelineSteps || []) === firstExcludedSteps ), }; } function getInitialValue(features: Feature[], key: keyof Feature, defaultValue: T): T { if (features.length === 0) return defaultValue; return (features[0][key] as T) ?? defaultValue; } interface FieldWrapperProps { label: string; isMixed: boolean; willApply: boolean; onApplyChange: (apply: boolean) => void; children: React.ReactNode; } function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: FieldWrapperProps) { return (
onApplyChange(!!checked)} className="data-[state=checked]:bg-brand-500 data-[state=checked]:border-brand-500" />
{isMixed && ( Mixed values )}
{children}
); } export function MassEditDialog({ open, onClose, selectedFeatures, onApply, branchSuggestions, branchCardCounts, currentBranch, projectPath, }: MassEditDialogProps) { const [isApplying, setIsApplying] = useState(false); // Track which fields to apply const [applyState, setApplyState] = useState({ model: false, thinkingLevel: false, planningMode: false, requirePlanApproval: false, priority: false, skipTests: false, branchName: false, excludedPipelineSteps: false, }); // Field values const [model, setModel] = useState('claude-sonnet'); const [thinkingLevel, setThinkingLevel] = useState('none'); const [providerId, setProviderId] = useState(undefined); const [planningMode, setPlanningMode] = useState('skip'); const [requirePlanApproval, setRequirePlanApproval] = useState(false); const [priority, setPriority] = useState(2); const [skipTests, setSkipTests] = useState(false); // Work mode and branch name state const [workMode, setWorkMode] = useState(() => { // Derive initial work mode from first selected feature's branchName if (selectedFeatures.length > 0 && selectedFeatures[0].branchName) { return 'custom'; } return 'current'; }); const [branchName, setBranchName] = useState(() => { return getInitialValue(selectedFeatures, 'branchName', '') as string; }); // Pipeline exclusion state const [excludedPipelineSteps, setExcludedPipelineSteps] = useState(() => { return getInitialValue(selectedFeatures, 'excludedPipelineSteps', []) as string[]; }); // Calculate mixed values const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]); // Reset state when dialog opens with new features useEffect(() => { if (open && selectedFeatures.length > 0) { setApplyState({ model: false, thinkingLevel: false, planningMode: false, requirePlanApproval: false, priority: false, skipTests: false, branchName: false, excludedPipelineSteps: false, }); setModel(getInitialValue(selectedFeatures, 'model', 'claude-sonnet') as ModelAlias); setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel); setProviderId(undefined); // Features don't store providerId, but we track it after selection setPlanningMode(getInitialValue(selectedFeatures, 'planningMode', 'skip') as PlanningMode); setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false)); setPriority(getInitialValue(selectedFeatures, 'priority', 2)); setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false)); // Reset work mode and branch name const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string; setBranchName(initialBranchName); setWorkMode(initialBranchName ? 'custom' : 'current'); // Reset pipeline exclusions setExcludedPipelineSteps( getInitialValue(selectedFeatures, 'excludedPipelineSteps', []) as string[] ); } }, [open, selectedFeatures]); const handleApply = async () => { const updates: Partial = {}; if (applyState.model) updates.model = model; if (applyState.thinkingLevel) updates.thinkingLevel = thinkingLevel; if (applyState.planningMode) updates.planningMode = planningMode; if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval; if (applyState.priority) updates.priority = priority; if (applyState.skipTests) updates.skipTests = skipTests; if (applyState.branchName) { // For 'current' mode, use empty string (work on current branch) // For 'auto' mode, use empty string (will be auto-generated) // For 'custom' mode, use the specified branch name updates.branchName = workMode === 'custom' ? branchName : ''; } if (applyState.excludedPipelineSteps) { updates.excludedPipelineSteps = excludedPipelineSteps.length > 0 ? excludedPipelineSteps : undefined; } if (Object.keys(updates).length === 0) { onClose(); return; } setIsApplying(true); try { await onApply(updates, workMode); onClose(); } finally { setIsApplying(false); } }; const hasAnyApply = Object.values(applyState).some(Boolean); const isCurrentModelCursor = isCursorModel(model); const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model); const modelSupportsPlanningMode = isClaudeModel(model); return ( !open && onClose()}> Edit {selectedFeatures.length} Features Select which settings to apply to all selected features.
{/* Model Selector */}

Select a specific model configuration

{ setModel(entry.model as ModelAlias); setThinkingLevel(entry.thinkingLevel || 'none'); setProviderId(entry.providerId); // Auto-enable model and thinking level for apply state setApplyState((prev) => ({ ...prev, model: true, thinkingLevel: true, })); }} compact />
{/* Separator */}
{/* Planning Mode */} {modelSupportsPlanningMode ? ( setApplyState((prev) => ({ ...prev, planningMode: apply, requirePlanApproval: apply, })) } > { setPlanningMode(newMode); // Auto-suggest approval based on mode, but user can override setRequirePlanApproval(newMode === 'spec' || newMode === 'full'); }} requireApproval={requirePlanApproval} onRequireApprovalChange={setRequirePlanApproval} testIdPrefix="mass-edit-planning" /> ) : (
{}} testIdPrefix="mass-edit-planning" disabled />

Planning modes are only available for Claude Provider

)} {/* Priority */} setApplyState((prev) => ({ ...prev, priority: apply }))} > {/* Testing */} setApplyState((prev) => ({ ...prev, skipTests: apply }))} > {/* Branch / Work Mode */} setApplyState((prev) => ({ ...prev, branchName: apply }))} > {/* Pipeline Exclusion */} setApplyState((prev) => ({ ...prev, excludedPipelineSteps: apply })) } >
); }