import { useState, useEffect, useRef, useCallback } from 'react'; import { FolderOpen, Folder, ChevronRight, Home, ArrowLeft, HardDrive, CornerDownLeft, Clock, X, } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { getJSON, setJSON } from '@/lib/storage'; import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config'; interface DirectoryEntry { name: string; path: string; } interface BrowseResult { success: boolean; currentPath: string; parentPath: string | null; directories: DirectoryEntry[]; drives?: string[]; error?: string; warning?: string; } interface FileBrowserDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSelect: (path: string) => void; title?: string; description?: string; initialPath?: string; } const RECENT_FOLDERS_KEY = 'file-browser-recent-folders'; const MAX_RECENT_FOLDERS = 5; function getRecentFolders(): string[] { return getJSON(RECENT_FOLDERS_KEY) ?? []; } function addRecentFolder(path: string): void { const recent = getRecentFolders(); // Remove if already exists, then add to front const filtered = recent.filter((p) => p !== path); const updated = [path, ...filtered].slice(0, MAX_RECENT_FOLDERS); setJSON(RECENT_FOLDERS_KEY, updated); } function removeRecentFolder(path: string): string[] { const recent = getRecentFolders(); const updated = recent.filter((p) => p !== path); setJSON(RECENT_FOLDERS_KEY, updated); return updated; } export function FileBrowserDialog({ open, onOpenChange, onSelect, title = 'Select Project Directory', description = 'Navigate to your project folder or paste a path directly', initialPath, }: FileBrowserDialogProps) { const [currentPath, setCurrentPath] = useState(''); const [pathInput, setPathInput] = useState(''); const [parentPath, setParentPath] = useState(null); const [directories, setDirectories] = useState([]); const [drives, setDrives] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [warning, setWarning] = useState(''); const [recentFolders, setRecentFolders] = useState([]); const pathInputRef = useRef(null); // Load recent folders when dialog opens useEffect(() => { if (open) { setRecentFolders(getRecentFolders()); } }, [open]); const handleRemoveRecent = useCallback((e: React.MouseEvent, path: string) => { e.stopPropagation(); const updated = removeRecentFolder(path); setRecentFolders(updated); }, []); const browseDirectory = useCallback(async (dirPath?: string) => { setLoading(true); setError(''); setWarning(''); try { // Get server URL from environment or default const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:3008'; const response = await fetch(`${serverUrl}/api/fs/browse`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dirPath }), }); const result: BrowseResult = await response.json(); if (result.success) { setCurrentPath(result.currentPath); setPathInput(result.currentPath); setParentPath(result.parentPath); setDirectories(result.directories); setDrives(result.drives || []); setWarning(result.warning || ''); } else { setError(result.error || 'Failed to browse directory'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load directories'); } finally { setLoading(false); } }, []); const handleSelectRecent = useCallback( (path: string) => { browseDirectory(path); }, [browseDirectory] ); // Reset current path when dialog closes useEffect(() => { if (!open) { setCurrentPath(''); setPathInput(''); setParentPath(null); setDirectories([]); setError(''); setWarning(''); } }, [open]); // Load initial path or workspace directory when dialog opens useEffect(() => { if (open && !currentPath) { // Priority order: // 1. Last selected directory from this file browser (from localStorage) // 2. initialPath prop (from parent component) // 3. Default workspace directory // 4. Home directory const loadInitialPath = async () => { try { // First, check for last selected directory from getDefaultWorkspaceDirectory // which already implements the priority: last used > Documents/Automaker > DATA_DIR const defaultDir = await getDefaultWorkspaceDirectory(); // If we have a default directory, use it (unless initialPath is explicitly provided and different) const pathToUse = initialPath || defaultDir; if (pathToUse) { // Pre-fill the path input immediately setPathInput(pathToUse); // Then browse to that directory browseDirectory(pathToUse); } else { // No default directory, browse home directory browseDirectory(); } } catch { // If config fetch fails, try initialPath or fall back to home directory if (initialPath) { setPathInput(initialPath); browseDirectory(initialPath); } else { browseDirectory(); } } }; loadInitialPath(); } }, [open, initialPath, currentPath, browseDirectory]); const handleSelectDirectory = (dir: DirectoryEntry) => { browseDirectory(dir.path); }; const handleGoToParent = () => { if (parentPath) { browseDirectory(parentPath); } }; const handleGoHome = () => { browseDirectory(); }; const handleSelectDrive = (drivePath: string) => { browseDirectory(drivePath); }; const handleGoToPath = () => { const trimmedPath = pathInput.trim(); if (trimmedPath) { browseDirectory(trimmedPath); } }; const handlePathInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); handleGoToPath(); } }; const handleSelect = useCallback(() => { if (currentPath) { addRecentFolder(currentPath); // Save to last project directory so it's used as default next time saveLastProjectDirectory(currentPath); onSelect(currentPath); onOpenChange(false); } }, [currentPath, onSelect, onOpenChange]); // Handle Command/Ctrl+Enter keyboard shortcut to select current folder useEffect(() => { if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { // Check for Command+Enter (Mac) or Ctrl+Enter (Windows/Linux) if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (currentPath && !loading) { handleSelect(); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [open, currentPath, loading, handleSelect]); // Helper to get folder name from path const getFolderName = (path: string) => { const parts = path.split(/[/\\]/).filter(Boolean); return parts[parts.length - 1] || path; }; return ( {title} {description}
{/* Direct path input */}
setPathInput(e.target.value)} onKeyDown={handlePathInputKeyDown} className="flex-1 font-mono text-xs h-8" data-testid="path-input" disabled={loading} />
{/* Recent folders */} {recentFolders.length > 0 && (
Recent:
{recentFolders.map((folder) => ( ))}
)} {/* Drives selector (Windows only) */} {drives.length > 0 && (
Drives:
{drives.map((drive) => ( ))}
)} {/* Current path breadcrumb */}
{parentPath && ( )}
{currentPath || 'Loading...'}
{/* Directory list */}
{loading && (
Loading directories...
)} {error && (
{error}
)} {warning && (
{warning}
)} {!loading && !error && !warning && directories.length === 0 && (
No subdirectories found
)} {!loading && !error && directories.length > 0 && (
{directories.map((dir) => ( ))}
)}
Paste a full path above, or click on folders to navigate. Press Enter or click Go to jump to a path.
); }