From 71f213d8d174b04fa9c37269f4ed2853ef1726e7 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Tue, 12 May 2026 16:11:20 -0400 Subject: [PATCH] fix: FileTree duplication and corruption on expand - Replace recursive buildTree with buildVisibleNodes approach - Build flat list of visible nodes based on expanded state - Use processNode helper to traverse tree structure correctly - Each node appears exactly once in the rendered output - Eliminates duplication when expanding directories --- .../frontend/src/components/FileTree.tsx | 73 +++++++++++-------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/gadget-code/frontend/src/components/FileTree.tsx b/gadget-code/frontend/src/components/FileTree.tsx index 5c2905a..d1ead08 100644 --- a/gadget-code/frontend/src/components/FileTree.tsx +++ b/gadget-code/frontend/src/components/FileTree.tsx @@ -134,40 +134,32 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) } }, [onFileSelect]); - // Build tree structure from flat entries - const buildTree = useCallback((entries: FileTreeEntry[], depth: number = 0) => { - const nodes: JSX.Element[] = []; + // Build visible nodes list based on expanded state + const buildVisibleNodes = useCallback(() => { + const visibleNodes: Array<{ entry: FileTreeEntry; depth: number }> = []; - for (const entry of entries) { - 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'; - - // Render the node - nodes.push( - - ); - - // If directory is expanded, render children immediately after - if (hasChildren && isExpanded && state.directoryCache.has(entry.path)) { - const children = buildTree(state.directoryCache.get(entry.path)!, depth + 1); - nodes.push(...children); + 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 nodes; - }, [state.expandedPaths, state.loadingPaths, state.errors, state.directoryCache, toggleExpand, handleFileSelect]); + return visibleNodes; + }, [state.expandedPaths, state.directoryCache]); const rootEntries = state.directoryCache.get('') || []; const rootError = state.errors.get(''); @@ -200,7 +192,26 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) )}
- {buildTree(rootEntries)} + {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 ( + + ); + })}
);