import { useState, useEffect, useCallback } from 'react'; import { socketClient } from '../lib/socket'; import { WorkspaceMode } from '../lib/types'; import FileTreeNode from './FileTreeNode'; export interface FileTreeEntry { name: string; path: string; type: 'file' | 'directory' | 'symlink'; size?: number; modified?: string; isHidden?: boolean; } interface FileTreeProps { workspaceMode: WorkspaceMode; onFileSelect?: (path: string) => void; } interface FileTreeState { directoryCache: Map; expandedPaths: Set; loadingPaths: Set; errors: Map; } export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) { const [state, setState] = useState({ directoryCache: new Map(), expandedPaths: new Set(), loadingPaths: new Set(), errors: new Map(), }); const [rootLoaded, setRootLoaded] = useState(false); // Load root directory on mount useEffect(() => { if (!rootLoaded) { loadDirectory(''); setRootLoaded(true); } }, []); const loadDirectory = useCallback(async (path: string) => { // Don't reload if already cached if (state.directoryCache.has(path)) { return; } // Don't reload if already loading if (state.loadingPaths.has(path)) { return; } setState(prev => { const newLoading = new Set(prev.loadingPaths); newLoading.add(path); const newErrors = new Map(prev.errors); newErrors.delete(path); return { ...prev, loadingPaths: newLoading, errors: newErrors, }; }); try { const result = await socketClient.requestFileTree({ path: path || undefined, recursive: false, maxDepth: 1, }); if (result.success && result.entries) { setState(prev => { const newCache = new Map(prev.directoryCache); newCache.set(path, result.entries!); const newLoading = new Set(prev.loadingPaths); newLoading.delete(path); return { ...prev, directoryCache: newCache, loadingPaths: newLoading, }; }); } else { setState(prev => { const newErrors = new Map(prev.errors); newErrors.set(path, result.error || 'Failed to load directory'); const newLoading = new Set(prev.loadingPaths); newLoading.delete(path); return { ...prev, errors: newErrors, loadingPaths: newLoading, }; }); } } catch (error) { setState(prev => { const newErrors = new Map(prev.errors); newErrors.set(path, error instanceof Error ? error.message : 'Unknown error'); const newLoading = new Set(prev.loadingPaths); newLoading.delete(path); return { ...prev, errors: newErrors, loadingPaths: newLoading, }; }); } }, [state.directoryCache, state.loadingPaths]); const toggleExpand = useCallback((path: string) => { setState(prev => { const newExpanded = new Set(prev.expandedPaths); if (newExpanded.has(path)) { newExpanded.delete(path); } else { newExpanded.add(path); // Load children if not already cached if (!prev.directoryCache.has(path)) { setTimeout(() => loadDirectory(path), 0); } } return { ...prev, expandedPaths: newExpanded }; }); }, [loadDirectory]); const handleFileSelect = useCallback((path: string) => { if (onFileSelect) { onFileSelect(path); } }, [onFileSelect]); // Build visible nodes list based on expanded state const buildVisibleNodes = useCallback(() => { const visibleNodes: Array<{ entry: FileTreeEntry; depth: number }> = []; const processNode = (entry: FileTreeEntry, depth: number) => { visibleNodes.push({ entry, depth }); // If this is an expanded directory with cached children, process them if (entry.type === 'directory' && state.expandedPaths.has(entry.path) && state.directoryCache.has(entry.path)) { const children = state.directoryCache.get(entry.path)!; for (const child of children) { processNode(child, depth + 1); } } }; // Process root entries const rootEntries = state.directoryCache.get('') || []; for (const entry of rootEntries) { processNode(entry, 0); } return visibleNodes; }, [state.expandedPaths, state.directoryCache]); const rootEntries = state.directoryCache.get('') || []; const rootError = state.errors.get(''); const rootLoading = state.loadingPaths.has(''); return (
{rootLoading && (
)} {rootError && (

{rootError}

)} {!rootLoading && !rootError && rootEntries.length === 0 && (
Empty directory
)}
{buildVisibleNodes().map(({ entry, depth }) => { const isExpanded = state.expandedPaths.has(entry.path); const isLoading = state.loadingPaths.has(entry.path); const error = state.errors.get(entry.path); const hasChildren = entry.type === 'directory'; return ( ); })}
); }