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 { 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<string | null>(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<TaskCategory | ''>('');
|
||||
@@ -81,6 +104,9 @@ export function TaskCreationWizard({
|
||||
// Image attachments
|
||||
const [images, setImages] = useState<ImageAttachment[]>([]);
|
||||
|
||||
// Referenced files from file explorer
|
||||
const [referencedFiles, setReferencedFiles] = useState<ReferencedFile[]>([]);
|
||||
|
||||
// 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<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
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[550px] max-h-[90vh] overflow-y-auto">
|
||||
<DndContext
|
||||
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>
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-foreground">Create New Task</DialogTitle>
|
||||
@@ -600,6 +744,105 @@ export function TaskCreationWizard({
|
||||
</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 */}
|
||||
<div className="flex items-start gap-3 p-4 rounded-lg border border-border bg-muted/30">
|
||||
<Checkbox
|
||||
@@ -632,21 +875,65 @@ export function TaskCreationWizard({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<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'
|
||||
<div className="flex items-center gap-2">
|
||||
{/* File Explorer Toggle Button */}
|
||||
{projectPath && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={showFileExplorer ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setShowFileExplorer(!showFileExplorer)}
|
||||
disabled={isCreating}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user