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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user