fix: make file tree loading more reliable

Avoid gitignore filtering during folder browsing
Show folder load errors instead of empty folders
Add timeout fallback for gitignore checks
This commit is contained in:
Bohdan Triapitsyn
2026-06-05 18:58:50 +03:00
parent 3e0a105dfe
commit de0ebbac8b
5 changed files with 146 additions and 16 deletions
@@ -419,14 +419,13 @@ export const SidebarFilesTree: React.FC = () => {
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
inFlightDirsRef.current.add(normalizedDir);
const respectGitignore = !showGitignored;
const listPromise = files.listDirectory
? files.listDirectory(normalizedDir, { respectGitignore }).then((result) => result.entries.map((entry) => ({
? files.listDirectory(normalizedDir).then((result) => result.entries.map((entry) => ({
name: entry.name,
path: entry.path,
isDirectory: entry.isDirectory,
})))
: opencodeClient.listLocalDirectory(normalizedDir, { respectGitignore }).then((result) => result.map((entry) => ({
: opencodeClient.listLocalDirectory(normalizedDir).then((result) => result.map((entry) => ({
name: entry.name,
path: entry.path,
isDirectory: entry.isDirectory,
@@ -458,7 +457,7 @@ export const SidebarFilesTree: React.FC = () => {
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
inFlightDirsRef.current.delete(normalizedDir);
});
}, [files, mapDirectoryEntries, showGitignored]);
}, [files, mapDirectoryEntries]);
const refreshRoot = React.useCallback(async () => {
if (!root) return;
@@ -809,6 +808,15 @@ export const SidebarFilesTree: React.FC = () => {
/>
{isDir && isExpanded && (
<ul className="flex flex-col gap-1 ml-3 pl-3 border-l border-border/40 relative">
{loadErrorsByDir[node.path] ? (
<li className="flex items-center gap-2 px-2 py-1 typography-meta text-muted-foreground">
<span className="min-w-0 flex-1 truncate text-[var(--status-error)]" title={loadErrorsByDir[node.path]}>{loadErrorsByDir[node.path]}</span>
<Button variant="ghost" size="xs" className="h-6 gap-1" onClick={() => void refreshDirectory(node.path)}>
<Icon name="refresh" className="h-3.5 w-3.5" />
{t('sidebarFilesTree.actions.refreshTitle')}
</Button>
</li>
) : null}
{renderTree(node.path, depth + 1)}
</ul>
)}
+35 -7
View File
@@ -775,6 +775,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}, [openFiles.length]);
const [childrenByDir, setChildrenByDir] = React.useState<Record<string, FileNode[]>>({});
const [loadErrorsByDir, setLoadErrorsByDir] = React.useState<Record<string, string>>({});
const loadedDirsRef = React.useRef<Set<string>>(new Set());
const inFlightDirsRef = React.useRef<Set<string>>(new Set());
const activeDirectoryLoadIdsRef = React.useRef<Map<string, number>>(new Map());
@@ -1037,14 +1038,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const isCurrentRequest = () => activeDirectoryLoadIdsRef.current.get(normalizedDir) === requestId;
const respectGitignore = !showGitignored;
const listPromise = files.listDirectory
? files.listDirectory(normalizedDir, { respectGitignore }).then((result) => result.entries.map((entry) => ({
? files.listDirectory(normalizedDir).then((result) => result.entries.map((entry) => ({
name: entry.name,
path: entry.path,
isDirectory: entry.isDirectory,
})))
: opencodeClient.listLocalDirectory(normalizedDir, { respectGitignore }).then((result) => result.map((entry) => ({
: opencodeClient.listLocalDirectory(normalizedDir).then((result) => result.map((entry) => ({
name: entry.name,
path: entry.path,
isDirectory: entry.isDirectory,
@@ -1060,16 +1060,24 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
loadedDirsRef.current = new Set(loadedDirsRef.current);
loadedDirsRef.current.add(normalizedDir);
setLoadErrorsByDir((prev) => {
if (!prev[normalizedDir]) return prev;
const next = { ...prev };
delete next[normalizedDir];
return next;
});
setChildrenByDir((prev) => ({ ...prev, [normalizedDir]: mapped }));
})
.catch(() => {
.catch((error) => {
if (!isCurrentRequest()) {
return;
}
setChildrenByDir((prev) => ({
const message = error instanceof Error ? error.message : String(error ?? '');
console.error('Failed to load files directory:', error);
setLoadErrorsByDir((prev) => ({
...prev,
[normalizedDir]: prev[normalizedDir] ?? [],
[normalizedDir]: message,
}));
})
.finally(() => {
@@ -1082,7 +1090,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
inFlightDirsRef.current.delete(normalizedDir);
});
}, [files, mapDirectoryEntries, showGitignored]);
}, [files, mapDirectoryEntries]);
const refreshRoot = React.useCallback(async () => {
if (!root) {
@@ -1092,6 +1100,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
loadedDirsRef.current = new Set();
inFlightDirsRef.current = new Set();
activeDirectoryLoadIdsRef.current = new Map();
setLoadErrorsByDir({});
setChildrenByDir((prev) => (Object.keys(prev).length === 0 ? prev : {}));
await loadDirectory(root);
@@ -1148,6 +1157,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
loadedDirsRef.current = new Set();
inFlightDirsRef.current = new Set();
activeDirectoryLoadIdsRef.current = new Map();
setLoadErrorsByDir({});
setChildrenByDir((prev) => (Object.keys(prev).length === 0 ? prev : {}));
void loadDirectory(root);
}
@@ -2090,6 +2100,15 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
/>
{isDir && isExpanded && (
<ul className="flex flex-col gap-1 ml-3 pl-3 border-l border-border/40 relative">
{loadErrorsByDir[node.path] ? (
<li className="flex items-center gap-2 px-2 py-1 typography-meta text-muted-foreground">
<span className="min-w-0 flex-1 truncate text-[var(--status-error)]" title={loadErrorsByDir[node.path]}>{loadErrorsByDir[node.path]}</span>
<Button variant="ghost" size="xs" className="h-6 gap-1" onClick={() => void refreshDirectory(node.path)}>
<Icon name="refresh" className="size-3.5" />
{t('filesView.tree.actions.refreshTitle')}
</Button>
</li>
) : null}
{renderTree(node.path, depth + 1)}
</ul>
)}
@@ -3475,6 +3494,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
);
const hasTree = Boolean(root && childrenByDir[root]);
const rootLoadError = root ? loadErrorsByDir[root] : null;
const treePanel = (
<section className={cn(
@@ -3585,6 +3605,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
</li>
);
})
) : rootLoadError ? (
<li className="flex flex-col gap-2 px-2 py-1 typography-meta text-muted-foreground">
<span className="text-[var(--status-error)]">{rootLoadError}</span>
<Button variant="outline" size="xs" className="w-fit gap-1.5" onClick={() => void refreshRoot()}>
<Icon name="refresh" className="size-3.5" />
{t('filesView.tree.actions.refreshTitle')}
</Button>
</li>
) : hasTree ? (
renderTree(root, 0)
) : (