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
This commit is contained in:
Rob Colbert 2026-05-12 16:11:20 -04:00
parent 3062420e99
commit 71f213d8d1

View File

@ -134,40 +134,32 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps)
} }
}, [onFileSelect]); }, [onFileSelect]);
// Build tree structure from flat entries // Build visible nodes list based on expanded state
const buildTree = useCallback((entries: FileTreeEntry[], depth: number = 0) => { const buildVisibleNodes = useCallback(() => {
const nodes: JSX.Element[] = []; const visibleNodes: Array<{ entry: FileTreeEntry; depth: number }> = [];
for (const entry of entries) { const processNode = (entry: FileTreeEntry, depth: number) => {
const isExpanded = state.expandedPaths.has(entry.path); visibleNodes.push({ entry, depth });
const isLoading = state.loadingPaths.has(entry.path);
const error = state.errors.get(entry.path);
const hasChildren = entry.type === 'directory';
// Render the node // If this is an expanded directory with cached children, process them
nodes.push( if (entry.type === 'directory' &&
<FileTreeNode state.expandedPaths.has(entry.path) &&
key={entry.path} state.directoryCache.has(entry.path)) {
entry={entry} const children = state.directoryCache.get(entry.path)!;
depth={depth} for (const child of children) {
isExpanded={isExpanded} processNode(child, depth + 1);
isLoading={isLoading}
hasChildren={hasChildren}
error={error}
onToggle={toggleExpand}
onSelect={handleFileSelect}
/>
);
// 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);
} }
} }
};
return nodes; // Process root entries
}, [state.expandedPaths, state.loadingPaths, state.errors, state.directoryCache, toggleExpand, handleFileSelect]); 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 rootEntries = state.directoryCache.get('') || [];
const rootError = state.errors.get(''); const rootError = state.errors.get('');
@ -200,7 +192,26 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps)
)} )}
<div className="space-y-0.5"> <div className="space-y-0.5">
{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 (
<FileTreeNode
key={entry.path}
entry={entry}
depth={depth}
isExpanded={isExpanded}
isLoading={isLoading}
hasChildren={hasChildren}
error={error}
onToggle={toggleExpand}
onSelect={handleFileSelect}
/>
);
})}
</div> </div>
</div> </div>
); );