Repository navigation
feat(git): worktrees in the git menu — where am I, switch-aware, go to, prune, remove - #180
Merged
Merged
Conversation
…o, prune, remove
Worktrees are how this tool is mostly used (a Claude session per task, each in
its own worktree), so the git menu now knows about them:
- Chip tooltip and menu header end with "· worktree of <repo>" in a linked
worktree (from the .git FILE, no git spawn; /worktrees/ tells it apart from
a submodule).
- Switch branch lists a branch checked out in another worktree as
"feature — in wt-feature" and opens that worktree instead of failing with
"already used by worktree at …".
- Worktrees ▸ lists every worktree with branch, dirty dot and ↑/↓ (fetched only
when it opens); click focuses its session, wakes a sleeping one, or opens a
new one. Then New worktree from this branch…, Prune N missing worktrees, and
Remove this worktree….
Remove is guarded three ways: refused up front when the worktree has changes;
confirmed with the exact folder and the sessions it closes / sleeping ones it
deletes; run as `git worktree remove` WITHOUT --force, so git itself refuses
modified, untracked and locked worktrees and the main checkout. Branch deletion
is a separate question afterwards, with `branch -d`, so an unmerged branch is
kept. Sessions in the folder are closed and their PTY exits awaited first, and
the remove runs from the main worktree: on Windows a process whose cwd is in
the folder locks it and git fails half-way ("failed to delete … Permission
denied" — reproduced by running the remove from inside the worktree).
Found by the end-to-end run: git lists long paths, but a session can hold an
8.3 spelling (this machine's %TEMP%; GitHub runners' RUNNER~1) or a junction,
so a string compare put the session in no worktree and Remove never appeared.
GitWorktrees.Canonical resolves both sides via GetFinalPathNameByHandle, off
the UI thread.
Also: GitService.RunOpAsync is Task.Run-wrapped (its Directory.Exists ran on
the caller's thread — \wsl$ I/O on the UI thread for WSL), and
GitServiceThreadingTests find the repo root when .git is a file, so they pass
in a worktree checkout.
Tests: GitWorktreeTests against real git — listing/mapping, the switch
refusal, linked vs main vs submodule, clean remove (branch kept), refusing
modified/untracked/locked/main, branch -d keeping unmerged, deleting "-x",
prune touching only the missing one, 8.3 and junction spellings. --force on
the remove fails 3; dropping -- from branch delete fails 2. Run end to end in
the app with a terminal open inside the worktree: session closed, folder
deleted, merged branch deleted. 747/747.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
4 of 5 tasks
mortenaslo
added a commit
that referenced
this pull request
Nov 6, 2026
… relaunch, worktree remove safety (#182) * fix(explorer): open the right folder for forward-slash and comma paths "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 * fix: pre-release review — never delete a session on a failed relaunch; 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 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the toolbar git menu worktree-aware, since a Claude session per task in its own worktree is how this tool is mostly used:
· worktree of <repo>.feature — in wt-featureand opens that worktree, instead of failing with git'salready used by worktree at ….Why
With many worktrees per repo: panes on one repo looked identical apart from their path; Switch branch offered branches git would always refuse; there was no way to jump between worktrees from the git menu; and finished worktrees piled up.
Implementation notes
Remove is the one write beyond pull/switch, guarded three ways:
git worktree removewithout--force— git itself refuses modified, untracked or locked worktrees and the main checkout.Branch deletion is a separate question afterwards, using
branch -d(not-D), so an unmerged branch is kept.Sessions are closed and their processes awaited before git runs, and the remove runs from the main worktree. On Windows a process whose cwd is inside the folder locks it, and git fails half-way:
failed to delete … Permission denied. Reproduced by running the remove from inside the worktree. Anything else still holding the folder (editor, dev server) gets a hint with the--forcecommand to finish. That's safe, because git had already verified the tree was clean.Paths are compared by their real form. git lists long paths, but a session can hold an 8.3 spelling or come through a junction. The first end-to-end run failed on exactly that: this machine's
%TEMP%isMORTEN~1, and GitHub runners useRUNNER~1. The session was judged to be in no worktree, so Remove never appeared.GitWorktrees.Canonicalresolves both sides viaGetFinalPathNameByHandle, always off the UI thread."Worktree of" comes from the
.gitfile (gitdir: …/worktrees/<name>), with no git spawn. The/worktrees/segment tells it apart from a submodule.Two related fixes:
GitService.RunOpAsyncis nowTask.Run-wrapped. ItsDirectory.Existsran on the caller's thread, which for a WSL session meant\\wsl$I/O on the UI thread.GitServiceThreadingTestsnow find the repo root when.gitis a file, so they pass in a worktree checkout.WPF-free logic is in
Services/GitWorktrees; wording and hints are inGitStatusText.CLAUDE.md has a new "Worktrees in the git menu" section.
Test plan
GitWorktreeTestsagainst real git:branch -dkeeping an unmerged branch; deleting a branch named-x--forceto the remove fails 3 tests; dropping--from the branch delete fails 2.cmdsession open inside the worktree: chip → Worktrees → Remove → Yes → delete branch Yes. Result: the session closed, the folder was fully deleted, the merged branch was deleted, git lists only the main worktree, andcrash.logis clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX