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:
@@ -811,9 +811,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const selectedPath = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.selectedPath ?? null) : null));
|
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 expandedPaths = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.expandedPaths ?? EMPTY_PATHS) : EMPTY_PATHS));
|
||||||
const addOpenPath = useFilesViewTabsStore((state) => state.addOpenPath);
|
const addOpenPath = useFilesViewTabsStore((state) => state.addOpenPath);
|
||||||
const removeOpenPath = useFilesViewTabsStore((state) => state.removeOpenPath);
|
const removeOpenPath = useFilesViewTabsStore((state) => state.removeOpenPath);
|
||||||
const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix);
|
const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix);
|
||||||
const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath);
|
const removeExpandedPathsByPrefix = useFilesViewTabsStore((state) => state.removeExpandedPathsByPrefix);
|
||||||
|
const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath);
|
||||||
const toggleExpandedPath = useFilesViewTabsStore((state) => state.toggleExpandedPath);
|
const toggleExpandedPath = useFilesViewTabsStore((state) => state.toggleExpandedPath);
|
||||||
const expandPaths = useFilesViewTabsStore((state) => state.expandPaths);
|
const expandPaths = useFilesViewTabsStore((state) => state.expandPaths);
|
||||||
|
|
||||||
@@ -1203,12 +1204,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : String(error ?? '');
|
const message = error instanceof Error ? error.message : String(error ?? '');
|
||||||
console.error('Failed to load files directory:', error);
|
if (message === 'Directory not found' && root && normalizedDir !== root) {
|
||||||
setLoadErrorsByDir((prev) => ({
|
removeExpandedPathsByPrefix(root, normalizedDir);
|
||||||
...prev,
|
setLoadErrorsByDir((prev) => {
|
||||||
[normalizedDir]: message,
|
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(() => {
|
.finally(() => {
|
||||||
if (!isCurrentRequest()) {
|
if (!isCurrentRequest()) {
|
||||||
@@ -1220,7 +1231,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
||||||
inFlightDirsRef.current.delete(normalizedDir);
|
inFlightDirsRef.current.delete(normalizedDir);
|
||||||
});
|
});
|
||||||
}, [files, mapDirectoryEntries]);
|
}, [files, mapDirectoryEntries, removeExpandedPathsByPrefix, root]);
|
||||||
|
|
||||||
const refreshRoot = React.useCallback(async () => {
|
const refreshRoot = React.useCallback(async () => {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|||||||
@@ -28,4 +28,23 @@ describe('useFilesViewTabsStore', () => {
|
|||||||
|
|
||||||
expect(useFilesViewTabsStore.getState().byRoot[root]?.expandedPaths).toEqual(['/repo/src']);
|
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;
|
addOpenPath: (root: string, path: string, options?: { allowOutsideRoot?: boolean }) => void;
|
||||||
removeOpenPath: (root: string, path: string) => void;
|
removeOpenPath: (root: string, path: string) => void;
|
||||||
removeOpenPathsByPrefix: (root: string, prefixPath: string) => void;
|
removeOpenPathsByPrefix: (root: string, prefixPath: string) => void;
|
||||||
|
removeExpandedPathsByPrefix: (root: string, prefixPath: string) => void;
|
||||||
setSelectedPath: (root: string, path: string | null, options?: { allowOutsideRoot?: boolean }) => void;
|
setSelectedPath: (root: string, path: string | null, options?: { allowOutsideRoot?: boolean }) => void;
|
||||||
ensureSelectedPath: (root: string) => void;
|
ensureSelectedPath: (root: string) => void;
|
||||||
toggleExpandedPath: (root: string, path: 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) => {
|
setSelectedPath: (root, path, options) => {
|
||||||
const normalizedRoot = normalizePath((root || '').trim());
|
const normalizedRoot = normalizePath((root || '').trim());
|
||||||
const normalizedPath = path ? normalizePath(path.trim()) : null;
|
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
|
} 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", () => {
|
describe("shareSession live state", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
replyCalls.length = 0
|
replyCalls.length = 0
|
||||||
|
|||||||
@@ -1126,19 +1126,19 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str
|
|||||||
const resolvedDir = directory ?? dir()
|
const resolvedDir = directory ?? dir()
|
||||||
if (!resolvedDir) return
|
if (!resolvedDir) return
|
||||||
|
|
||||||
const s = sdk()
|
|
||||||
const store = directory
|
|
||||||
? dirStoreForDirectory(directory)
|
|
||||||
: dirStore()
|
|
||||||
|
|
||||||
if (getSessionMaterializationStatus(store.getState(), sessionID).renderable) return
|
|
||||||
|
|
||||||
const loadingKey = `${resolvedDir}:${sessionID}`
|
const loadingKey = `${resolvedDir}:${sessionID}`
|
||||||
if (FETCH_MESSAGES_LOADING.has(loadingKey)) return
|
if (FETCH_MESSAGES_LOADING.has(loadingKey)) return
|
||||||
|
|
||||||
FETCH_MESSAGES_LOADING.add(loadingKey)
|
FETCH_MESSAGES_LOADING.add(loadingKey)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const s = sdk()
|
||||||
|
const store = directory
|
||||||
|
? dirStoreForDirectory(directory)
|
||||||
|
: dirStore()
|
||||||
|
|
||||||
|
if (getSessionMaterializationStatus(store.getState(), sessionID).renderable) return
|
||||||
|
|
||||||
const result = await retry(async () => {
|
const result = await retry(async () => {
|
||||||
const response = await s.session.messages({
|
const response = await s.session.messages({
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|||||||
Reference in New Issue
Block a user