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"; 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[] { if (typeof window === "undefined") return []; try { const stored = localStorage.getItem(RECENT_FOLDERS_KEY); return stored ? JSON.parse(stored) : []; } catch { return []; } } function addRecentFolder(path: string): void { if (typeof window === "undefined") return; try { 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); localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(updated)); } catch { // Ignore localStorage errors } } function removeRecentFolder(path: string): string[] { if (typeof window === "undefined") return []; try { const recent = getRecentFolders(); const updated = recent.filter((p) => p !== path); localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(updated)); return updated; } catch { return []; } } 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 handleSelectRecent = useCallback((path: string) => { browseDirectory(path); }, []); const browseDirectory = 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); } }; // Reset current path when dialog closes useEffect(() => { if (!open) { setCurrentPath(""); setPathInput(""); setParentPath(null); setDirectories([]); setError(""); setWarning(""); } }, [open]); // Load initial path or home directory when dialog opens useEffect(() => { if (open && !currentPath) { browseDirectory(initialPath); } }, [open, initialPath]); 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 = () => { if (currentPath) { addRecentFolder(currentPath); onSelect(currentPath); onOpenChange(false); } }; // 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.
); }