import { useState, useEffect, useCallback } from 'react'; import { FolderOpen, Folder, ChevronRight, HardDrive, Clock, X } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { PathInput } from '@/components/ui/path-input'; import { Kbd, KbdGroup } from '@/components/ui/kbd'; import { getJSON, setJSON } from '@/lib/storage'; import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config'; import { useOSDetection } from '@/hooks'; import { apiPost } from '@/lib/api-fetch'; 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 { isMac } = useOSDetection(); const [currentPath, setCurrentPath] = 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([]); // 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 { const result = await apiPost('/api/fs/browse', { dirPath }); if (result.success) { setCurrentPath(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 state when dialog closes useEffect(() => { if (!open) { setCurrentPath(''); 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) { 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) { browseDirectory(initialPath); } else { browseDirectory(); } } }; loadInitialPath(); } }, [open, initialPath, currentPath, browseDirectory]); const handleSelectDirectory = (dir: DirectoryEntry) => { browseDirectory(dir.path); }; const handleGoHome = useCallback(() => { browseDirectory(); }, [browseDirectory]); const handleNavigate = useCallback( (path: string) => { browseDirectory(path); }, [browseDirectory] ); const handleSelectDrive = (drivePath: string) => { browseDirectory(drivePath); }; 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}
{/* Path navigation */} ({ ...dir, isDirectory: true }))} onSelectEntry={(entry) => { if (entry.isDirectory) { handleSelectDirectory(entry); } }} /> {/* Recent folders */} {recentFolders.length > 0 && (
Recent:
{recentFolders.map((folder) => ( ))}
)} {/* Drives selector (Windows only) */} {drives.length > 0 && (
Drives:
{drives.map((drive) => ( ))}
)} {/* 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 → to jump to a path.
); }