"use client"; import { Fragment, useCallback, useEffect, useRef, useState } from "react"; type BrowseEntry = { name: string; path: string; type: "folder" | "file" | "document" | "database"; }; type DirectoryPickerModalProps = { open: boolean; onClose: () => void; onSelect: (path: string) => void; /** Starting directory (absolute). Falls back to the workspace root / home. */ startDir?: string; }; function buildBreadcrumbs(dir: string): { label: string; path: string }[] { const segments: { label: string; path: string }[] = []; const homeMatch = dir.match(/^(\/Users\/[^/]+|\/home\/[^/]+)/); const homeDir = homeMatch?.[1]; if (homeDir) { segments.push({ label: "~", path: homeDir }); const rest = dir.slice(homeDir.length); const parts = rest.split("/").filter(Boolean); let currentPath = homeDir; for (const part of parts) { currentPath += "/" + part; segments.push({ label: part, path: currentPath }); } } else if (dir === "/") { segments.push({ label: "/", path: "/" }); } else { segments.push({ label: "/", path: "/" }); const parts = dir.split("/").filter(Boolean); let currentPath = ""; for (const part of parts) { currentPath += "/" + part; segments.push({ label: part, path: currentPath }); } } return segments; } const folderColors = { bg: "rgba(245, 158, 11, 0.12)", fg: "#f59e0b" }; function FolderIcon({ size = 16 }: { size?: number }) { return ( ); } export function DirectoryPickerModal({ open, onClose, onSelect, startDir, }: DirectoryPickerModalProps) { const [currentDir, setCurrentDir] = useState(startDir ?? null); const [displayDir, setDisplayDir] = useState(""); const [entries, setEntries] = useState([]); const [parentDir, setParentDir] = useState(null); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(""); const [creatingFolder, setCreatingFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(""); const [error, setError] = useState(null); const [visible, setVisible] = useState(false); useEffect(() => { if (open) { requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true))); } else { setVisible(false); } }, [open]); useEffect(() => { if (!open) { setSearch(""); setCreatingFolder(false); setNewFolderName(""); setError(null); } }, [open]); // Reset to startDir when reopening useEffect(() => { if (open) { setCurrentDir(startDir ?? null); } }, [open, startDir]); const searchRef = useRef(null); const newFolderRef = useRef(null); const fetchDir = useCallback(async (dir: string | null) => { setLoading(true); setError(null); try { const url = dir ? `/api/workspace/browse?dir=${encodeURIComponent(dir)}` : "/api/workspace/browse"; const res = await fetch(url); if (!res.ok) {throw new Error("Failed to list directory");} const data = await res.json(); setEntries(data.entries || []); setDisplayDir(data.currentDir || ""); setParentDir(data.parentDir ?? null); } catch { setError("Could not load this directory"); setEntries([]); } finally { setLoading(false); } }, []); useEffect(() => { if (open) { void fetchDir(currentDir); } }, [open, currentDir, fetchDir]); useEffect(() => { if (!open) {return;} const handler = (e: KeyboardEvent) => { if (e.key === "Escape") {onClose();} }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [open, onClose]); const navigateInto = useCallback((path: string) => { setCurrentDir(path); setSearch(""); setCreatingFolder(false); }, []); const handleCreateFolder = useCallback(async () => { if (!newFolderName.trim() || !displayDir) {return;} const folderPath = `${displayDir}/${newFolderName.trim()}`; try { const res = await fetch("/api/workspace/mkdir", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: folderPath, absolute: true }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); setError((data as { error?: string }).error || "Failed to create folder"); return; } setCreatingFolder(false); setNewFolderName(""); void fetchDir(currentDir); } catch { setError("Failed to create folder"); } }, [newFolderName, displayDir, currentDir, fetchDir]); const handleSelectCurrent = useCallback(() => { if (displayDir) { onSelect(displayDir); onClose(); } }, [displayDir, onSelect, onClose]); // Only show folders const folders = entries .filter((e) => e.type === "folder") .filter((e) => !search || e.name.toLowerCase().includes(search.toLowerCase())) .toSorted((a, b) => a.name.localeCompare(b.name)); const breadcrumbs = displayDir ? buildBreadcrumbs(displayDir) : []; // Shorten display path for the footer const shortDir = displayDir .replace(/^\/Users\/[^/]+/, "~") .replace(/^\/home\/[^/]+/, "~"); if (!open) {return null;} return (
{/* Header */}

Choose Directory

Navigate to a folder for the workspace

{/* Breadcrumbs */} {displayDir && (
{breadcrumbs.map((seg, i) => ( {i > 0 && ( / )} ))}
)} {/* Search + New Folder */}
setSearch(e.target.value)} placeholder="Filter folders..." className="flex-1 bg-transparent outline-none text-[13px] placeholder:text-[var(--color-text-muted)]" style={{ color: "var(--color-text)" }} />
{/* Folder list */}
{loading ? (
) : error ? (
{error}
) : ( <> {/* Go up */} {parentDir && ( )} {/* New folder input */} {creatingFolder && (
setNewFolderName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") {void handleCreateFolder();} if (e.key === "Escape") { setCreatingFolder(false); setNewFolderName(""); } }} placeholder="Folder name..." className="flex-1 bg-transparent outline-none text-[13px] placeholder:text-[var(--color-text-muted)] rounded px-2 py-1" style={{ color: "var(--color-text)", background: "var(--color-surface)", border: "1px solid var(--color-accent)", }} />
)} {/* Folder entries */} {folders.length === 0 && !parentDir && (
No subfolders here
)} {folders.map((entry) => ( ))} )}
{/* Footer */}

{shortDir || "Loading..."}

); }