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.
This commit is contained in:
@@ -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 <Folder className="h-4 w-4 text-warning shrink-0" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <FileCode className="h-4 w-4 text-info shrink-0" />;
|
||||||
|
case 'json':
|
||||||
|
case 'yaml':
|
||||||
|
case 'yml':
|
||||||
|
case 'toml':
|
||||||
|
return <FileJson className="h-4 w-4 text-warning shrink-0" />;
|
||||||
|
case 'md':
|
||||||
|
case 'txt':
|
||||||
|
case 'rst':
|
||||||
|
return <FileText className="h-4 w-4 text-muted-foreground shrink-0" />;
|
||||||
|
case 'png':
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'gif':
|
||||||
|
case 'svg':
|
||||||
|
case 'webp':
|
||||||
|
case 'ico':
|
||||||
|
return <FileImage className="h-4 w-4 text-purple-400 shrink-0" />;
|
||||||
|
case 'css':
|
||||||
|
case 'scss':
|
||||||
|
case 'sass':
|
||||||
|
case 'less':
|
||||||
|
return <FileCode className="h-4 w-4 text-pink-400 shrink-0" />;
|
||||||
|
case 'html':
|
||||||
|
case 'htm':
|
||||||
|
return <FileCode className="h-4 w-4 text-orange-400 shrink-0" />;
|
||||||
|
default:
|
||||||
|
return <File className="h-4 w-4 text-muted-foreground shrink-0" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className={cn('space-y-2', className)}>
|
||||||
|
{/* Header with count badge */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Referenced Files
|
||||||
|
<span className="ml-2 text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">
|
||||||
|
{files.length}/{maxFiles}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File list */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{files.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.id}
|
||||||
|
className={cn(
|
||||||
|
'group flex items-center gap-2 py-1.5 px-2 rounded-md',
|
||||||
|
'bg-muted/50 hover:bg-muted transition-colors',
|
||||||
|
'border border-border'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* File/folder icon */}
|
||||||
|
{getFileIcon(file.name, file.isDirectory)}
|
||||||
|
|
||||||
|
{/* File name and path */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-foreground truncate">
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
{file.isDirectory && (
|
||||||
|
<span className="text-[10px] text-muted-foreground bg-muted px-1 py-0.5 rounded">
|
||||||
|
folder
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<p className="text-xs text-muted-foreground truncate cursor-default">
|
||||||
|
{truncatePath(file.path)}
|
||||||
|
</p>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="start" className="max-w-md">
|
||||||
|
<p className="text-xs break-all">{file.path}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Remove button */}
|
||||||
|
{!disabled && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
'h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity',
|
||||||
|
'hover:bg-destructive/10 hover:text-destructive'
|
||||||
|
)}
|
||||||
|
onClick={() => onRemove(file.id)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
import { useState, useEffect, useCallback, useRef, type ClipboardEvent } from 'react';
|
import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent } from 'react';
|
||||||
import { Loader2, ChevronDown, ChevronUp, Image as ImageIcon, X, RotateCcw } from 'lucide-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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -28,15 +38,19 @@ import {
|
|||||||
isValidImageMimeType,
|
isValidImageMimeType,
|
||||||
resolveFilename
|
resolveFilename
|
||||||
} from './ImageUpload';
|
} from './ImageUpload';
|
||||||
|
import { ReferencedFilesSection } from './ReferencedFilesSection';
|
||||||
|
import { TaskFileExplorerDrawer } from './TaskFileExplorerDrawer';
|
||||||
import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store';
|
import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store';
|
||||||
|
import { useProjectStore } from '../stores/project-store';
|
||||||
import { cn } from '../lib/utils';
|
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 {
|
import {
|
||||||
TASK_CATEGORY_LABELS,
|
TASK_CATEGORY_LABELS,
|
||||||
TASK_PRIORITY_LABELS,
|
TASK_PRIORITY_LABELS,
|
||||||
TASK_COMPLEXITY_LABELS,
|
TASK_COMPLEXITY_LABELS,
|
||||||
TASK_IMPACT_LABELS,
|
TASK_IMPACT_LABELS,
|
||||||
MAX_IMAGES_PER_TASK,
|
MAX_IMAGES_PER_TASK,
|
||||||
|
MAX_REFERENCED_FILES,
|
||||||
ALLOWED_IMAGE_TYPES_DISPLAY,
|
ALLOWED_IMAGE_TYPES_DISPLAY,
|
||||||
DEFAULT_AGENT_PROFILES,
|
DEFAULT_AGENT_PROFILES,
|
||||||
AVAILABLE_MODELS,
|
AVAILABLE_MODELS,
|
||||||
@@ -67,6 +81,15 @@ export function TaskCreationWizard({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
const [showImages, setShowImages] = 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
|
// Metadata fields
|
||||||
const [category, setCategory] = useState<TaskCategory | ''>('');
|
const [category, setCategory] = useState<TaskCategory | ''>('');
|
||||||
@@ -81,6 +104,9 @@ export function TaskCreationWizard({
|
|||||||
// Image attachments
|
// Image attachments
|
||||||
const [images, setImages] = useState<ImageAttachment[]>([]);
|
const [images, setImages] = useState<ImageAttachment[]>([]);
|
||||||
|
|
||||||
|
// Referenced files from file explorer
|
||||||
|
const [referencedFiles, setReferencedFiles] = useState<ReferencedFile[]>([]);
|
||||||
|
|
||||||
// Review setting
|
// Review setting
|
||||||
const [requireReviewBeforeCoding, setRequireReviewBeforeCoding] = useState(false);
|
const [requireReviewBeforeCoding, setRequireReviewBeforeCoding] = useState(false);
|
||||||
|
|
||||||
@@ -88,9 +114,34 @@ export function TaskCreationWizard({
|
|||||||
const [isDraftRestored, setIsDraftRestored] = useState(false);
|
const [isDraftRestored, setIsDraftRestored] = useState(false);
|
||||||
const [pasteSuccess, setPasteSuccess] = 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
|
// Ref for the textarea to handle paste events
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>(null);
|
const descriptionRef = useRef<HTMLTextAreaElement>(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
|
// Load draft when dialog opens, or initialize from selected profile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && projectId) {
|
if (open && projectId) {
|
||||||
@@ -106,6 +157,7 @@ export function TaskCreationWizard({
|
|||||||
setModel(draft.model || selectedProfile.model);
|
setModel(draft.model || selectedProfile.model);
|
||||||
setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel);
|
setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel);
|
||||||
setImages(draft.images);
|
setImages(draft.images);
|
||||||
|
setReferencedFiles(draft.referencedFiles ?? []);
|
||||||
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
|
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
|
||||||
setIsDraftRestored(true);
|
setIsDraftRestored(true);
|
||||||
|
|
||||||
@@ -116,6 +168,9 @@ export function TaskCreationWizard({
|
|||||||
if (draft.images.length > 0) {
|
if (draft.images.length > 0) {
|
||||||
setShowImages(true);
|
setShowImages(true);
|
||||||
}
|
}
|
||||||
|
if (draft.referencedFiles && draft.referencedFiles.length > 0) {
|
||||||
|
setShowFiles(true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No draft - initialize model/thinkingLevel from selected profile
|
// No draft - initialize model/thinkingLevel from selected profile
|
||||||
setModel(selectedProfile.model);
|
setModel(selectedProfile.model);
|
||||||
@@ -138,10 +193,10 @@ export function TaskCreationWizard({
|
|||||||
model,
|
model,
|
||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
images,
|
images,
|
||||||
|
referencedFiles,
|
||||||
requireReviewBeforeCoding,
|
requireReviewBeforeCoding,
|
||||||
savedAt: new Date()
|
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
|
* Handle paste event for screenshot support
|
||||||
*/
|
*/
|
||||||
@@ -222,6 +277,78 @@ export function TaskCreationWizard({
|
|||||||
}
|
}
|
||||||
}, [images]);
|
}, [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 () => {
|
const handleCreate = async () => {
|
||||||
if (!description.trim()) {
|
if (!description.trim()) {
|
||||||
setError('Please provide a description');
|
setError('Please provide a description');
|
||||||
@@ -244,6 +371,7 @@ export function TaskCreationWizard({
|
|||||||
if (model) metadata.model = model;
|
if (model) metadata.model = model;
|
||||||
if (thinkingLevel) metadata.thinkingLevel = thinkingLevel;
|
if (thinkingLevel) metadata.thinkingLevel = thinkingLevel;
|
||||||
if (images.length > 0) metadata.attachedImages = images;
|
if (images.length > 0) metadata.attachedImages = images;
|
||||||
|
if (referencedFiles.length > 0) metadata.referencedFiles = referencedFiles;
|
||||||
if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true;
|
if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true;
|
||||||
|
|
||||||
// Title is optional - if empty, it will be auto-generated by the backend
|
// Title is optional - if empty, it will be auto-generated by the backend
|
||||||
@@ -275,10 +403,13 @@ export function TaskCreationWizard({
|
|||||||
setModel(selectedProfile.model);
|
setModel(selectedProfile.model);
|
||||||
setThinkingLevel(selectedProfile.thinkingLevel);
|
setThinkingLevel(selectedProfile.thinkingLevel);
|
||||||
setImages([]);
|
setImages([]);
|
||||||
|
setReferencedFiles([]);
|
||||||
setRequireReviewBeforeCoding(false);
|
setRequireReviewBeforeCoding(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
setShowAdvanced(false);
|
setShowAdvanced(false);
|
||||||
setShowImages(false);
|
setShowImages(false);
|
||||||
|
setShowFiles(false);
|
||||||
|
setShowFileExplorer(false);
|
||||||
setIsDraftRestored(false);
|
setIsDraftRestored(false);
|
||||||
setPasteSuccess(false);
|
setPasteSuccess(false);
|
||||||
};
|
};
|
||||||
@@ -313,8 +444,21 @@ export function TaskCreationWizard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<DndContext
|
||||||
<DialogContent className="sm:max-w-[550px] max-h-[90vh] overflow-y-auto">
|
sensors={sensors}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"max-h-[90vh] p-0 overflow-hidden transition-all duration-300 ease-out",
|
||||||
|
showFileExplorer ? "sm:max-w-[900px]" : "sm:max-w-[550px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex h-full">
|
||||||
|
{/* Form content */}
|
||||||
|
<div className="flex-1 flex flex-col p-6 min-w-0 overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<DialogTitle className="text-foreground">Create New Task</DialogTitle>
|
<DialogTitle className="text-foreground">Create New Task</DialogTitle>
|
||||||
@@ -600,6 +744,105 @@ export function TaskCreationWizard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Reference Files Toggle */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowFiles(!showFiles)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors',
|
||||||
|
'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
|
||||||
|
)}
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<FolderTree className="h-4 w-4" />
|
||||||
|
Reference Files (optional)
|
||||||
|
{referencedFiles.length > 0 && (
|
||||||
|
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">
|
||||||
|
{referencedFiles.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{showFiles ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Referenced Files Section - Drop Zone */}
|
||||||
|
{showFiles ? (
|
||||||
|
<div
|
||||||
|
ref={setDropRef}
|
||||||
|
className={cn(
|
||||||
|
"space-y-3 p-4 rounded-lg border bg-muted/30 relative transition-all",
|
||||||
|
isOverDropZone && !isAtMaxFiles && "ring-2 ring-info border-info",
|
||||||
|
isOverDropZone && isAtMaxFiles && "ring-2 ring-warning border-warning",
|
||||||
|
!isOverDropZone && "border-border"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Drop zone overlay indicator */}
|
||||||
|
{isOverDropZone && (
|
||||||
|
<div className={cn(
|
||||||
|
"absolute inset-0 z-10 flex items-center justify-center pointer-events-none rounded-lg",
|
||||||
|
isAtMaxFiles ? "bg-warning/10" : "bg-info/10"
|
||||||
|
)}>
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center gap-2 px-3 py-2 rounded-md",
|
||||||
|
isAtMaxFiles ? "bg-warning/90 text-warning-foreground" : "bg-info/90 text-info-foreground"
|
||||||
|
)}>
|
||||||
|
<FileDown className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{isAtMaxFiles ? `Max ${MAX_REFERENCED_FILES} files reached` : 'Drop to add reference'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Reference specific files or folders from your project to provide context for the AI.
|
||||||
|
</p>
|
||||||
|
<ReferencedFilesSection
|
||||||
|
files={referencedFiles}
|
||||||
|
onRemove={(id) => setReferencedFiles(prev => prev.filter(f => f.id !== id))}
|
||||||
|
maxFiles={MAX_REFERENCED_FILES}
|
||||||
|
disabled={isCreating}
|
||||||
|
/>
|
||||||
|
{referencedFiles.length === 0 && (
|
||||||
|
<p className="text-xs text-muted-foreground italic">
|
||||||
|
No files referenced yet. Drag files from the file explorer to add them.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Compact drop zone when section is collapsed - only visible during drag */
|
||||||
|
activeDragData && (
|
||||||
|
<div
|
||||||
|
ref={setDropRef}
|
||||||
|
className={cn(
|
||||||
|
"p-3 rounded-lg border-2 border-dashed flex items-center justify-center gap-2 transition-all animate-in fade-in slide-in-from-top-2 duration-200",
|
||||||
|
isOverDropZone && !isAtMaxFiles && "border-info bg-info/10",
|
||||||
|
isOverDropZone && isAtMaxFiles && "border-warning bg-warning/10",
|
||||||
|
!isOverDropZone && "border-muted-foreground/30 bg-muted/20"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileDown className={cn(
|
||||||
|
"h-4 w-4",
|
||||||
|
isOverDropZone && !isAtMaxFiles && "text-info",
|
||||||
|
isOverDropZone && isAtMaxFiles && "text-warning",
|
||||||
|
!isOverDropZone && "text-muted-foreground"
|
||||||
|
)} />
|
||||||
|
<span className={cn(
|
||||||
|
"text-sm",
|
||||||
|
isOverDropZone && !isAtMaxFiles && "text-info font-medium",
|
||||||
|
isOverDropZone && isAtMaxFiles && "text-warning font-medium",
|
||||||
|
!isOverDropZone && "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{isAtMaxFiles ? `Max ${MAX_REFERENCED_FILES} files reached` : 'Drop file here to add reference'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Review Requirement Toggle */}
|
{/* Review Requirement Toggle */}
|
||||||
<div className="flex items-start gap-3 p-4 rounded-lg border border-border bg-muted/30">
|
<div className="flex items-start gap-3 p-4 rounded-lg border border-border bg-muted/30">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -632,21 +875,65 @@ export function TaskCreationWizard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={handleClose} disabled={isCreating}>
|
<div className="flex items-center gap-2">
|
||||||
Cancel
|
{/* File Explorer Toggle Button */}
|
||||||
</Button>
|
{projectPath && (
|
||||||
<Button onClick={handleCreate} disabled={isCreating || !description.trim()}>
|
<Button
|
||||||
{isCreating ? (
|
type="button"
|
||||||
<>
|
variant={showFileExplorer ? 'default' : 'outline'}
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
size="sm"
|
||||||
Creating...
|
onClick={() => setShowFileExplorer(!showFileExplorer)}
|
||||||
</>
|
disabled={isCreating}
|
||||||
) : (
|
className="gap-1.5"
|
||||||
'Create Task'
|
>
|
||||||
|
<FolderTree className="h-4 w-4" />
|
||||||
|
{showFileExplorer ? 'Hide Files' : 'Browse Files'}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={handleClose} disabled={isCreating}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreate} disabled={isCreating || !description.trim()}>
|
||||||
|
{isCreating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Create Task'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</div>
|
||||||
</Dialog>
|
|
||||||
|
{/* File Explorer Drawer */}
|
||||||
|
{projectPath && (
|
||||||
|
<TaskFileExplorerDrawer
|
||||||
|
isOpen={showFileExplorer}
|
||||||
|
onClose={() => setShowFileExplorer(false)}
|
||||||
|
projectPath={projectPath}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Drag overlay - shows what's being dragged */}
|
||||||
|
<DragOverlay>
|
||||||
|
{activeDragData && (
|
||||||
|
<div className="flex items-center gap-2 bg-card border border-border rounded-md px-3 py-2 shadow-lg">
|
||||||
|
{activeDragData.isDirectory ? (
|
||||||
|
<Folder className="h-4 w-4 text-warning" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm">{activeDragData.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
variants={panelVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
transition={{
|
||||||
|
width: { duration: 0.3, ease: [0.4, 0, 0.2, 1] },
|
||||||
|
opacity: { duration: 0.2 }
|
||||||
|
}}
|
||||||
|
className="h-full bg-card border-l border-border flex flex-col shadow-xl overflow-hidden"
|
||||||
|
style={{ minWidth: 0 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
transition={{
|
||||||
|
duration: 0.25,
|
||||||
|
delay: 0.1,
|
||||||
|
ease: [0.4, 0, 0.2, 1]
|
||||||
|
}}
|
||||||
|
className="flex flex-col h-full w-72"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-3 py-2 border-b border-border bg-card/80 shrink-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FolderTree className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium whitespace-nowrap">Project Files</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Drag hint */}
|
||||||
|
<div className="px-3 py-2 bg-muted/30 border-b border-border shrink-0">
|
||||||
|
<p className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||||
|
Drag files to add as references
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File tree */}
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<FileTree rootPath={projectPath} />
|
||||||
|
</ScrollArea>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -817,6 +817,9 @@ export const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
|||||||
// Maximum number of images per task
|
// Maximum number of images per task
|
||||||
export const MAX_IMAGES_PER_TASK = 10;
|
export const MAX_IMAGES_PER_TASK = 10;
|
||||||
|
|
||||||
|
// Maximum number of referenced files per task
|
||||||
|
export const MAX_REFERENCED_FILES = 20;
|
||||||
|
|
||||||
// Allowed image MIME types
|
// Allowed image MIME types
|
||||||
export const ALLOWED_IMAGE_TYPES = [
|
export const ALLOWED_IMAGE_TYPES = [
|
||||||
'image/png',
|
'image/png',
|
||||||
|
|||||||
@@ -118,6 +118,15 @@ export interface ImageAttachment {
|
|||||||
thumbnail?: string; // Base64 thumbnail for preview
|
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)
|
// Draft state for task creation (auto-saved when dialog closes)
|
||||||
export interface TaskDraft {
|
export interface TaskDraft {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -130,6 +139,7 @@ export interface TaskDraft {
|
|||||||
model: ModelType | '';
|
model: ModelType | '';
|
||||||
thinkingLevel: ThinkingLevel | '';
|
thinkingLevel: ThinkingLevel | '';
|
||||||
images: ImageAttachment[];
|
images: ImageAttachment[];
|
||||||
|
referencedFiles: ReferencedFile[];
|
||||||
requireReviewBeforeCoding?: boolean;
|
requireReviewBeforeCoding?: boolean;
|
||||||
savedAt: Date;
|
savedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -192,6 +202,9 @@ export interface TaskMetadata {
|
|||||||
// Image attachments (screenshots, mockups, diagrams)
|
// Image attachments (screenshots, mockups, diagrams)
|
||||||
attachedImages?: ImageAttachment[];
|
attachedImages?: ImageAttachment[];
|
||||||
|
|
||||||
|
// Referenced files (files/folders from project for context)
|
||||||
|
referencedFiles?: ReferencedFile[];
|
||||||
|
|
||||||
// Review settings
|
// Review settings
|
||||||
requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts
|
requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user