"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { ArrowsClockwise, MagnifyingGlass, FileCode, Code, File } from "@phosphor-icons/react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import type { FileTreeNode } from "@/types"; import { FileTree as AIFileTree, FileTreeFolder, FileTreeFile, } from "@/components/ai-elements/file-tree"; import { useTranslation } from "@/hooks/useTranslation"; import type { ReactNode } from "react"; interface FileTreeProps { workingDirectory: string; onFileSelect: (path: string) => void; onFileAdd?: (path: string) => void; } function getFileIcon(extension?: string): ReactNode { switch (extension) { case "ts": case "tsx": case "js": case "jsx": case "py": case "rb": case "rs": case "go": case "java": case "c": case "cpp": case "h": case "hpp": case "cs": case "swift": case "kt": case "dart": case "lua": case "php": case "zig": return ; case "json": case "yaml": case "yml": case "toml": return ; case "md": case "mdx": case "txt": case "csv": return ; default: return ; } } function containsMatch(node: FileTreeNode, query: string): boolean { const q = query.toLowerCase(); if (node.name.toLowerCase().includes(q)) return true; if (node.children) { return node.children.some((child) => containsMatch(child, q)); } return false; } function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] { if (!query) return nodes; return nodes .filter((node) => containsMatch(node, query)) .map((node) => ({ ...node, children: node.children ? filterTree(node.children, query) : undefined, })); } function RenderTreeNodes({ nodes, searchQuery }: { nodes: FileTreeNode[]; searchQuery: string }) { const filtered = searchQuery ? filterTree(nodes, searchQuery) : nodes; return ( <> {filtered.map((node) => { if (node.type === "directory") { return ( {node.children && ( )} ); } return ( ); })} > ); } export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTreeProps) { const [tree, setTree] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const abortRef = useRef(null); const { t } = useTranslation(); const fetchTree = useCallback(async () => { // Always cancel in-flight request first — even when clearing directory, // otherwise a stale response from the old project can arrive and repopulate the tree. if (abortRef.current) { abortRef.current.abort(); } if (!workingDirectory) { abortRef.current = null; setTree([]); setError(null); setLoading(false); return; } const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); try { const res = await fetch( `/api/files?dir=${encodeURIComponent(workingDirectory)}&baseDir=${encodeURIComponent(workingDirectory)}&depth=4&_t=${Date.now()}`, { signal: controller.signal } ); if (controller.signal.aborted) return; if (res.ok) { const data = await res.json(); if (controller.signal.aborted) return; setTree(data.tree || []); } else { const errData = await res.json().catch(() => ({ error: res.statusText })); setTree([]); setError(errData.error || `Failed to load (${res.status})`); } } catch (e) { if ((e as Error).name === 'AbortError') return; setTree([]); setError('Failed to load file tree'); } finally { if (!controller.signal.aborted) { setLoading(false); } } }, [workingDirectory]); useEffect(() => { fetchTree(); }, [fetchTree]); // Cleanup abort controller on unmount useEffect(() => { return () => { if (abortRef.current) { abortRef.current.abort(); } }; }, []); // Auto-refresh when AI finishes streaming useEffect(() => { const handler = () => fetchTree(); window.addEventListener('refresh-file-tree', handler); return () => window.removeEventListener('refresh-file-tree', handler); }, [fetchTree]); // Default to all directories collapsed const defaultExpanded = new Set(); return ( {/* Search + Refresh */} setSearchQuery(e.target.value)} className="h-7 pl-7 text-xs" /> {t('fileTree.refresh')} {/* Tree */} {loading && tree.length === 0 ? ( ) : tree.length === 0 ? ( {error ? error : workingDirectory ? t('fileTree.noFiles') : t('fileTree.selectFolder')} ) : ( )} ); }
{error ? error : workingDirectory ? t('fileTree.noFiles') : t('fileTree.selectFolder')}