fix: clean up stale file tree paths on startup

Removes missing expanded folders from persisted Files state
Prevents repeated 404 noise for stale file tree paths
Avoids startup SDK race when restoring sessions
This commit is contained in:
Bohdan Triapitsyn
2026-06-26 12:47:01 +03:00
parent 8c1a24089d
commit 4a37b9a005
5 changed files with 100 additions and 17 deletions
+21 -10
View File
@@ -811,9 +811,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const selectedPath = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.selectedPath ?? null) : null));
const expandedPaths = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.expandedPaths ?? EMPTY_PATHS) : EMPTY_PATHS));
const addOpenPath = useFilesViewTabsStore((state) => state.addOpenPath);
const removeOpenPath = useFilesViewTabsStore((state) => state.removeOpenPath);
const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix);
const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath);
const removeOpenPath = useFilesViewTabsStore((state) => state.removeOpenPath);
const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix);
const removeExpandedPathsByPrefix = useFilesViewTabsStore((state) => state.removeExpandedPathsByPrefix);
const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath);
const toggleExpandedPath = useFilesViewTabsStore((state) => state.toggleExpandedPath);
const expandPaths = useFilesViewTabsStore((state) => state.expandPaths);
@@ -1203,12 +1204,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return;
}
const message = error instanceof Error ? error.message : String(error ?? '');
console.error('Failed to load files directory:', error);
setLoadErrorsByDir((prev) => ({
...prev,
[normalizedDir]: message,
}));
const message = error instanceof Error ? error.message : String(error ?? '');
if (message === 'Directory not found' && root && normalizedDir !== root) {
removeExpandedPathsByPrefix(root, normalizedDir);
setLoadErrorsByDir((prev) => {
if (!prev[normalizedDir]) return prev;
const next = { ...prev };
delete next[normalizedDir];
return next;
});
return;
}
console.error('Failed to load files directory:', error);
setLoadErrorsByDir((prev) => ({
...prev,
[normalizedDir]: message,
}));
})
.finally(() => {
if (!isCurrentRequest()) {
@@ -1220,7 +1231,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
inFlightDirsRef.current.delete(normalizedDir);
});
}, [files, mapDirectoryEntries]);
}, [files, mapDirectoryEntries, removeExpandedPathsByPrefix, root]);
const refreshRoot = React.useCallback(async () => {
if (!root) {
@@ -28,4 +28,23 @@ describe('useFilesViewTabsStore', () => {
expect(useFilesViewTabsStore.getState().byRoot[root]?.expandedPaths).toEqual(['/repo/src']);
});
test('removes stale expanded paths by prefix without closing files', () => {
const root = '/repo';
const store = useFilesViewTabsStore.getState();
store.addOpenPath(root, '/repo/src/index.ts');
store.expandPaths(root, [
'/repo/src',
'/repo/bun test packages',
'/repo/bun test packages/web',
'/repo/other',
]);
store.removeExpandedPathsByPrefix(root, '/repo/bun test packages');
const state = useFilesViewTabsStore.getState().byRoot[root];
expect(state?.openPaths).toEqual(['/repo/src/index.ts']);
expect(state?.expandedPaths).toEqual(['/repo/src', '/repo/other']);
});
});
@@ -18,6 +18,7 @@ type FilesViewTabsActions = {
addOpenPath: (root: string, path: string, options?: { allowOutsideRoot?: boolean }) => void;
removeOpenPath: (root: string, path: string) => void;
removeOpenPathsByPrefix: (root: string, prefixPath: string) => void;
removeExpandedPathsByPrefix: (root: string, prefixPath: string) => void;
setSelectedPath: (root: string, path: string | null, options?: { allowOutsideRoot?: boolean }) => void;
ensureSelectedPath: (root: string) => void;
toggleExpandedPath: (root: string, path: string) => void;
@@ -270,6 +271,43 @@ export const useFilesViewTabsStore = create<FilesViewTabsStore>()(
});
},
removeExpandedPathsByPrefix: (root, prefixPath) => {
const normalizedRoot = normalizePath((root || '').trim());
const normalizedPrefix = normalizePath((prefixPath || '').trim());
if (!normalizedRoot || !normalizedPrefix) {
return;
}
set((state) => {
const current = state.byRoot[normalizedRoot];
if (!current) {
return state;
}
const comparablePrefix = toComparablePath(normalizedPrefix);
const comparablePrefixWithSlash = comparablePrefix.endsWith('/') ? comparablePrefix : `${comparablePrefix}/`;
const expandedPaths = current.expandedPaths.filter((candidate) => {
const comparablePath = toComparablePath(candidate);
return comparablePath !== comparablePrefix && !comparablePath.startsWith(comparablePrefixWithSlash);
});
if (expandedPaths.length === current.expandedPaths.length) {
return state;
}
const byRoot = {
...state.byRoot,
[normalizedRoot]: {
...current,
expandedPaths,
touchedAt: Date.now(),
},
};
return { byRoot: clampRoots(byRoot, 20) };
});
},
setSelectedPath: (root, path, options) => {
const normalizedRoot = normalizePath((root || '').trim());
const normalizedPath = path ? normalizePath(path.trim()) : null;
@@ -220,6 +220,21 @@ function createChildStores(entries: Array<[string, StoreApi<DirectoryStore>]>) {
} as unknown as import("./child-store").ChildStoreManager
}
describe("fetchMessagesForSession startup race", () => {
test("does not reject before sync action refs are initialized", async () => {
const { fetchMessagesForSession } = await import("./session-actions")
let error: unknown = null
try {
await fetchMessagesForSession("session-a", "/test/project")
} catch (err) {
error = err
}
expect(error).toBe(null)
})
})
describe("shareSession live state", () => {
beforeEach(() => {
replyCalls.length = 0
+7 -7
View File
@@ -1126,19 +1126,19 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str
const resolvedDir = directory ?? dir()
if (!resolvedDir) return
const s = sdk()
const store = directory
? dirStoreForDirectory(directory)
: dirStore()
if (getSessionMaterializationStatus(store.getState(), sessionID).renderable) return
const loadingKey = `${resolvedDir}:${sessionID}`
if (FETCH_MESSAGES_LOADING.has(loadingKey)) return
FETCH_MESSAGES_LOADING.add(loadingKey)
try {
const s = sdk()
const store = directory
? dirStoreForDirectory(directory)
: dirStore()
if (getSessionMaterializationStatus(store.getState(), sessionID).renderable) return
const result = await retry(async () => {
const response = await s.session.messages({
sessionID,