Skip to content

Add a Worktrees tab to projects - #107

Open
vivi7 wants to merge 18 commits into
doctly:mainfrom
vivi7:feat/worktrees-tab
Open

vivi7 wants to merge 18 commits into
doctly:mainfrom
vivi7:feat/worktrees-tab

Conversation

@vivi7

@vivi7 vivi7 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Worktrees tab to projects, between Git and Settings. It lists every git worktree of the project's repositories and manages them from one place. It's inspired by the VS Code extension git-worktree-manager.

Today Switchboard only knows about the worktrees it creates itself under <project>/repos/. Worktrees made by Claude (.claude/worktrees/), by Codex or by hand don't show up anywhere. This tab shows them all and adds the git operations that are missing: lock, move, prune and repair.

Unlike #99, this tab does change repositories: creating and removing worktrees is what it's for. Every action is listed below. If you'd rather review it in two steps, I can split out a read-only list first (listing, state and open folder) and send the actions afterwards.

What's included

  • List: worktrees are grouped by repository (by --git-common-dir), and each group can be collapsed. Each row shows:

    • the branch, or the hash when detached;
    • its folder, with the full path in the tooltip;
    • a kind badge: main, Switchboard, Claude, Codex, classic or other;
    • locked (with its reason), missing or prunable;
    • dirty, ahead/behind and the last commit;
    • how many Switchboard sessions are running in it.

    The list has search and sort, and it refreshes when the window gets focus.

  • Repository discovery: an attached folder that is a repository is used as is. In a folder that isn't, it looks for .git entries up to three levels deep. It skips node_modules, dist, build and hidden folders, except the ones worktrees conventionally live in (.claude, .codex, .repos, .worktrees).

  • Create: a new branch from a start point, an existing local or remote branch, or a detached HEAD.

    • The default folder is <repo>.worktrees/<branch>, the extension's convention. It sits outside the repository, so nothing needs to be added to .gitignore.
    • Options: attach the worktree to the project, copy files such as .env*, run a setup command, and open a Claude, Codex or terminal session in it.
  • Remove: shows the worktree's state and running sessions first. It offers force when the worktree is dirty and unlock when it's locked. It can also delete the branch, and asks again before using -D when the branch isn't merged. The main worktree can't be removed.

  • Maintenance: lock with a reason, unlock, move, repair, prune with a preview, and Remove merged. Remove merged only ticks worktrees that are clean, unlocked, have no sessions running and have commits of their own.

  • Switchboard integration:

    • new session or terminal in the worktree;
    • attach to or detach from the project;
    • open the folder;
    • copy the path.
  • Per-project settings (stored under the settings key worktrees:<projectId>, no schema change):

    • path template, with the extension's variables $BASE_PATH, $BASE_NAME, $REF_NAME, $INDEX and so on;
    • copy and ignore patterns;
    • setup and pre-remove commands;
    • what to start after creating a worktree;
    • sort order.

Security

Following your review of #99:

  • Escaping: every attribute value goes through an attribute escaper that handles quotes (wtAttr, local to the view since escapeAttr isn't on main). Every text value goes through escapeHtml.
    • A new test renders the list and the Remove merged dialog with the real escapeHtml from utils.js. It pushes x"><img src=x onerror=alert(1)>'& through repository names, paths, branch names, lock reasons and commit subjects, then checks that no element and no extra attribute comes out.
  • Path checks: every IPC call re-checks its paths in the main process. The repository must belong to the project, and the path must be in that repository's own git worktree list. The main worktree is refused for anything that changes it. Git always runs with argv, never through a shell.
  • User commands: the setup and pre-remove commands are the only things that run through a shell ($SHELL -lc, or cmd.exe /d /s /c on Windows).
    • They come only from the project settings, never from files in a repository, so opening a repository runs nothing.
    • They run in the worktree, in their own process group, with a 15-minute timeout that kills the whole group.
    • A pre-remove command that fails stops the removal.
  • Removing a missing worktree removes only that worktree's record. It never runs a global prune, so it can't drop the records of worktrees on a disconnected drive.
  • No native alert/confirm: every dialog is an in-app modal.

Changes to existing files

  • git.js:
    • new worktree, branch and merge helpers;

    • worktreeAdd gains optional startPoint, createBranch, track and detach options. Existing callers behave as before.

    • One change that affects every git call: git now runs with GIT_TERMINAL_PROMPT=0, GIT_OPTIONAL_LOCKS=0 and LC_ALL=C. This means:

      • no credential prompt can hang a call;
      • a background status doesn't take the index lock;
      • error messages are always in English, so they can be parsed.

      Happy to scope this to the new helpers if you prefer.

  • projects.js:
    • registerWorktreeFolder attaches an existing worktree without running git;
    • moveFolderPath moves a folder row and the project's default and track working directories.
    • attachWorktree is unchanged.
  • main.js, preload.js: the tab's IPC endpoints, each wrapped in guarded().
  • projects-view.js: the tab registration (three small edits).
  • index.html: the new script tag.
  • style.css: a .wt-* block placed after the Git tab styles.
  • README.md: a line in "Features at a glance" and a section under "Worktrees".

Size: about 3.8k lines, of which ~2.75k are code and ~1k tests.

Testing

  • npm test: 483/485. The 2 failures are db-schema-reconcile and fail on main too. The new tests cover:
    • parsing and every git helper, against real temporary repositories (test/git-worktrees.test.js);
    • discovery in nested folders, kinds, path checks (including symlinked attachments), create with templates and copied files, remove (locked, dirty, with branch, a missing folder that leaves other records alone), move, prune and repair. Also user commands that succeed, fail or time out (test/worktrees.test.js).
    • the view: sort, filter, session counts, menus, the merged list, and hostile names with the real escapeHtml (test/project-worktrees-view.test.js).
  • End-to-end: I drove the real app with an isolated data directory against fixture repositories, including a non-repository folder holding two repositories and a hand-made .claude/worktrees/ worktree. I checked:
    • listing and kinds;
    • create with an attached folder, a copied .env and a setup command;
    • a terminal session showing up as "1 running";
    • lock, then removing it while locked, dirty, and with an unmerged branch (the second confirmation);
    • move, which updates the project's folder row;
    • prune after deleting a folder by hand;
    • Remove merged.
  • Requires git 2.36 or newer for worktree list --porcelain -z. With older versions the tab shows a clear error.

abasiri and others added 18 commits October 1, 2026 01:15
Cmd+Left/Right switched sessions everywhere, so it stole line start/end
in terminals, the file editor and text fields. In a terminal it now sends
Ctrl+A / Ctrl+E (Ctrl+Left/Right stays xterm's word jump off the Mac), and
text fields and editors keep their own handling. Cmd+Up/Down and
Cmd+Shift+[ / ] still switch sessions; the grid keeps Cmd+Arrow for cards.

Session switching in the Projects tab followed the Sessions sidebar, which
jumped between projects. It now follows the project's own list: the session
pane when working, the track cards on the overview.

Every session's file tab shares one viewer, and showing a tab reopened it
from the text captured when the file was first opened, so switching
sessions or toggling the explorer threw away typed edits. The viewer now
copies its buffer and cursor back onto the tab before showing another, and
leaves the buffer alone when the same tab repaints. Replacing a background
session's file no longer wipes the file on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
worktreeList parses `git worktree list --porcelain -z`, and worktreeAdd can
now start a branch at a given commit, track a remote branch or check out a
detached HEAD. Also: delete a branch, list branches, find the merged ones and
the default branch, and read the last commit. These back the Worktrees tab.
worktrees.js finds a project's repositories (an attached folder that is not
a repository is searched a few levels down), lists every worktree of each
with git, and tells main, Switchboard, Claude, Codex and side-by-side
worktrees apart. Every action re-checks that its path is a worktree of one
of the project's repositories.

Actions: create (new, existing, remote or detached; path from a template,
next to the repository by default; copy local files by pattern), remove
(with the branch), lock, unlock, move, repair, prune, the merged ones, and
attach to or detach from the project. Commands to run after creating or
before removing come from the project's settings only.

projects.js gains registerWorktreeFolder and moveFolderPath, so a worktree
made or moved here keeps the project's rows and cwds right.
Between Git and Settings, the tab lists every worktree of the project's
repositories, grouped by repository: branch, folder, where it came from
(main, project, Claude, Codex), locked or missing, local changes,
ahead/behind, last commit and the sessions running in it. Filter and sort
like the git-worktree-manager extension does.

From the tab: create a worktree (new, existing, remote or detached; the
folder comes from a template, next to the repository by default), remove
it and its branch, lock, unlock, move, repair, prune, remove the merged
ones, start a Claude or Codex session or a terminal in it, attach it to or
detach it from the project, open its folder and copy its path. Worktree
settings hold the folder template, the files to copy into a new worktree
and the commands to run after creating and before removing one.
- Removing one worktree no longer prunes the records of others whose folder
  is missing for now (moved by hand, on a drive that is not mounted).
- A setup command that ignores SIGTERM is killed, and one that leaves a
  process in the background answers when the shell exits.
- Remove merged does not tick a worktree with no commits of its own or with
  sessions running in it, ignores a stale base choice, and lists branches
  git kept.
- A folder attached through a symlink matches git's real path.
- Copy patterns: ignores apply inside a folder copied whole, links are
  copied as links.
- Remote names with a slash, and git run with LC_ALL=C and no optional
  locks, so its messages are recognised and a status never takes the index
  lock.
Combines repository history with the Git workspace, removes the redundant recent-commits section, and keeps repository selection synchronized across Changes and History.
Extra claude CLI arguments, appended to the launch command as raw shell
like the pre-launch command, e.g. --settings '{"outputStyle": "Learning"}'.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar and its resize handle were 100vh tall, but they sit in the app
container, which ends above the 22px status bar. So the container's content
ran 22px past it, and overflow: hidden still let a focus() or
scrollIntoView() scroll it: the whole window then slid up 22px, cutting
off the top of the sidebar and the project header, and stayed that way.
The last 22px of the sidebar lists were also always hidden.

The sidebar and the handle now take the container's height, and the
container uses overflow: clip, so nothing can scroll it.
@abasiri

abasiri commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Thanks, this is good stuff, the only challenge i have with it is that it shows All worktrees rather than the current project, so it creates a confusion. For that reason i moved it under 'Git'. However still + New Session will create a new session of a different worktree under current project that might be unrelated to this project and it breaks the project boundary.

Would love to hear your use case for that.

Here is new look and feel for it
image

@vivi7

vivi7 commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Actually I just used the work tree feature tab to scan more multiple folders to have the possibility to manage different work tree from different places create for esemplo by cloud code directly.

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.

3 participants