Every async flow that ends by replacing the tree root (refreshListing,
rescopeServer, reloadDir, and the app.js back/forward popstate handler) ran
without any concurrency guard. Two overlapping listings — a double-click into
a folder, a refresh fired mid-load, rapid back/forward — could resolve out of
order, so a slow fetch would setRoot/pushState on top of a newer navigation
and leave the tree out of sync with state.currentPath and the URL bar.
Introduce a shared monotonic nav-sequence token in events.js (beginNav /
isCurrentNav, exported so the app.js popstate handler joins the same
sequence). Each flow claims a token before its fetch and bails if a newer
navigation has started by the time it resolves — last navigation wins,
stale ones drop their result before mutating anything. navigateIntoFolder's
FS branch is reordered to mutate scope state only after a successful fetch +
token check, so a bail leaves the previous scope intact instead of
half-swapped.
Duplicate-fetch race fixed at the source: tree.loadChildren took only a
`loaded` check, so rapid Enter/ArrowRight key-repeat or a double-click
landing during a single-click's load fired two concurrent fetches that raced
in setChildren. Added a `loading` in-flight flag that serializes per-node
loads — the second caller is a no-op until the first resolves. This also
removes the need to await the fire-and-forget toggleFolder calls in the
keyboard handler.
Also surfaces reloadDir fetch failures via statusError instead of swallowing
them (the success path's create/rename/delete toast no longer hides a failed
refresh).
All 6 browse Playwright specs pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>