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]);
// 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';
const processNode = (entry: FileTreeEntry, depth: number) => {
visibleNodes.push({ entry, depth });
// Render the node
nodes.push(
<FileTreeNode
key={entry.path}
entry={entry}
depth={depth}
isExpanded={isExpanded}
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);
// 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);
}
}
};
return nodes;
}, [state.expandedPaths, state.loadingPaths, state.errors, state.directoryCache, toggleExpand, handleFileSelect]);
// 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('');
@ -200,7 +192,26 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps)
)}
<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>
);