fix(desktop): tolerate file/directory path collisions in changes tree

Merged
KI
3mo ago
+185-29

Problem

Rendering a changes section in the workspace sidebar's Changes tab crashes when the section's changed-path list contains a path that is simultaneously a file entry and a parent directory of other entries (e.g. a file deleted and a same-named directory added in the same change set, or a symlink/submodule swap). @pierre/trees keys children by exact segment name and throws Path collides with an existing file while creating directory, and the section re-renders into the same crash repeatedly — the Changes tab is effectively broken for as long as the changeset exists. 3 events on release 1.15.1.

Root cause

ChangesTreeView handed the raw files.map((f) => f.path) list straight to Pierre at both the initial usePierreFileTree({ paths }) call and the model.resetPaths(paths) sync. A git changeset can legitimately contain the file+directory shape, so the renderer must tolerate it.

Fix

New co-located helper buildCollisionSafeTreePaths disambiguates any file path that is also a directory prefix of another path by appending a zero-width-space suffix to its tree path — visually identical, but a distinct segment for Pierre — and returns bidirectional toTreePath/toRealPath maps. Verified against the real Pierre builder that the raw list throws in both orderings (file-first and directory-first) and the disambiguated list builds cleanly in both.

ChangesTreeView now feeds the collision-safe list to usePierreFileTree, model.resetPaths, buildTreeShape, and setGitStatus, keys its file lookup by tree path, and maps tree paths back to real changeset paths before they leave the component (diff selection, open file, external editor, selection echo). Every changed file stays visible and actionable; no entry is dropped.

Verification

bunx biome check <changed files>:

Checked 4 files in 6ms. No fixes applied.

cd apps/desktop && bun run typecheck: passes (tsc --noEmit, exit 0).

bun test .../buildCollisionSafeTreePaths.test.ts:

 6 pass
 0 fail
 26 expect() calls
Ran 6 tests across 1 file. [65.00ms]

Mutation check: reverting the helper to pass-through makes 4 of the 6 tests fail, so the tests catch the regression.

Direct check against @pierre/trees PathStoreBuilder:

file-first raw -> throws: Path collides with an existing file while creating directory "skills"
file-first safe -> ok
dir-first raw -> throws: Path collides with an existing file while creating directory "skills"
dir-first safe -> ok

Refs DESKTOP-HJ

https://claude.ai/code/session_258293a5-982b-4459-b3f5-11d12493402b


Summary by cubic

Fixes a crash in the Changes tab when a path is both a file and a directory prefix (e.g., a deleted file and a same-named added directory). We disambiguate file entries for @pierre/trees with a zero‑width‑space suffix and map back to real paths for all user actions.

  • Bug Fixes
    • Added buildCollisionSafeTreePaths to produce safe tree paths with bidirectional maps.
    • Used safe paths in usePierreFileTree, model.resetPaths, buildTreeShape, and git status; mapped back for selection, diff, open, and external editor.
    • Prevents crashes; all changed files remain visible and actionable.
    • Tests cover file-first/dir-first and nested collisions, including pre-existing ZWSP cases. Aligns with DESKTOP-HJ.