From d93eefe8060b634f7255e22865fa4ba68fd9dcd4 Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Thu, 18 Dec 2025 17:45:23 +0100 Subject: [PATCH] feat: enhance TaskCreationWizard with drag-and-drop support for file references and inline @mentions - Added a drop zone for file references and a separate drop zone for inline @mentions in the description textarea. - Updated drag-and-drop handling to allow inserting @mentions directly into the description or adding files to the referenced files list. - Implemented parsing of @mentions from the description to create ReferencedFile entries, avoiding duplicates. - Improved visual feedback for drag-and-drop interactions, including indicators for maximum file capacity and drop zones. --- .../components/TaskCreationWizard.tsx | 257 ++++++++++++------ 1 file changed, 178 insertions(+), 79 deletions(-) diff --git a/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx b/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx index decbbd9f..5565601f 100644 --- a/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx +++ b/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx @@ -135,12 +135,18 @@ export function TaskCreationWizard({ }) ); - // Setup drop zone for file references + // Setup drop zone for file references (entire form) const { setNodeRef: setDropRef, isOver: isOverDropZone } = useDroppable({ id: 'file-drop-zone', data: { type: 'file-drop-zone' } }); + // Setup drop zone for description textarea (inline @mentions) + const { setNodeRef: setTextareaDropRef, isOver: isOverTextarea } = useDroppable({ + id: 'description-drop-zone', + data: { type: 'description-drop-zone' } + }); + // Determine if drop zone is at capacity const isAtMaxFiles = referencedFiles.length >= MAX_REFERENCED_FILES; @@ -397,7 +403,7 @@ export function TaskCreationWizard({ }, []); /** - * Handle drag end - add file to referencedFiles when dropped on valid target + * Handle drag end - insert @mention in description or add to referencedFiles */ const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event; @@ -408,9 +414,6 @@ export function TaskCreationWizard({ // If not dropped on a valid target, do nothing if (!over) return; - // Only accept drops on the file-drop-zone - if (over.id !== 'file-drop-zone') return; - const data = active.data.current as { type?: string; path?: string; @@ -421,30 +424,89 @@ export function TaskCreationWizard({ // Only process file drops if (data?.type !== 'file' || !data.path || !data.name) return; - // Check if we're at the max limit - if (referencedFiles.length >= MAX_REFERENCED_FILES) { - setError(`Maximum of ${MAX_REFERENCED_FILES} referenced files allowed`); + // Handle drop on description textarea - insert inline @mention + if (over.id === 'description-drop-zone') { + const textarea = descriptionRef.current; + if (!textarea) return; + + 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; } - // Check for duplicates - if (referencedFiles.some(f => f.path === data.path)) { - // Silently skip duplicates - return; + // Handle drop on file-drop-zone - add to referenced files list + if (over.id === 'file-drop-zone') { + // Check if we're at the max limit + if (referencedFiles.length >= MAX_REFERENCED_FILES) { + setError(`Maximum of ${MAX_REFERENCED_FILES} referenced files allowed`); + return; + } + + // Check for duplicates + if (referencedFiles.some(f => f.path === data.path)) { + // Silently skip duplicates + return; + } + + // Add the file to referenced files + const newFile: ReferencedFile = { + id: crypto.randomUUID(), + path: data.path, + name: data.name, + isDirectory: data.isDirectory ?? false, + addedAt: new Date() + }; + + setReferencedFiles(prev => [...prev, newFile]); } + }, [referencedFiles, description]); - // Add the file to referenced files - const newFile: ReferencedFile = { - id: crypto.randomUUID(), - path: data.path, - name: data.name, - isDirectory: data.isDirectory ?? false, - addedAt: new Date() - }; + /** + * Parse @mentions from description and create ReferencedFile entries + * Merges with existing referencedFiles, avoiding duplicates + */ + 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)); - setReferencedFiles(prev => [...prev, newFile]); - // Note: Referenced Files section is always visible, no need to expand - }, [referencedFiles]); + 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 + name: fileName, + isDirectory: false, + addedAt: new Date() + }); + existingNames.add(fileName); // Prevent duplicates within mentions + } + }); + + return [...existingFiles, ...newFiles]; + }, []); const handleCreate = async () => { if (!description.trim()) { @@ -456,6 +518,9 @@ 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' @@ -468,7 +533,7 @@ export function TaskCreationWizard({ if (model) metadata.model = model; if (thinkingLevel) metadata.thinkingLevel = thinkingLevel; if (images.length > 0) metadata.attachedImages = images; - if (referencedFiles.length > 0) metadata.referencedFiles = referencedFiles; + if (allReferencedFiles.length > 0) metadata.referencedFiles = allReferencedFiles; if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true; // Title is optional - if empty, it will be auto-generated by the backend @@ -558,11 +623,35 @@ export function TaskCreationWizard({
+ {/* Drop zone indicator overlay - shows when dragging over form */} + {activeDragData && isOverDropZone && ( +
+
+ + + {isAtMaxFiles + ? `Maximum ${MAX_REFERENCED_FILES} files reached` + : 'Drop file to add reference'} + +
+
+ )}
Create New Task @@ -595,25 +684,67 @@ export function TaskCreationWizard({ -