/** * Folder Browser Component * * Server-side filesystem browser for selecting project directories. * Cross-platform support for Windows, macOS, and Linux. */ import { useState, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { Folder, FolderOpen, ChevronRight, HardDrive, Loader2, AlertCircle, FolderPlus, ArrowLeft, } from 'lucide-react' import * as api from '../lib/api' import { isSubmitEnter } from '../lib/keyboard' import type { DirectoryEntry, DriveInfo } from '../lib/types' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent } from '@/components/ui/card' interface FolderBrowserProps { onSelect: (path: string) => void onCancel: () => void initialPath?: string } export function FolderBrowser({ onSelect, onCancel, initialPath }: FolderBrowserProps) { const [currentPath, setCurrentPath] = useState(initialPath) const [selectedPath, setSelectedPath] = useState(null) const [isCreatingFolder, setIsCreatingFolder] = useState(false) const [newFolderName, setNewFolderName] = useState('') const [createError, setCreateError] = useState(null) // Fetch directory listing const { data: directoryData, isLoading, error, refetch, } = useQuery({ queryKey: ['filesystem', 'list', currentPath], queryFn: () => api.listDirectory(currentPath), }) // Update selected path when directory changes useEffect(() => { if (directoryData?.current_path) { setSelectedPath(directoryData.current_path) } }, [directoryData?.current_path]) const handleNavigate = (path: string) => { setCurrentPath(path) setSelectedPath(path) setIsCreatingFolder(false) setNewFolderName('') setCreateError(null) } const handleNavigateUp = () => { if (directoryData?.parent_path) { handleNavigate(directoryData.parent_path) } } const handleDriveSelect = (drive: DriveInfo) => { handleNavigate(`${drive.letter}:/`) } const handleEntryClick = (entry: DirectoryEntry) => { if (entry.is_directory) { handleNavigate(entry.path) } } const handleCreateFolder = async () => { if (!newFolderName.trim()) { setCreateError('Folder name is required') return } // Basic validation if (!/^[a-zA-Z0-9_\-. ]+$/.test(newFolderName)) { setCreateError('Invalid folder name') return } const newPath = `${directoryData?.current_path}/${newFolderName.trim()}` try { await api.createDirectory(newPath) // Refresh the directory listing await refetch() // Navigate to the new folder handleNavigate(newPath) } catch (err) { setCreateError(err instanceof Error ? err.message : 'Failed to create folder') } } const handleSelect = () => { if (selectedPath) { onSelect(selectedPath) } } // Parse breadcrumb segments from path const getBreadcrumbs = (path: string): { name: string; path: string }[] => { if (!path) return [] const segments: { name: string; path: string }[] = [] // Handle Windows drive letters if (/^[A-Za-z]:/.test(path)) { const drive = path.slice(0, 2) segments.push({ name: drive, path: `${drive}/` }) path = path.slice(3) } else if (path.startsWith('/')) { segments.push({ name: '/', path: '/' }) path = path.slice(1) } // Split remaining path const parts = path.split('/').filter(Boolean) let currentPath = segments.length > 0 ? segments[0].path : '' for (const part of parts) { currentPath = currentPath.endsWith('/') ? currentPath + part : currentPath + '/' + part segments.push({ name: part, path: currentPath }) } return segments } const breadcrumbs = directoryData?.current_path ? getBreadcrumbs(directoryData.current_path) : [] return (
{/* Header with breadcrumb navigation */}
Select Project Folder
{/* Breadcrumb navigation */}
{directoryData?.parent_path && ( )} {breadcrumbs.map((crumb, index) => (
{index > 0 && }
))}
{/* Drive selector (Windows only) */} {directoryData?.drives && directoryData.drives.length > 0 && (
Drives: {directoryData.drives.map((drive) => ( ))}
)} {/* Directory listing */}
{isLoading ? (
) : error ? (

{error instanceof Error ? error.message : 'Failed to load directory'}

) : (
{/* Directory entries - only show directories */} {directoryData?.entries .filter((entry) => entry.is_directory) .map((entry) => ( ))} {/* Empty state */} {directoryData?.entries.filter((e) => e.is_directory).length === 0 && (

No subfolders

You can create a new folder or select this directory.

)}
)} {/* New folder creation */} {isCreatingFolder && (
setNewFolderName(e.target.value)} placeholder="New folder name" className="flex-1" autoFocus onKeyDown={(e) => { if (isSubmitEnter(e, false)) handleCreateFolder() if (e.key === 'Escape') { setIsCreatingFolder(false) setNewFolderName('') setCreateError(null) } }} />
{createError && (

{createError}

)}
)}
{/* Footer with selected path and actions */}
{/* Selected path display */}
Selected path:
{selectedPath || 'No folder selected'}
{selectedPath && (
This folder will contain all project files
)}
{/* Actions */}
) }