mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-22 11:43:07 +00:00
- Added `isAdaptiveThinkingModel` utility to improve model identification logic in the AddFeatureDialog. - Updated the ThinkingLevelSelector to conditionally display information based on available thinking levels. - Enhanced model name formatting in agent-context-parser to include 'GPT-5.3 Codex' for better clarity. These changes improve the user experience by refining model handling and UI feedback related to adaptive thinking capabilities.
734 lines
26 KiB
TypeScript
734 lines
26 KiB
TypeScript
// @ts-nocheck - feature data building with conditional fields and model type inference
|
|
import { useState, useEffect, useRef } from 'react';
|
|
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, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store';
|
|
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
|
|
import { supportsReasoningEffort, isAdaptiveThinkingModel } from '@automaker/types';
|
|
import {
|
|
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';
|
|
|
|
/**
|
|
* 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<string, number>;
|
|
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<WorkMode>('current');
|
|
|
|
// Form state
|
|
const [title, setTitle] = useState('');
|
|
const [category, setCategory] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [images, setImages] = useState<FeatureImage[]>([]);
|
|
const [imagePaths, setImagePaths] = useState<DescriptionImagePath[]>([]);
|
|
const [textFilePaths, setTextFilePaths] = useState<DescriptionTextFilePath[]>([]);
|
|
const [skipTests, setSkipTests] = useState(false);
|
|
const [branchName, setBranchName] = useState('');
|
|
const [priority, setPriority] = useState(2);
|
|
|
|
// Model selection state
|
|
const [modelEntry, setModelEntry] = useState<PhaseModelEntry>({ model: 'claude-opus' });
|
|
|
|
// Planning mode state
|
|
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
|
|
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
|
|
|
|
// UI state
|
|
const [previewMap, setPreviewMap] = useState<ImagePreviewMap>(() => new Map());
|
|
const [descriptionError, setDescriptionError] = useState(false);
|
|
|
|
// Description history state
|
|
const [descriptionHistory, setDescriptionHistory] = useState<DescriptionHistoryEntry[]>([]);
|
|
|
|
// Spawn mode state
|
|
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
|
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
|
|
|
// Dependency selection state (not in spawn mode)
|
|
const [parentDependencies, setParentDependencies] = useState<string[]>([]);
|
|
const [childDependencies, setChildDependencies] = useState<string[]>([]);
|
|
|
|
// Pipeline exclusion state
|
|
const [excludedPipelineSteps, setExcludedPipelineSteps] = useState<string[]>([]);
|
|
|
|
// 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,
|
|
]);
|
|
|
|
// Clear requirePlanApproval when planning mode is skip or lite
|
|
useEffect(() => {
|
|
if (planningMode === 'skip' || planningMode === 'lite') {
|
|
setRequirePlanApproval(false);
|
|
}
|
|
}, [planningMode]);
|
|
|
|
const handleModelChange = (entry: PhaseModelEntry) => {
|
|
// Normalize thinking level when switching between adaptive and non-adaptive models
|
|
const isNewModelAdaptive =
|
|
typeof entry.model === 'string' && isAdaptiveThinkingModel(entry.model);
|
|
const currentLevel = entry.thinkingLevel || 'none';
|
|
|
|
if (isNewModelAdaptive && currentLevel !== 'none' && currentLevel !== 'adaptive') {
|
|
// Switching TO Opus 4.6 with a manual level -> auto-switch to 'adaptive'
|
|
setModelEntry({ ...entry, thinkingLevel: 'adaptive' });
|
|
} else if (!isNewModelAdaptive && currentLevel === 'adaptive') {
|
|
// Switching FROM Opus 4.6 with adaptive -> auto-switch to 'high'
|
|
setModelEntry({ ...entry, thinkingLevel: 'high' });
|
|
} else {
|
|
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 (
|
|
<Dialog open={open} onOpenChange={handleDialogClose}>
|
|
<DialogContent
|
|
compact={!isMaximized}
|
|
data-testid="add-feature-dialog"
|
|
onPointerDownOutside={(e: CustomEvent) => {
|
|
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();
|
|
}
|
|
}}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>{isSpawnMode ? 'Spawn Sub-Task' : 'Add New Feature'}</DialogTitle>
|
|
<DialogDescription>
|
|
{isSpawnMode
|
|
? `Create a sub-task that depends on "${parentFeature?.title || parentFeature?.description.slice(0, 50)}..."`
|
|
: 'Create a new feature card for the Kanban board.'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="py-4 space-y-4 overflow-y-auto flex-1 min-h-0">
|
|
{/* Ancestor Context Section - only in spawn mode */}
|
|
{isSpawnMode && parentFeature && (
|
|
<AncestorContextSection
|
|
parentFeature={{
|
|
id: parentFeature.id,
|
|
title: parentFeature.title,
|
|
description: parentFeature.description,
|
|
spec: parentFeature.spec,
|
|
summary: parentFeature.summary,
|
|
}}
|
|
ancestors={ancestors}
|
|
selectedAncestorIds={selectedAncestorIds}
|
|
onSelectionChange={setSelectedAncestorIds}
|
|
/>
|
|
)}
|
|
|
|
{/* Task Details Section */}
|
|
<div className={cardClass}>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="description">Description</Label>
|
|
{/* Version History Button */}
|
|
<EnhancementHistoryButton
|
|
history={descriptionHistory}
|
|
currentValue={description}
|
|
onRestore={setDescription}
|
|
valueAccessor={(entry) => entry.description}
|
|
title="Version History"
|
|
restoreMessage="Description restored from history"
|
|
/>
|
|
</div>
|
|
<DescriptionImageDropZone
|
|
value={description}
|
|
onChange={(value) => {
|
|
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}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="title">Title (optional)</Label>
|
|
<Input
|
|
id="title"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Leave blank to auto-generate"
|
|
/>
|
|
</div>
|
|
|
|
{/* Enhancement Section */}
|
|
<EnhanceWithAI
|
|
value={description}
|
|
onChange={setDescription}
|
|
onHistoryAdd={({ mode, originalText, enhancedText }) => {
|
|
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;
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* AI & Execution Section */}
|
|
<div className={cardClass}>
|
|
<div className="flex items-center justify-between">
|
|
<div className={sectionHeaderClass}>
|
|
<Cpu className="w-4 h-4 text-muted-foreground" />
|
|
<span>AI & Execution</span>
|
|
</div>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onOpenChange(false);
|
|
navigate({ to: '/settings', search: { view: 'defaults' } });
|
|
}}
|
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<Settings2 className="w-3.5 h-3.5" />
|
|
<span>Edit Defaults</span>
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Change default model and planning settings for new features</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Model</Label>
|
|
<PhaseModelSelector
|
|
value={modelEntry}
|
|
onChange={handleModelChange}
|
|
compact
|
|
align="end"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-3 grid-cols-2">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Planning</Label>
|
|
<PlanningModeSelect
|
|
mode={planningMode}
|
|
onModeChange={setPlanningMode}
|
|
testIdPrefix="add-feature-planning"
|
|
compact
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Options</Label>
|
|
<div className="flex flex-col gap-2 pt-1">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="add-feature-skip-tests"
|
|
checked={!skipTests}
|
|
onCheckedChange={(checked) => setSkipTests(!checked)}
|
|
data-testid="add-feature-skip-tests-checkbox"
|
|
/>
|
|
<Label
|
|
htmlFor="add-feature-skip-tests"
|
|
className="text-xs font-normal cursor-pointer"
|
|
>
|
|
Run tests
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="add-feature-require-approval"
|
|
checked={requirePlanApproval}
|
|
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
|
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
|
data-testid="add-feature-planning-require-approval-checkbox"
|
|
/>
|
|
<Label
|
|
htmlFor="add-feature-require-approval"
|
|
className={cn(
|
|
'text-xs font-normal',
|
|
planningMode === 'skip' || planningMode === 'lite'
|
|
? 'cursor-not-allowed text-muted-foreground'
|
|
: 'cursor-pointer'
|
|
)}
|
|
>
|
|
Require approval
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Organization Section */}
|
|
<div className={cardClass}>
|
|
<div className={sectionHeaderClass}>
|
|
<FolderKanban className="w-4 h-4 text-muted-foreground" />
|
|
<span>Organization</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Category</Label>
|
|
<CategoryAutocomplete
|
|
value={category}
|
|
onChange={setCategory}
|
|
suggestions={categorySuggestions}
|
|
placeholder="e.g., Core, UI, API"
|
|
data-testid="feature-category-input"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Priority</Label>
|
|
<PrioritySelector
|
|
selectedPriority={priority}
|
|
onPrioritySelect={setPriority}
|
|
testIdPrefix="priority"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Work Mode Selector */}
|
|
<div className="pt-2">
|
|
<WorkModeSelector
|
|
workMode={workMode}
|
|
onWorkModeChange={setWorkMode}
|
|
branchName={branchName}
|
|
onBranchNameChange={setBranchName}
|
|
branchSuggestions={branchSuggestions}
|
|
branchCardCounts={branchCardCounts}
|
|
currentBranch={currentBranch}
|
|
testIdPrefix="feature-work-mode"
|
|
/>
|
|
</div>
|
|
|
|
{/* Dependencies - only show when not in spawn mode */}
|
|
{!isSpawnMode && allFeatures.length > 0 && (
|
|
<div className="pt-2 space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Parent Dependencies (this feature depends on)
|
|
</Label>
|
|
<DependencySelector
|
|
value={parentDependencies}
|
|
onChange={setParentDependencies}
|
|
features={allFeatures}
|
|
type="parent"
|
|
placeholder="Select features this depends on..."
|
|
data-testid="add-feature-parent-deps"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Child Dependencies (features that depend on this)
|
|
</Label>
|
|
<DependencySelector
|
|
value={childDependencies}
|
|
onChange={setChildDependencies}
|
|
features={allFeatures}
|
|
type="child"
|
|
placeholder="Select features that will depend on this..."
|
|
data-testid="add-feature-child-deps"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pipeline Exclusion Controls */}
|
|
<div className="pt-2">
|
|
<PipelineExclusionControls
|
|
projectPath={projectPath}
|
|
excludedPipelineSteps={excludedPipelineSteps}
|
|
onExcludedStepsChange={setExcludedPipelineSteps}
|
|
testIdPrefix="add-feature-pipeline"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
{onAddAndStart && (
|
|
<Button
|
|
onClick={handleAddAndStart}
|
|
variant="secondary"
|
|
data-testid="confirm-add-and-start-feature"
|
|
disabled={workMode === 'custom' && !branchName.trim()}
|
|
>
|
|
<Play className="w-4 h-4 mr-2" />
|
|
Make
|
|
</Button>
|
|
)}
|
|
<HotkeyButton
|
|
onClick={handleAdd}
|
|
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
|
hotkeyActive={open}
|
|
data-testid="confirm-add-feature"
|
|
disabled={workMode === 'custom' && !branchName.trim()}
|
|
>
|
|
{isSpawnMode ? 'Spawn Task' : 'Add Feature'}
|
|
</HotkeyButton>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|