diff --git a/apps/frontend/src/renderer/components/TaskCreationWizard.tsx b/apps/frontend/src/renderer/components/TaskCreationWizard.tsx index 7f67f3e2b3..aa0fd3c431 100644 --- a/apps/frontend/src/renderer/components/TaskCreationWizard.tsx +++ b/apps/frontend/src/renderer/components/TaskCreationWizard.tsx @@ -1,19 +1,20 @@ -import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent, type DragEvent } from 'react'; +/** + * TaskCreationWizard - Dialog for creating new tasks + * + * Now uses the shared TaskModalLayout for consistent styling with other task modals, + * and TaskFormFields for the form content. + * + * Features unique to creation (not in TaskEditDialog): + * - Draft persistence (auto-save to localStorage) + * - @ mention autocomplete for file references + * - File explorer drawer sidebar + * - Git branch selection options + */ +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Loader2, ChevronDown, ChevronUp, Image as ImageIcon, X, RotateCcw, FolderTree, GitBranch } from 'lucide-react'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from './ui/dialog'; +import { Loader2, ChevronDown, ChevronUp, RotateCcw, FolderTree, GitBranch } from 'lucide-react'; import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Textarea } from './ui/textarea'; import { Label } from './ui/label'; -import { Checkbox } from './ui/checkbox'; import { Select, SelectContent, @@ -21,15 +22,9 @@ import { SelectTrigger, SelectValue } from './ui/select'; -import { - generateImageId, - blobToBase64, - createThumbnail, - isValidImageMimeType, - resolveFilename -} from './ImageUpload'; +import { TaskModalLayout } from './task-form/TaskModalLayout'; +import { TaskFormFields } from './task-form/TaskFormFields'; import { TaskFileExplorerDrawer } from './TaskFileExplorerDrawer'; -import { AgentProfileSelector } from './AgentProfileSelector'; import { FileAutocomplete } from './FileAutocomplete'; import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store'; import { useProjectStore } from '../stores/project-store'; @@ -37,12 +32,6 @@ import { cn } from '../lib/utils'; import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft, ModelType, ThinkingLevel, ReferencedFile } from '../../shared/types'; import type { PhaseModelConfig, PhaseThinkingConfig } from '../../shared/types/settings'; import { - TASK_CATEGORY_LABELS, - TASK_PRIORITY_LABELS, - TASK_COMPLEXITY_LABELS, - TASK_IMPACT_LABELS, - MAX_IMAGES_PER_TASK, - ALLOWED_IMAGE_TYPES_DISPLAY, DEFAULT_AGENT_PROFILES, DEFAULT_PHASE_MODELS, DEFAULT_PHASE_THINKING @@ -55,29 +44,30 @@ interface TaskCreationWizardProps { onOpenChange: (open: boolean) => void; } +// Special value for "use project default" branch +const PROJECT_DEFAULT_BRANCH = '__project_default__'; + export function TaskCreationWizard({ projectId, open, onOpenChange }: TaskCreationWizardProps) { - const { t } = useTranslation('tasks'); - // Get selected agent profile from settings + const { t } = useTranslation(['tasks', 'common']); const { settings } = useSettingsStore(); const selectedProfile = DEFAULT_AGENT_PROFILES.find( p => p.id === settings.selectedAgentProfile ) || DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto')!; + // Form state const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(null); - const [showAdvanced, setShowAdvanced] = useState(false); + const [showClassification, setShowClassification] = useState(false); const [showFileExplorer, setShowFileExplorer] = useState(false); const [showGitOptions, setShowGitOptions] = useState(false); // Git options state - // Use a special value to represent "use project default" since Radix UI Select doesn't allow empty string values - const PROJECT_DEFAULT_BRANCH = '__project_default__'; const [branches, setBranches] = useState([]); const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [baseBranch, setBaseBranch] = useState(PROJECT_DEFAULT_BRANCH); @@ -92,18 +82,16 @@ export function TaskCreationWizard({ return project?.path ?? null; }, [projects, projectId]); - // Metadata fields + // Classification fields const [category, setCategory] = useState(''); const [priority, setPriority] = useState(''); const [complexity, setComplexity] = useState(''); const [impact, setImpact] = useState(''); - // Model configuration (initialized from selected agent profile) + // Model configuration const [profileId, setProfileId] = useState(settings.selectedAgentProfile || 'auto'); const [model, setModel] = useState(selectedProfile.model); const [thinkingLevel, setThinkingLevel] = useState(selectedProfile.thinkingLevel); - // Auto profile - per-phase configuration - // Use custom settings from app settings if available, otherwise fall back to defaults const [phaseModels, setPhaseModels] = useState( settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS ); @@ -111,10 +99,8 @@ export function TaskCreationWizard({ settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING ); - // Image attachments + // Images and files const [images, setImages] = useState([]); - - // Referenced files from file explorer const [referencedFiles, setReferencedFiles] = useState([]); // Review setting @@ -122,18 +108,9 @@ export function TaskCreationWizard({ // Draft state const [isDraftRestored, setIsDraftRestored] = useState(false); - const [pasteSuccess, setPasteSuccess] = useState(false); - - // Ref for the textarea to handle paste events - const descriptionRef = useRef(null); - - // Ref for the form scroll container (for drag auto-scroll) - const formContainerRef = useRef(null); - - // Drag-and-drop state for images over textarea - const [isDragOverTextarea, setIsDragOverTextarea] = useState(false); // @ autocomplete state + const descriptionRef = useRef(null); const [autocomplete, setAutocomplete] = useState<{ show: boolean; query: string; @@ -141,7 +118,7 @@ export function TaskCreationWizard({ position: { top: number; left: number }; } | null>(null); - // Load draft when dialog opens, or initialize from selected profile + // Load draft when dialog opens useEffect(() => { if (open && projectId) { const draft = loadDraft(projectId); @@ -152,7 +129,6 @@ export function TaskCreationWizard({ setPriority(draft.priority); setComplexity(draft.complexity); setImpact(draft.impact); - // Load model/thinkingLevel/profileId from draft if present, otherwise use profile defaults setProfileId(draft.profileId || settings.selectedAgentProfile || 'auto'); setModel(draft.model || selectedProfile.model); setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel); @@ -163,12 +139,11 @@ export function TaskCreationWizard({ setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false); setIsDraftRestored(true); - // Expand sections if they have content if (draft.category || draft.priority || draft.complexity || draft.impact) { - setShowAdvanced(true); + setShowClassification(true); } } else { - // No draft - initialize from selected profile and custom settings + // No draft - initialize from selected profile setProfileId(settings.selectedAgentProfile || 'auto'); setModel(selectedProfile.model); setThinkingLevel(selectedProfile.thinkingLevel); @@ -176,52 +151,53 @@ export function TaskCreationWizard({ setPhaseThinking(settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING); } } - }, [open, projectId, settings.selectedAgentProfile, settings.customPhaseModels, settings.customPhaseThinking, selectedProfile.model, selectedProfile.thinkingLevel]); + }, [open, projectId, settings.selectedAgentProfile, settings.customPhaseModels, settings.customPhaseThinking, selectedProfile.model, selectedProfile.thinkingLevel, selectedProfile.phaseModels, selectedProfile.phaseThinking]); - // Fetch branches and project default branch when dialog opens + // Fetch branches when dialog opens useEffect(() => { - if (open && projectPath) { - fetchBranches(); - fetchProjectDefaultBranch(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, projectPath]); - - const fetchBranches = async () => { - if (!projectPath) return; + let isMounted = true; - setIsLoadingBranches(true); - try { - const result = await window.electronAPI.getGitBranches(projectPath); - if (result.success && result.data) { - setBranches(result.data); + const fetchBranches = async () => { + if (!projectPath) return; + if (isMounted) setIsLoadingBranches(true); + try { + const result = await window.electronAPI.getGitBranches(projectPath); + if (isMounted && result.success && result.data) { + setBranches(result.data); + } + } catch (err) { + console.error('Failed to fetch branches:', err); + } finally { + if (isMounted) setIsLoadingBranches(false); } - } catch (err) { - console.error('Failed to fetch branches:', err); - } finally { - setIsLoadingBranches(false); - } - }; + }; - const fetchProjectDefaultBranch = async () => { - if (!projectId) return; - - try { - // Get env config to check if there's a configured default branch - const result = await window.electronAPI.getProjectEnv(projectId); - if (result.success && result.data?.defaultBranch) { - setProjectDefaultBranch(result.data.defaultBranch); - } else if (projectPath) { - // Fall back to auto-detect - const detectResult = await window.electronAPI.detectMainBranch(projectPath); - if (detectResult.success && detectResult.data) { - setProjectDefaultBranch(detectResult.data); + const fetchProjectDefaultBranch = async () => { + if (!projectId) return; + try { + const result = await window.electronAPI.getProjectEnv(projectId); + if (isMounted && result.success && result.data?.defaultBranch) { + setProjectDefaultBranch(result.data.defaultBranch); + } else if (projectPath) { + const detectResult = await window.electronAPI.detectMainBranch(projectPath); + if (isMounted && detectResult.success && detectResult.data) { + setProjectDefaultBranch(detectResult.data); + } } + } catch (err) { + console.error('Failed to fetch project default branch:', err); } - } catch (err) { - console.error('Failed to fetch project default branch:', err); + }; + + if (open && projectPath) { + fetchBranches(); + fetchProjectDefaultBranch(); } - }; + + return () => { + isMounted = false; + }; + }, [open, projectPath, projectId]); /** * Get current form state as a draft @@ -244,97 +220,15 @@ export function TaskCreationWizard({ requireReviewBeforeCoding, savedAt: new Date() }), [projectId, title, description, category, priority, complexity, impact, profileId, model, thinkingLevel, phaseModels, phaseThinking, images, referencedFiles, requireReviewBeforeCoding]); - /** - * Handle paste event for screenshot support - */ - const handlePaste = useCallback(async (e: ClipboardEvent) => { - const clipboardItems = e.clipboardData?.items; - if (!clipboardItems) return; - - // Find image items in clipboard - const imageItems: DataTransferItem[] = []; - for (let i = 0; i < clipboardItems.length; i++) { - const item = clipboardItems[i]; - if (item.type.startsWith('image/')) { - imageItems.push(item); - } - } - - // If no images, allow normal paste behavior - if (imageItems.length === 0) return; - - // Prevent default paste when we have images - e.preventDefault(); - - // Check if we can add more images - const remainingSlots = MAX_IMAGES_PER_TASK - images.length; - if (remainingSlots <= 0) { - setError(`Maximum of ${MAX_IMAGES_PER_TASK} images allowed`); - return; - } - - setError(null); - - // Process image items - const newImages: ImageAttachment[] = []; - const existingFilenames = images.map(img => img.filename); - - for (const item of imageItems.slice(0, remainingSlots)) { - const file = item.getAsFile(); - if (!file) continue; - - // Validate image type - if (!isValidImageMimeType(file.type)) { - setError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`); - continue; - } - - try { - const dataUrl = await blobToBase64(file); - const thumbnail = await createThumbnail(dataUrl); - - // Generate filename for pasted images (screenshot-timestamp.ext) - const extension = file.type.split('/')[1] || 'png'; - const baseFilename = `screenshot-${Date.now()}.${extension}`; - const resolvedFilename = resolveFilename(baseFilename, [ - ...existingFilenames, - ...newImages.map(img => img.filename) - ]); - - newImages.push({ - id: generateImageId(), - filename: resolvedFilename, - mimeType: file.type, - size: file.size, - data: dataUrl.split(',')[1], // Store base64 without data URL prefix - thumbnail - }); - } catch { - setError('Failed to process pasted image'); - } - } - - if (newImages.length > 0) { - setImages(prev => [...prev, ...newImages]); - // Show success feedback - setPasteSuccess(true); - setTimeout(() => setPasteSuccess(false), 2000); - } - }, [images]); /** * Detect @ mention being typed and show autocomplete */ const detectAtMention = useCallback((text: string, cursorPos: number) => { const beforeCursor = text.slice(0, cursorPos); - // Match @ followed by optional path characters (letters, numbers, dots, dashes, slashes) const match = beforeCursor.match(/@([\w\-./\\]*)$/); - if (match) { - return { - query: match[1], - startPos: cursorPos - match[0].length - }; + return { query: match[1], startPos: cursorPos - match[0].length }; } return null; }, []); @@ -342,61 +236,48 @@ export function TaskCreationWizard({ /** * Handle description change and check for @ mentions */ - const handleDescriptionChange = useCallback((e: React.ChangeEvent) => { - const newValue = e.target.value; - const cursorPos = e.target.selectionStart || 0; + const handleDescriptionChange = useCallback((newValue: string) => { + const textarea = descriptionRef.current; + const cursorPos = textarea?.selectionStart || 0; setDescription(newValue); - // Check for @ mention at cursor const mention = detectAtMention(newValue, cursorPos); - - if (mention) { - // Calculate popup position based on cursor - const textarea = descriptionRef.current; - if (textarea) { - const rect = textarea.getBoundingClientRect(); - const textareaStyle = window.getComputedStyle(textarea); - const lineHeight = parseFloat(textareaStyle.lineHeight) || 20; - const paddingTop = parseFloat(textareaStyle.paddingTop) || 8; - const paddingLeft = parseFloat(textareaStyle.paddingLeft) || 12; - - // Estimate cursor position (simplified - assumes fixed-width font) - const textBeforeCursor = newValue.slice(0, cursorPos); - const lines = textBeforeCursor.split('\n'); - const currentLineIndex = lines.length - 1; - const currentLineLength = lines[currentLineIndex].length; - - // Calculate position relative to textarea - const charWidth = 8; // Approximate character width - const top = paddingTop + (currentLineIndex + 1) * lineHeight + 4; - const left = paddingLeft + Math.min(currentLineLength * charWidth, rect.width - 300); - - setAutocomplete({ - show: true, - query: mention.query, - startPos: mention.startPos, - position: { top, left: Math.max(0, left) } - }); - } - } else { - // No @ mention at cursor, close autocomplete - if (autocomplete?.show) { - setAutocomplete(null); - } + if (mention && textarea) { + const rect = textarea.getBoundingClientRect(); + const textareaStyle = window.getComputedStyle(textarea); + const lineHeight = parseFloat(textareaStyle.lineHeight) || 20; + const paddingTop = parseFloat(textareaStyle.paddingTop) || 8; + const paddingLeft = parseFloat(textareaStyle.paddingLeft) || 12; + + const textBeforeCursor = newValue.slice(0, cursorPos); + const lines = textBeforeCursor.split('\n'); + const currentLineIndex = lines.length - 1; + const currentLineLength = lines[currentLineIndex].length; + + const charWidth = 8; + const top = paddingTop + (currentLineIndex + 1) * lineHeight + 4; + const left = paddingLeft + Math.min(currentLineLength * charWidth, rect.width - 300); + + setAutocomplete({ + show: true, + query: mention.query, + startPos: mention.startPos, + position: { top, left: Math.max(0, left) } + }); + } else if (autocomplete?.show) { + setAutocomplete(null); } }, [detectAtMention, autocomplete?.show]); /** * Handle autocomplete selection */ - const handleAutocompleteSelect = useCallback((filename: string) => { + const handleAutocompleteSelect = useCallback((filename: string, _fullPath?: string) => { if (!autocomplete) return; - const textarea = descriptionRef.current; if (!textarea) return; - // Replace the @query with @filename const beforeMention = description.slice(0, autocomplete.startPos); const afterMention = description.slice(autocomplete.startPos + 1 + autocomplete.query.length); const newDescription = beforeMention + '@' + filename + afterMention; @@ -404,198 +285,36 @@ export function TaskCreationWizard({ setDescription(newDescription); setAutocomplete(null); - // Set cursor after the inserted mention - setTimeout(() => { + // Use queueMicrotask instead of setTimeout - doesn't need cleanup on unmount + queueMicrotask(() => { const newCursorPos = autocomplete.startPos + 1 + filename.length; textarea.focus(); textarea.setSelectionRange(newCursorPos, newCursorPos); - }, 0); + }); }, [autocomplete, description]); /** - * Close autocomplete - */ - const handleAutocompleteClose = useCallback(() => { - setAutocomplete(null); - }, []); - - /** - * Handle drag over the form container to auto-scroll when dragging near edges - */ - const handleContainerDragOver = useCallback((e: DragEvent) => { - const container = formContainerRef.current; - if (!container) return; - - const rect = container.getBoundingClientRect(); - const edgeThreshold = 60; // px from edge to trigger scroll - const scrollSpeed = 8; - - // Auto-scroll when dragging near top or bottom edges - if (e.clientY < rect.top + edgeThreshold) { - container.scrollTop -= scrollSpeed; - } else if (e.clientY > rect.bottom - edgeThreshold) { - container.scrollTop += scrollSpeed; - } - }, []); - - /** - * Handle drag over textarea for image drops - */ - const handleTextareaDragOver = useCallback((e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOverTextarea(true); - }, []); - - /** - * Handle drag leave from textarea - */ - const handleTextareaDragLeave = useCallback((e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOverTextarea(false); - }, []); - - /** - * Handle drop on textarea for file references and images - */ - const handleTextareaDrop = useCallback( - async (e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOverTextarea(false); - - if (isCreating) return; - - // First, check for file reference drops (from the file explorer) - const jsonData = e.dataTransfer?.getData('application/json'); - if (jsonData) { - try { - const data = JSON.parse(jsonData); - if (data.type === 'file-reference' && data.name) { - // Insert @mention at cursor position in the textarea - const textarea = descriptionRef.current; - if (textarea) { - const cursorPos = textarea.selectionStart || 0; - const textBefore = description.substring(0, cursorPos); - const textAfter = description.substring(cursorPos); - - // Insert @mention at cursor position - const mention = `@${data.name}`; - const newDescription = textBefore + mention + textAfter; - setDescription(newDescription); - - // Set cursor after the inserted mention - setTimeout(() => { - textarea.focus(); - const newCursorPos = cursorPos + mention.length; - textarea.setSelectionRange(newCursorPos, newCursorPos); - }, 0); - - return; // Don't process as image - } - } - } catch { - // Not valid JSON, continue to image handling - } - } - - // Fall back to image file handling - const files = e.dataTransfer?.files; - if (!files || files.length === 0) return; - - // Filter for image files - const imageFiles: File[] = []; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (file.type.startsWith('image/')) { - imageFiles.push(file); - } - } - - if (imageFiles.length === 0) return; - - // Check if we can add more images - const remainingSlots = MAX_IMAGES_PER_TASK - images.length; - if (remainingSlots <= 0) { - setError(`Maximum of ${MAX_IMAGES_PER_TASK} images allowed`); - return; - } - - setError(null); - - // Process image files - const newImages: ImageAttachment[] = []; - const existingFilenames = images.map(img => img.filename); - - for (const file of imageFiles.slice(0, remainingSlots)) { - // Validate image type - if (!isValidImageMimeType(file.type)) { - setError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`); - continue; - } - - try { - const dataUrl = await blobToBase64(file); - const thumbnail = await createThumbnail(dataUrl); - - // Use original filename or generate one - const baseFilename = file.name || `dropped-image-${Date.now()}.${file.type.split('/')[1] || 'png'}`; - const resolvedFilename = resolveFilename(baseFilename, [ - ...existingFilenames, - ...newImages.map(img => img.filename) - ]); - - newImages.push({ - id: generateImageId(), - filename: resolvedFilename, - mimeType: file.type, - size: file.size, - data: dataUrl.split(',')[1], // Store base64 without data URL prefix - thumbnail - }); - } catch { - setError('Failed to process dropped image'); - } - } - - if (newImages.length > 0) { - setImages(prev => [...prev, ...newImages]); - // Show success feedback - setPasteSuccess(true); - setTimeout(() => setPasteSuccess(false), 2000); - } - }, - [images, isCreating, description] - ); - - /** - * Parse @mentions from description and create ReferencedFile entries - * Merges with existing referencedFiles, avoiding duplicates + * Parse @mentions from description */ const parseFileMentions = useCallback((text: string, existingFiles: ReferencedFile[]): ReferencedFile[] => { - // Match @filename patterns (supports filenames with dots, hyphens, underscores, and path separators) const mentionRegex = /@([\w\-./\\]+\.\w+)/g; const matches = Array.from(text.matchAll(mentionRegex)); - if (matches.length === 0) return existingFiles; - // Create a set of existing file names for quick lookup const existingNames = new Set(existingFiles.map(f => f.name)); - - // Parse mentioned files that aren't already in the list const newFiles: ReferencedFile[] = []; + matches.forEach(match => { const fileName = match[1]; if (!existingNames.has(fileName)) { newFiles.push({ id: crypto.randomUUID(), - path: fileName, // Store relative path from @mention + path: fileName, name: fileName, isDirectory: false, addedAt: new Date() }); - existingNames.add(fileName); // Prevent duplicates within mentions + existingNames.add(fileName); } }); @@ -604,7 +323,7 @@ export function TaskCreationWizard({ const handleCreate = async () => { if (!description.trim()) { - setError('Please provide a description'); + setError(t('tasks:form.errors.descriptionRequired')); return; } @@ -612,48 +331,37 @@ export function TaskCreationWizard({ setError(null); try { - // Parse @mentions from description and merge with referenced files const allReferencedFiles = parseFileMentions(description, referencedFiles); - // Build metadata from selected values - const metadata: TaskMetadata = { - sourceType: 'manual' - }; - + const metadata: TaskMetadata = { sourceType: 'manual' }; if (category) metadata.category = category; if (priority) metadata.priority = priority; if (complexity) metadata.complexity = complexity; if (impact) metadata.impact = impact; if (model) metadata.model = model; if (thinkingLevel) metadata.thinkingLevel = thinkingLevel; - // All profiles now support per-phase configuration - // isAutoProfile indicates task uses phase-specific models/thinking if (phaseModels && phaseThinking) { - metadata.isAutoProfile = true; + metadata.isAutoProfile = profileId === 'auto'; metadata.phaseModels = phaseModels; metadata.phaseThinking = phaseThinking; } if (images.length > 0) metadata.attachedImages = images; if (allReferencedFiles.length > 0) metadata.referencedFiles = allReferencedFiles; if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true; - // Only include baseBranch if it's not the project default placeholder if (baseBranch && baseBranch !== PROJECT_DEFAULT_BRANCH) metadata.baseBranch = baseBranch; // Pass worktree preference - false means use --direct mode if (!useWorktree) metadata.useWorktree = false; - // Title is optional - if empty, it will be auto-generated by the backend const task = await createTask(projectId, title.trim(), description.trim(), metadata); if (task) { - // Clear draft on successful creation clearDraft(projectId); - // Reset form and close resetForm(); onOpenChange(false); } else { - setError('Failed to create task. Please try again.'); + setError(t('tasks:wizard.errors.createFailed')); } } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); + setError(err instanceof Error ? err.message : t('common:errors.unknownError')); } finally { setIsCreating(false); } @@ -666,7 +374,6 @@ export function TaskCreationWizard({ setPriority(''); setComplexity(''); setImpact(''); - // Reset to selected profile defaults and custom settings setProfileId(settings.selectedAgentProfile || 'auto'); setModel(selectedProfile.model); setThinkingLevel(selectedProfile.thinkingLevel); @@ -678,26 +385,19 @@ export function TaskCreationWizard({ setBaseBranch(PROJECT_DEFAULT_BRANCH); setUseWorktree(true); setError(null); - setShowAdvanced(false); + setShowClassification(false); setShowFileExplorer(false); setShowGitOptions(false); setIsDraftRestored(false); - setPasteSuccess(false); }; - /** - * Handle dialog close - save draft if content exists - */ const handleClose = () => { if (isCreating) return; const draft = getCurrentDraft(); - - // Save draft if there's any content if (!isDraftEmpty(draft)) { saveDraft(draft); } else { - // Clear any existing draft if form is empty clearDraft(projectId); } @@ -705,38 +405,67 @@ export function TaskCreationWizard({ onOpenChange(false); }; - /** - * Discard draft and start fresh - */ const handleDiscardDraft = () => { clearDraft(projectId); resetForm(); setError(null); }; + // Render @ mention highlight overlay for the description textarea + const descriptionOverlay = ( +
+ {description.split(/(@[\w\-./\\]+\.\w+)/g).map((part, i) => { + if (part.match(/^@[\w\-./\\]+\.\w+$/)) { + return ( + + {part} + + ); + } + return {part}; + })} +
+ ); + return ( - - -
- {/* Form content */} -
- -
- Create New Task + setShowFileExplorer(false)} + projectPath={projectPath} + /> + ) + } + sidebarOpen={showFileExplorer} + footer={ +
+
+ {/* Draft restored indicator */} {isDraftRestored && (
- Draft restored + {t('tasks:wizard.draftRestored')}
)} -
- - Describe what you want to build. The AI will analyze your request and - create a detailed specification. - - - -
- {/* Description (Primary - Required) */} -
- - {/* Wrap textarea for file @mentions */} -
- {/* Syntax highlight overlay for @mentions */} -
- {description.split(/(@[\w\-./\\]+\.\w+)/g).map((part, i) => { - // Check if this part is an @mention - if (part.match(/^@[\w\-./\\]+\.\w+$/)) { - return ( - - {part} - - ); - } - return {part}; - })} -
-