From 697e363b7460be27267c56fbf2e71bea3461fe1c Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Tue, 28 Jul 2026 01:48:07 +0800 Subject: [PATCH] fix file manager navigation after permission errors (#1103) --- src/ui/features/file-manager/FileManager.tsx | 26 ++++++++++++++++--- .../file-manager/FileManagerSidebar.tsx | 16 +++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/ui/features/file-manager/FileManager.tsx b/src/ui/features/file-manager/FileManager.tsx index a8bced7d..64cd03c2 100644 --- a/src/ui/features/file-manager/FileManager.tsx +++ b/src/ui/features/file-manager/FileManager.tsx @@ -106,6 +106,9 @@ function FileManagerContent({ const [currentPath, setCurrentPath] = useState( initialPath || initialHost?.defaultPath || "/", ); + const lastSuccessfulPathRef = useRef( + initialPath || initialHost?.defaultPath || "/", + ); const [navHistory, setNavHistory] = useState([ initialPath || initialHost?.defaultPath || "/", ]); @@ -497,6 +500,7 @@ function FileManagerContent({ ? response : response?.files || []; setFiles(files); + lastSuccessfulPathRef.current = currentPath; clearSelection(); initialLoadDoneRef.current = true; @@ -565,7 +569,7 @@ function FileManagerContent({ } const loadDirectory = useCallback( - async (path: string): Promise => { + async (path: string, conflictAttempt = 0): Promise => { if (!sshSessionId) { console.error("Cannot load directory: no SSH session ID"); return false; @@ -595,6 +599,7 @@ function FileManagerContent({ : response?.files || []; setFiles(files); + lastSuccessfulPathRef.current = resolvedPath; clearSelection(); return true; } catch (error: unknown) { @@ -617,12 +622,27 @@ function FileManagerContent({ const httpStatus = apiError.status ?? apiError.response?.status; - // 409 = concurrent request already in flight — silently drop + // The sidebar may be listing the same path to populate its tree. + // Retry instead of leaving the breadcrumb and visible files out of sync. if (httpStatus === 409) { + if (conflictAttempt < 3) { + await new Promise((resolve) => setTimeout(resolve, 250)); + return loadDirectory(resolvedPath, conflictAttempt + 1); + } + const previousPath = lastSuccessfulPathRef.current; + lastPathChangeRef.current = previousPath; + setCurrentPath((current) => + current === resolvedPath ? previousPath : current, + ); return false; } if (apiError.response?.data?.needsSudo) { + const previousPath = lastSuccessfulPathRef.current; + lastPathChangeRef.current = previousPath; + setCurrentPath((current) => + current === resolvedPath ? previousPath : current, + ); if (!sudoDialogOpen) { setPendingSudoOperation({ type: "navigate", path: resolvedPath }); setSudoDialogOpen(true); @@ -700,7 +720,7 @@ function FileManagerContent({ } } }, - [sshSessionId, isLoading, clearSelection, t, sudoDialogOpen, currentHost], + [sshSessionId, clearSelection, t, sudoDialogOpen, currentHost], ); const debouncedLoadDirectory = useCallback( diff --git a/src/ui/features/file-manager/FileManagerSidebar.tsx b/src/ui/features/file-manager/FileManagerSidebar.tsx index 22ae9739..071369f1 100644 --- a/src/ui/features/file-manager/FileManagerSidebar.tsx +++ b/src/ui/features/file-manager/FileManagerSidebar.tsx @@ -228,8 +228,8 @@ export function FileManagerSidebar({ * Called the first time a folder is expanded via FolderTree's onSelect. */ const loadSubdirectory = useCallback( - async (folderId: string, folderPath: string) => { - if (!sshSessionId) return; + async (folderId: string, folderPath: string): Promise => { + if (!sshSessionId) return false; try { const subResponse = await listSSHFiles(sshSessionId, folderPath); @@ -262,16 +262,20 @@ export function FileManagerSidebar({ }); return updateChildren(prevTree); }); + loadedFoldersRef.current.add(folderPath); + return true; } catch (error: unknown) { const status = (error as { status?: number })?.status || (error as { response?: { status?: number } })?.response?.status; if (status === 409) { // Another request was listing this path — retry after the lock clears - setTimeout(() => loadSubdirectory(folderId, folderPath), 600); - return; + setTimeout(() => void loadSubdirectory(folderId, folderPath), 600); + return false; } + loadedFoldersRef.current.delete(folderPath); console.error("Failed to load subdirectory:", error); + return false; } }, [sshSessionId], @@ -309,8 +313,7 @@ export function FileManagerSidebar({ const parent = findByPath(directoryTree); if (parent && !loadedFoldersRef.current.has(parent.path)) { - loadedFoldersRef.current.add(parent.path); - loadSubdirectory(parent.id, parent.path); + void loadSubdirectory(parent.id, parent.path); } }, [currentPath, directoryTree, loadSubdirectory, sshSessionId]); @@ -416,7 +419,6 @@ export function FileManagerSidebar({ item.path !== "/" && !loadedFoldersRef.current.has(item.path) ) { - loadedFoldersRef.current.add(item.path); await loadSubdirectory(id, item.path); } },