mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
This commit introduces significant updates to the cursor model handling and auto mode features. The cursor model IDs have been standardized to a canonical format, ensuring backward compatibility while migrating legacy IDs. New endpoints for starting and stopping the auto mode loop have been added, allowing for better control over project-specific auto mode operations. Key changes: - Updated cursor model IDs to use the 'cursor-' prefix for consistency. - Added new API endpoints: `/start` and `/stop` for managing auto mode. - Enhanced the status endpoint to provide detailed project-specific auto mode information. - Improved error handling and logging throughout the auto mode service. - Migrated legacy model IDs to their canonical counterparts in various components. This update aims to streamline the user experience and ensure a smooth transition for existing users while providing new functionalities.
731 lines
26 KiB
TypeScript
731 lines
26 KiB
TypeScript
// @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,
|
|
type BaseHistoryEntry,
|
|
} from '../shared';
|
|
import type { WorkMode } from '../shared';
|
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
|
import { Tooltip, TooltipContent, TooltipProvider, 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
|
|
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[];
|
|
/**
|
|
* 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 = [],
|
|
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' });
|
|
|
|
// Check if current model supports planning mode (Claude/Anthropic only)
|
|
const modelSupportsPlanningMode = isClaudeModel(modelEntry.model);
|
|
|
|
// 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[]>([]);
|
|
|
|
// Get defaults from store
|
|
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
|
|
useAppStore();
|
|
|
|
// 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(defaultFeatureModel);
|
|
|
|
// 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([]);
|
|
}
|
|
}, [
|
|
open,
|
|
defaultSkipTests,
|
|
defaultBranch,
|
|
defaultPlanningMode,
|
|
defaultRequirePlanApproval,
|
|
defaultFeatureModel,
|
|
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,
|
|
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(defaultFeatureModel);
|
|
setWorkMode(
|
|
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
|
|
);
|
|
setPlanningMode(defaultPlanningMode);
|
|
setRequirePlanApproval(defaultRequirePlanApproval);
|
|
setPreviewMap(new Map());
|
|
setDescriptionError(false);
|
|
setDescriptionHistory([]);
|
|
setParentDependencies([]);
|
|
setChildDependencies([]);
|
|
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>
|
|
<TooltipProvider>
|
|
<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>
|
|
</TooltipProvider>
|
|
</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={cn(
|
|
'text-xs text-muted-foreground',
|
|
!modelSupportsPlanningMode && 'opacity-50'
|
|
)}
|
|
>
|
|
Planning
|
|
</Label>
|
|
{modelSupportsPlanningMode ? (
|
|
<PlanningModeSelect
|
|
mode={planningMode}
|
|
onModeChange={setPlanningMode}
|
|
testIdPrefix="add-feature-planning"
|
|
compact
|
|
/>
|
|
) : (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div>
|
|
<PlanningModeSelect
|
|
mode="skip"
|
|
onModeChange={() => {}}
|
|
testIdPrefix="add-feature-planning"
|
|
compact
|
|
disabled
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Planning modes are only available for Claude Provider</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)}
|
|
</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={
|
|
!modelSupportsPlanningMode ||
|
|
planningMode === 'skip' ||
|
|
planningMode === 'lite'
|
|
}
|
|
data-testid="add-feature-require-approval-checkbox"
|
|
/>
|
|
<Label
|
|
htmlFor="add-feature-require-approval"
|
|
className={cn(
|
|
'text-xs font-normal',
|
|
!modelSupportsPlanningMode ||
|
|
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>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|