Skip to content

fix(app): prune stale file-explorer paths during hydration#1146

Open
muzhi1991 wants to merge 1 commit into
getpaseo:mainfrom
muzhi1991:fix/file-explorer-stale-expanded-paths
Open

fix(app): prune stale file-explorer paths during hydration#1146
muzhi1991 wants to merge 1 commit into
getpaseo:mainfrom
muzhi1991:fix/file-explorer-stale-expanded-paths

Conversation

@muzhi1991
Copy link
Copy Markdown
Contributor

Fixes #1145.

Problem

Opening the file-explorer panel for a workspace fires a directory listing for every entry in expandedPathsByWorkspace. When one of those subdirectories has been removed outside the app, the listing returns an ENOENT, which writes to the shared lastError and renders the whole panel as an error banner with a Retry button. Retry recovers the current session, but the failed path stays in expandedPathsByWorkspace, so the same error returns on the next open.

I hit this on macOS with 0.1.80 after deleting a directory I'd previously expanded. Once I confirmed the stale paths were sitting in panel-state localStorage and that pruning them by hand made the banner go away, the cause was clear.

Fix

In the hydration useEffect of FileExplorerPane:

  1. await the persisted listings instead of fire-and-forget.
  2. Pass a new silent flag to requestDirectoryListing so a failed listing does not overwrite the panel's lastError.
  3. Remove any failed paths from expandedPathsByWorkspace after hydration so the persisted state self-heals.

The interactive paths (toggle, Retry, ancestor expand, navigation) don't pass silent, so their error-reporting behaviour is unchanged.

A small pure helper dropFailedExpandedPaths is exported and unit-tested so the prune logic is covered without rendering the React tree.

Verification

Only tested on macOS desktop (relay transport) — other platforms not exercised.

  • npm run typecheck --workspace=@getpaseo/app
  • npm run format:check
  • npm run lint --workspace=@getpaseo/app -- src/hooks/use-file-explorer-actions.ts src/hooks/use-file-explorer-actions.test.ts src/components/file-explorer-pane.tsx — no new findings (8 pre-existing warnings in file-explorer-pane.tsx on main are unrelated)
  • npx vitest run src/hooks/use-file-explorer-actions.test.ts ✓ 4/4

Bug repro & root-cause confirmation:

  1. Inspected panel-state in localStorage; found the stale paths under the workspace key.
  2. Cross-checked ~/.paseo/daemon.log: every open of the panel emits Failed to fulfill file explorer request for workspace … path: <stale-path> for every stale entry.
  3. Manually pruning the stale entries from expandedPathsByWorkspace made the banner stop appearing — confirming that the persisted state is the trigger, which is exactly what this patch removes automatically on hydration.

I didn't rebuild the Electron desktop app to run the patched code end-to-end, but the patch only changes the front-end hydration flow and is covered by the unit test plus the manual root-cause evidence above.

@muzhi1991 muzhi1991 force-pushed the fix/file-explorer-stale-expanded-paths branch from 07c9efc to a26ec99 Compare May 27, 2026 10:02
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR fixes a persistent error banner in the file-explorer panel that appeared whenever a previously-expanded directory had been deleted on disk. The root cause was stale paths in expandedPathsByWorkspace triggering ENOENT responses on every panel open, which wrote to the shared lastError state.

  • use-file-explorer-actions.ts: Adds a silent option to requestDirectoryListing that preserves all lastError branches (start, workspace-unavailable, host-disconnected, success, and catch) when the flag is set; also exports the pure dropFailedExpandedPaths helper.
  • file-explorer-pane.tsx: Converts requestPersistedExpandedPaths to async, awaiting a Promise.all over all persisted sub-paths before re-reading the store and pruning only the failed entries, so self-healing happens in one hydration cycle.
  • use-file-explorer-actions.test.ts: Adds four unit tests for dropFailedExpandedPaths covering empty-failure, single-path, multi-path, and non-member failure cases.

Confidence Score: 4/5

Safe to merge for the primary regression; one residual robustness gap means valid paths can be silently dropped on a transient connectivity failure during hydration.

The core fix is sound: silent hydration suppresses the error banner correctly across all five state-update sites, and the post-Promise.all pruning only touches the persisted list after all listings have settled. The main open concern is that requestDirectoryListing returns false for any failure — ENOENT, timeout, or connection drop — so a brief relay disconnect during the Promise.all batch would mark every in-flight path as deleted and silently remove them. On the next open those directories would no longer auto-expand with no feedback to the user.

The hydration logic in file-explorer-pane.tsx (specifically requestPersistedExpandedPaths) and the requestDirectoryListing callback in use-file-explorer-actions.ts warrant the closest attention because together they determine which paths are pruned and under what error conditions.

Important Files Changed

Filename Overview
packages/app/src/hooks/use-file-explorer-actions.ts Adds silent flag threading through all five error/success branches of requestDirectoryListing and exports the pure dropFailedExpandedPaths helper; logic is correct but any transient listing failure (not just ENOENT) is indistinguishable from a missing-on-disk failure and will cause the path to be pruned
packages/app/src/components/file-explorer-pane.tsx Converts requestPersistedExpandedPaths to async/Promise.all with silent flag and post-hydration pruning; type signature for requestDirectoryListing is correctly updated to include silent?; the function is still fire-and-forgotten (void) from initializeExplorer, which is intentional for non-blocking hydration
packages/app/src/hooks/use-file-explorer-actions.test.ts New test file with four focused unit tests for dropFailedExpandedPaths; covers empty failures, single-path pruning, multi-path pruning, and non-member failures — all correct

Sequence Diagram

sequenceDiagram
    participant UE as useEffect
    participant IE as initializeExplorer
    participant RPE as requestPersistedExpandedPaths
    participant RDL as requestDirectoryListing
    participant PS as panelStore

    UE->>IE: void initializeExplorer(...)
    IE->>RDL: "await requestDirectoryListing(".", {recordHistory:false})"
    RDL-->>IE: true (root listing succeeds)
    IE->>RPE: void requestPersistedExpandedPaths(...) [fire-and-forget]
    IE-->>UE: resolves

    Note over RPE: runs concurrently
    RPE->>PS: read expandedPathsByWorkspace[key]
    PS-->>RPE: [".", "src", "vendor/deleted"]
    RPE->>RPE: "candidates = ["src", "vendor/deleted"]"
    par Promise.all
        RPE->>RDL: "requestDirectoryListing("src", {silent:true})"
        RDL-->>RPE: true
    and
        RPE->>RDL: "requestDirectoryListing("vendor/deleted", {silent:true})"
        Note over RDL: ENOENT — silent, no lastError written
        RDL-->>RPE: false
    end
    RPE->>RPE: "failed = {"vendor/deleted"}"
    RPE->>PS: re-read latest expandedPathsByWorkspace[key]
    PS-->>RPE: [".", "src", "vendor/deleted"]
    RPE->>RPE: "pruned = dropFailedExpandedPaths(latest, failed)"
    RPE->>PS: setExpandedPathsForWorkspace(key, [".", "src"])
Loading

Reviews (3): Last reviewed commit: "fix(app): prune stale file-explorer path..." | Re-trigger Greptile

@muzhi1991 muzhi1991 force-pushed the fix/file-explorer-stale-expanded-paths branch from a26ec99 to f273762 Compare May 27, 2026 13:58
Comment thread packages/app/src/components/file-explorer-pane.tsx
When a subdirectory in expandedPathsByWorkspace no longer exists on disk
(e.g. removed externally while the app was closed), requestPersistedExpandedPaths
was fire-and-forgetting a directory listing for each persisted path. A single
ENOENT wrote to the shared lastError, so the whole panel rendered as an error
banner that only Retry could clear, and the stale path stayed in the persisted
state so the error returned on the next open.

Make the hydration phase await its listings, pass a new silent flag so failed
listings do not overwrite lastError, and prune any failed paths from
expandedPathsByWorkspace so subsequent opens stay clean. Interactive paths
(toggle, Retry, ancestor expand, navigation) keep their existing error-reporting
behaviour.

Fixes getpaseo#1145.
@muzhi1991 muzhi1991 force-pushed the fix/file-explorer-stale-expanded-paths branch from f273762 to 91d367c Compare May 28, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File explorer ENOENT-fails on stale expanded paths after external deletion (v0.1.80)

1 participant