Skip to content

fix: pre-release — Explorer paths, never delete a session on a failed relaunch, worktree remove safety - #182

Merged
mortenaslo merged 2 commits into
mainfrom
fix/pre-release-v0.10.0
Nov 6, 2026
Merged

mortenaslo merged 2 commits into
mainfrom
fix/pre-release-v0.10.0

Conversation

@mortenaslo

Copy link
Copy Markdown
Contributor

Summary

Fixes from the v0.10.0 pre-release pass. Each one was reproduced before it was fixed.

  1. "Open in Explorer" opened Documents instead of the folder for forward-slash paths (the form git prints, so every session opened in a sibling worktree) and for names containing a comma.
  2. Restore and Wake permanently deleted a session whose launch failed. This bug predates v0.9.0.
  3. Worktree remove (feat(git): worktrees in the git menu — where am I, switch-aware, go to, prune, remove #180) could:
    • delete a worktree nested inside the one being removed, uncommitted work included;
    • delete sleeping sessions even when git then refused the remove.
    • WSL paths were also matched the wrong way.

Why

1. Explorer

The folder was passed to explorer.exe as its raw command line. I measured the result by asking the Shell which folder each new Explorer window showed:

Path Before After (FolderOpener)
plain, spaces, &, %, trailing \, 8.3 ✓ ✓
C:/forward/slashes Documents ✓
a,b (quoting doesn't help) Documents ✓

Services/FolderOpener opens the folder itself through the shell (ShellExecute "open" on the full Windows path). It refuses anything that isn't an existing directory first: ShellExecute on a file runs it, and folder paths come from state.json and imports. It runs off the UI thread. New sibling-worktree sessions now store the Windows path form in the first place.

2. Restore and Wake

Both called LaunchSessionAsync with the default removeOnFailure: true. A refused launch (for example a WSL distro that's gone) or a PTY that failed to start therefore removed the session, and the next save made that permanent. Wake's own catch never ran, because the PTY-failure path returns rather than throws.

Both now pass removeOnFailure: false and end in FallBackToDormantIfNotLive. That also covers every WebView2 failure on restore; #175 only covered the two batched ones.

Before/after, run against a real state.json via CSM_STATE_PATH:

Before (main 448aca7) After
sidebar row gone kept, asleep
saved in state.json deleted kept, IsDormant=true

3. Worktree remove

  • Nested worktree. git's clean check skips ignored folders. Reproduced with git 2.54: worktree B in A's .gitignored .worktrees/, with uncommitted work. A reports clean, and git worktree remove A exits 0 and deletes B. The menu now refuses while GitWorktrees.NestedInside finds any, checked against a list re-read at click time.
  • Sleeping sessions are deleted only after git has removed the folder. They never go through Recently closed, so a refusal used to lose them for good.
  • WSL paths:
    • \\wsl.localhost\ is folded into \\wsl$\. Before, such a session matched no worktree at all.
    • Below the distro name, paths compare case-sensitively: /home/u/WT is not /home/u/wt.

Implementation notes

  • Not fixed here: a mixed-DPI window-bounds restore issue and two low-severity git items from the same review. Tracked in a follow-up issue.
  • CLAUDE.md: new "Opening a folder in Explorer" section; Wake/restore failure behaviour under Session Lifecycle; the nested / sleeping / WSL rules under "Worktrees in the git menu".

Test plan

  • FolderOpenerTests: forward slashes, comma, &, %, spaces, drive root; a file, a missing path, a relative path, a URL and calc.exe are all refused.
  • GitWorktreeTests: WSL case rules and both share spellings; NestedInside; and the real-git reproduction of the nested layout (A reported clean, B detected).
  • Restore failure, before/after against the real app (table above).
  • Full suite: 762/762.
  • Manual: 📁 on a session opened via the Worktrees menu.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

mortenaslo and others added 2 commits November 6, 2026 11:38
"Open in Explorer" ran explorer.exe with the folder as raw command-line text,
and Explorer silently opened Documents instead for two kinds of path —
measured by asking the Shell which folder each new Explorer window showed:

- forward slashes (C:/repos/wt-feat): the form git prints, and so the form a
  session opened through "New session in sibling worktree" — and now the
  Worktrees menu — stored as its WorkingFolder;
- a comma in the name: Explorer splits its arguments on commas, and quoting
  the path does not help (also measured).

Services/FolderOpener opens the folder itself through the shell (ShellExecute
"open" on the full Windows path), which opened the right folder for every case
tried: plain, spaces, comma, &, %, forward slashes, trailing separator, 8.3. It
refuses anything that is not an existing directory first — ShellExecute on a
FILE runs it, and a session's folder comes from state.json and imports — and
runs off the UI thread (the check is \wsl$ I/O for WSL). Used by the toolbar
📁, the sidebar menu and note search results.

LaunchSessionInSiblingWorktreeAsync now stores the Windows form of the git
path, so new worktree sessions don't carry forward slashes at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
…; worktree remove safety

From two read-only reviews of everything since v0.9.0. Each finding was
reproduced before fixing.

Relaunch (restore, wake) — data loss since before v0.9.0:
- Both called LaunchSessionAsync with the default removeOnFailure: true, so a
  session whose launch was refused or whose PTY failed to start (WSL distro
  uninstalled, claude briefly off PATH) was DELETED, and the next save made it
  permanent. Wake's catch never ran: the PTY-failure path returns, not throws.
  Both now pass removeOnFailure: false and end in FallBackToDormantIfNotLive,
  which also covers every WebView2 failure on restore (#175 only covered the
  two batched ones). Verified before/after with a real state.json via
  CSM_STATE_PATH: old build — session gone from sidebar and file; fixed —
  kept, asleep.

Worktree remove (#180):
- Refused when another worktree lives inside the target. git's clean check
  skips ignored folders: with B in A's .gitignored .worktrees/ holding
  uncommitted work, `git worktree remove A` exited 0 and deleted B (reproduced
  with git 2.54). Checked against a list re-read at click time.
- Sleeping sessions are deleted only after git removed the folder — they never
  go through Recently closed, so a refusal used to lose them for good.
- WSL: \wsl.localhost folded into \wsl$ (otherwise no match at all), and
  below the distro paths compare case-sensitively (/home/u/WT is not .../wt).

Not fixed here (follow-up issue): window bounds restore under per-monitor DPI
on mixed-DPI setups, the busy-slot key before RepoRoot resolves, and
ParseStatusV2 showing every unmerged entry as "UU".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
@mortenaslo
mortenaslo merged commit a8736b0 into main Nov 6, 2026
1 check passed
mortenaslo added a commit that referenced this pull request Nov 6, 2026
…check (#183)

Second-round review (fresh eyes on the #182 fixes) and a release-facing review:

- Worktree remove's nested check failed OPEN: ListWorktreesAsync returns empty on
  any git error, which read as "nothing nested". It now refuses unless the
  freshly read list contains the target.
- It only saw worktrees registered with the same repo. An independent clone or
  another repo's worktree in an ignored folder was still deleted silently.
  GitWorktrees.NestedRepositories scans the folder for any .git of its own (no
  junction following, never into .git, capped — over the cap it refuses).
- Stale comment: restart is no longer the only removeOnFailure: false caller.

Docs: README feature list gains the git chip/menu, Worktrees menu, Open in VS
Code / Explorer / PowerShell, drag-and-drop ordering and the full bulk-action
list; worktrees re-rated "one click, not automatic" in README and comparison.

CI: after Publish, fail if the SHIPPED exe lacks the PerMonitorV2 manifest — the
unit test only pins it into the build output (verified by hand that the
single-file publish keeps it).

Tests: independent clone + submodule found, plain worktree and a junction to a
repo not, cap returns null. 765/765. End-to-end remove re-run in the app.


Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant