From 31e4e8786986a493b760aa1a55d69fb1f4e7749c Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Tue, 16 Dec 2025 20:34:12 +0100 Subject: [PATCH] Add Referenced Files Section and File Explorer Integration in Task Creation Wizard - Introduced a new ReferencedFilesSection component to display and manage referenced files in the task creation process. - Implemented drag-and-drop functionality for adding files from the file explorer to the referenced files section. - Enhanced TaskCreationWizard to include a file explorer drawer for easy file selection and management. - Updated task draft state to include referenced files, ensuring persistence across sessions. - Refactored constants for better organization and maintainability, including the addition of MAX_REFERENCED_FILES limit. --- .../components/ReferencedFilesSection.tsx | 176 ++++++++++ .../components/TaskCreationWizard.tsx | 329 ++++++++++++++++-- .../components/TaskFileExplorerDrawer.tsx | 117 +++++++ auto-claude-ui/src/shared/constants.ts | 3 + auto-claude-ui/src/shared/types/task.ts | 13 + 5 files changed, 617 insertions(+), 21 deletions(-) create mode 100644 auto-claude-ui/src/renderer/components/ReferencedFilesSection.tsx create mode 100644 auto-claude-ui/src/renderer/components/TaskFileExplorerDrawer.tsx diff --git a/auto-claude-ui/src/renderer/components/ReferencedFilesSection.tsx b/auto-claude-ui/src/renderer/components/ReferencedFilesSection.tsx new file mode 100644 index 00000000..4a50dc00 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/ReferencedFilesSection.tsx @@ -0,0 +1,176 @@ +import { X, Folder, File, FileCode, FileJson, FileText, FileImage } from 'lucide-react'; +import { Button } from './ui/button'; +import { cn } from '../lib/utils'; +import type { ReferencedFile } from '../../shared/types'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from './ui/tooltip'; + +interface ReferencedFilesSectionProps { + files: ReferencedFile[]; + onRemove: (id: string) => void; + maxFiles: number; + disabled?: boolean; + className?: string; +} + +/** + * Get appropriate icon based on file extension + * Matches the pattern from FileTreeItem.tsx + */ +function getFileIcon(name: string, isDirectory: boolean): React.ReactNode { + if (isDirectory) { + return ; + } + + const ext = name.split('.').pop()?.toLowerCase(); + + switch (ext) { + case 'ts': + case 'tsx': + case 'js': + case 'jsx': + case 'py': + case 'rb': + case 'go': + case 'rs': + case 'java': + case 'c': + case 'cpp': + case 'h': + case 'cs': + case 'php': + case 'swift': + case 'kt': + return ; + case 'json': + case 'yaml': + case 'yml': + case 'toml': + return ; + case 'md': + case 'txt': + case 'rst': + return ; + case 'png': + case 'jpg': + case 'jpeg': + case 'gif': + case 'svg': + case 'webp': + case 'ico': + return ; + case 'css': + case 'scss': + case 'sass': + case 'less': + return ; + case 'html': + case 'htm': + return ; + default: + return ; + } +} + +/** + * Truncate a path for display, showing the beginning and end + */ +function truncatePath(path: string, maxLength: number = 40): string { + if (path.length <= maxLength) return path; + + const start = Math.floor(maxLength / 3); + const end = maxLength - start - 3; // 3 for "..." + return `${path.slice(0, start)}...${path.slice(-end)}`; +} + +/** + * ReferencedFilesSection displays a list of referenced files with remove functionality + * Styled similarly to the ImageUpload section + */ +export function ReferencedFilesSection({ + files, + onRemove, + maxFiles, + disabled = false, + className +}: ReferencedFilesSectionProps) { + if (files.length === 0) { + return null; + } + + return ( + +
+ {/* Header with count badge */} +
+ + Referenced Files + + {files.length}/{maxFiles} + + +
+ + {/* File list */} +
+ {files.map((file) => ( +
+ {/* File/folder icon */} + {getFileIcon(file.name, file.isDirectory)} + + {/* File name and path */} +
+
+ + {file.name} + + {file.isDirectory && ( + + folder + + )} +
+ + +

+ {truncatePath(file.path)} +

+
+ +

{file.path}

+
+
+
+ + {/* Remove button */} + {!disabled && ( + + )} +
+ ))} +
+
+
+ ); +} diff --git a/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx b/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx index 15711c3e..fcd8e905 100644 --- a/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx +++ b/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx @@ -1,5 +1,15 @@ -import { useState, useEffect, useCallback, useRef, type ClipboardEvent } from 'react'; -import { Loader2, ChevronDown, ChevronUp, Image as ImageIcon, X, RotateCcw } from 'lucide-react'; +import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent } from 'react'; +import { + DndContext, + DragOverlay, + useDroppable, + type DragEndEvent, + type DragStartEvent, + PointerSensor, + useSensor, + useSensors +} from '@dnd-kit/core'; +import { Loader2, ChevronDown, ChevronUp, Image as ImageIcon, X, RotateCcw, File, Folder, FolderTree, FileDown } from 'lucide-react'; import { Dialog, DialogContent, @@ -28,15 +38,19 @@ import { isValidImageMimeType, resolveFilename } from './ImageUpload'; +import { ReferencedFilesSection } from './ReferencedFilesSection'; +import { TaskFileExplorerDrawer } from './TaskFileExplorerDrawer'; import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store'; +import { useProjectStore } from '../stores/project-store'; import { cn } from '../lib/utils'; -import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft, ModelType, ThinkingLevel } from '../../shared/types'; +import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft, ModelType, ThinkingLevel, ReferencedFile } from '../../shared/types'; import { TASK_CATEGORY_LABELS, TASK_PRIORITY_LABELS, TASK_COMPLEXITY_LABELS, TASK_IMPACT_LABELS, MAX_IMAGES_PER_TASK, + MAX_REFERENCED_FILES, ALLOWED_IMAGE_TYPES_DISPLAY, DEFAULT_AGENT_PROFILES, AVAILABLE_MODELS, @@ -67,6 +81,15 @@ export function TaskCreationWizard({ const [error, setError] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); const [showImages, setShowImages] = useState(false); + const [showFiles, setShowFiles] = useState(false); + const [showFileExplorer, setShowFileExplorer] = useState(false); + + // Get project path from project store + const projects = useProjectStore((state) => state.projects); + const projectPath = useMemo(() => { + const project = projects.find((p) => p.id === projectId); + return project?.path ?? null; + }, [projects, projectId]); // Metadata fields const [category, setCategory] = useState(''); @@ -81,6 +104,9 @@ export function TaskCreationWizard({ // Image attachments const [images, setImages] = useState([]); + // Referenced files from file explorer + const [referencedFiles, setReferencedFiles] = useState([]); + // Review setting const [requireReviewBeforeCoding, setRequireReviewBeforeCoding] = useState(false); @@ -88,9 +114,34 @@ export function TaskCreationWizard({ const [isDraftRestored, setIsDraftRestored] = useState(false); const [pasteSuccess, setPasteSuccess] = useState(false); + // Drag-and-drop state for file references + const [activeDragData, setActiveDragData] = useState<{ + path: string; + name: string; + isDirectory: boolean; + } | null>(null); + // Ref for the textarea to handle paste events const descriptionRef = useRef(null); + // Setup drag sensors with distance constraint to prevent accidental drags + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, // 8px movement required before drag starts + }, + }) + ); + + // Setup drop zone for file references + const { setNodeRef: setDropRef, isOver: isOverDropZone } = useDroppable({ + id: 'file-drop-zone', + data: { type: 'file-drop-zone' } + }); + + // Determine if drop zone is at capacity + const isAtMaxFiles = referencedFiles.length >= MAX_REFERENCED_FILES; + // Load draft when dialog opens, or initialize from selected profile useEffect(() => { if (open && projectId) { @@ -106,6 +157,7 @@ export function TaskCreationWizard({ setModel(draft.model || selectedProfile.model); setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel); setImages(draft.images); + setReferencedFiles(draft.referencedFiles ?? []); setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false); setIsDraftRestored(true); @@ -116,6 +168,9 @@ export function TaskCreationWizard({ if (draft.images.length > 0) { setShowImages(true); } + if (draft.referencedFiles && draft.referencedFiles.length > 0) { + setShowFiles(true); + } } else { // No draft - initialize model/thinkingLevel from selected profile setModel(selectedProfile.model); @@ -138,10 +193,10 @@ export function TaskCreationWizard({ model, thinkingLevel, images, + referencedFiles, requireReviewBeforeCoding, savedAt: new Date() - }), [projectId, title, description, category, priority, complexity, impact, model, thinkingLevel, images, requireReviewBeforeCoding]); - + }), [projectId, title, description, category, priority, complexity, impact, model, thinkingLevel, images, referencedFiles, requireReviewBeforeCoding]); /** * Handle paste event for screenshot support */ @@ -222,6 +277,78 @@ export function TaskCreationWizard({ } }, [images]); + /** + * Handle drag start - capture file data for overlay + */ + const handleDragStart = useCallback((event: DragStartEvent) => { + const data = event.active.data.current as { + type: string; + path: string; + name: string; + isDirectory: boolean; + } | undefined; + + if (data?.type === 'file') { + setActiveDragData({ + path: data.path, + name: data.name, + isDirectory: data.isDirectory + }); + } + }, []); + + /** + * Handle drag end - add file to referencedFiles when dropped on valid target + */ + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event; + + // Clear drag state + setActiveDragData(null); + + // 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; + name?: string; + isDirectory?: boolean; + } | undefined; + + // 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`); + 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]); + + // Auto-expand the files section when a file is added + setShowFiles(true); + }, [referencedFiles]); + const handleCreate = async () => { if (!description.trim()) { setError('Please provide a description'); @@ -244,6 +371,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 (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true; // Title is optional - if empty, it will be auto-generated by the backend @@ -275,10 +403,13 @@ export function TaskCreationWizard({ setModel(selectedProfile.model); setThinkingLevel(selectedProfile.thinkingLevel); setImages([]); + setReferencedFiles([]); setRequireReviewBeforeCoding(false); setError(null); setShowAdvanced(false); setShowImages(false); + setShowFiles(false); + setShowFileExplorer(false); setIsDraftRestored(false); setPasteSuccess(false); }; @@ -313,8 +444,21 @@ export function TaskCreationWizard({ }; return ( - - + + + +
+ {/* Form content */} +
Create New Task @@ -600,6 +744,105 @@ export function TaskCreationWizard({
)} + {/* Reference Files Toggle */} + + + {/* Referenced Files Section - Drop Zone */} + {showFiles ? ( +
+ {/* Drop zone overlay indicator */} + {isOverDropZone && ( +
+
+ + + {isAtMaxFiles ? `Max ${MAX_REFERENCED_FILES} files reached` : 'Drop to add reference'} + +
+
+ )} +

+ Reference specific files or folders from your project to provide context for the AI. +

+ setReferencedFiles(prev => prev.filter(f => f.id !== id))} + maxFiles={MAX_REFERENCED_FILES} + disabled={isCreating} + /> + {referencedFiles.length === 0 && ( +

+ No files referenced yet. Drag files from the file explorer to add them. +

+ )} +
+ ) : ( + /* Compact drop zone when section is collapsed - only visible during drag */ + activeDragData && ( +
+ + + {isAtMaxFiles ? `Max ${MAX_REFERENCED_FILES} files reached` : 'Drop file here to add reference'} + +
+ ) + )} + {/* Review Requirement Toggle */}
- - )} - +
+
+ + +
- -
+ + + {/* File Explorer Drawer */} + {projectPath && ( + setShowFileExplorer(false)} + projectPath={projectPath} + /> + )} + +
+
+ + {/* Drag overlay - shows what's being dragged */} + + {activeDragData && ( +
+ {activeDragData.isDirectory ? ( + + ) : ( + + )} + {activeDragData.name} +
+ )} +
+ ); } diff --git a/auto-claude-ui/src/renderer/components/TaskFileExplorerDrawer.tsx b/auto-claude-ui/src/renderer/components/TaskFileExplorerDrawer.tsx new file mode 100644 index 00000000..e2d594fe --- /dev/null +++ b/auto-claude-ui/src/renderer/components/TaskFileExplorerDrawer.tsx @@ -0,0 +1,117 @@ +import { motion, AnimatePresence } from 'motion/react'; +import { X, FolderTree, RefreshCw } from 'lucide-react'; +import { Button } from './ui/button'; +import { ScrollArea } from './ui/scroll-area'; +import { FileTree } from './FileTree'; +import { useFileExplorerStore } from '../stores/file-explorer-store'; + +interface TaskFileExplorerDrawerProps { + isOpen: boolean; + onClose: () => void; + projectPath: string; +} + +// Animation variants for the sidebar panel +const panelVariants = { + hidden: { + width: 0, + opacity: 0 + }, + visible: { + width: 288, // w-72 = 18rem = 288px + opacity: 1 + } +}; + +// Animation for the content inside (slides in slightly delayed) +const contentVariants = { + hidden: { + x: 20, + opacity: 0 + }, + visible: { + x: 0, + opacity: 1 + } +}; + +export function TaskFileExplorerDrawer({ isOpen, onClose, projectPath }: TaskFileExplorerDrawerProps) { + const { clearCache, loadDirectory } = useFileExplorerStore(); + + const handleRefresh = () => { + clearCache(); + loadDirectory(projectPath); + }; + + return ( + + {isOpen && ( + + + {/* Header */} +
+
+ + Project Files +
+
+ + +
+
+ + {/* Drag hint */} +
+

+ Drag files to add as references +

+
+ + {/* File tree */} + + + +
+
+ )} +
+ ); +} diff --git a/auto-claude-ui/src/shared/constants.ts b/auto-claude-ui/src/shared/constants.ts index 1dc371b4..2dd97f3d 100644 --- a/auto-claude-ui/src/shared/constants.ts +++ b/auto-claude-ui/src/shared/constants.ts @@ -817,6 +817,9 @@ export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // Maximum number of images per task export const MAX_IMAGES_PER_TASK = 10; +// Maximum number of referenced files per task +export const MAX_REFERENCED_FILES = 20; + // Allowed image MIME types export const ALLOWED_IMAGE_TYPES = [ 'image/png', diff --git a/auto-claude-ui/src/shared/types/task.ts b/auto-claude-ui/src/shared/types/task.ts index a220ac6e..a5d66792 100644 --- a/auto-claude-ui/src/shared/types/task.ts +++ b/auto-claude-ui/src/shared/types/task.ts @@ -118,6 +118,15 @@ export interface ImageAttachment { thumbnail?: string; // Base64 thumbnail for preview } +// Referenced file types for task creation (files/folders from project) +export interface ReferencedFile { + id: string; // Unique identifier (UUID) + path: string; // Relative path from project root + name: string; // File or folder name + isDirectory: boolean; // True if this is a directory + addedAt: Date; // When the file was added as reference +} + // Draft state for task creation (auto-saved when dialog closes) export interface TaskDraft { projectId: string; @@ -130,6 +139,7 @@ export interface TaskDraft { model: ModelType | ''; thinkingLevel: ThinkingLevel | ''; images: ImageAttachment[]; + referencedFiles: ReferencedFile[]; requireReviewBeforeCoding?: boolean; savedAt: Date; } @@ -192,6 +202,9 @@ export interface TaskMetadata { // Image attachments (screenshots, mockups, diagrams) attachedImages?: ImageAttachment[]; + // Referenced files (files/folders from project for context) + referencedFiles?: ReferencedFile[]; + // Review settings requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts