Skip to content

Latest commit

 

History

123 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wtm — Worktree Manager

A desktop app for managing git worktrees across projects on macOS. Worktrees are tabs down the left, details and a live terminal on the right, and the New Worktree form is defined by each project itself in a wtm.toml — including dropdowns populated by your own shell commands (branch lists, Jira issues via acli, anything that prints to stdout).

The app knows nothing about just, Jira, Docker, or any particular repo. It reads git and it runs the commands your config declares. A repo with heavy worktree tooling and a bare library with none are the same code path with different TOML.

Built with Tauri v2 + Rust + Svelte 5.

screenshot

Status: personal tool, not distributed. macOS 13+, Apple silicon, unsigned .app.

What works today

✅ Register any git repository; worktrees appear as tabs down the left
✅ Favorite a worktree to pin it to the top of the sidebar, under its own heading; persisted per project in ~/.config/wtm/config.toml
✅ Detail pane: branch, directory, HEAD, dirty/staged/untracked, ahead/behind, Jira key, copyable path
✅ Config-driven display — badges, links, and prefix-grouped tables (e.g. a port table) from wtm.toml
✅ The New Worktree form, generated from the project's config, with dropdowns populated by running the project's own commands
✅ Light / dark / follow-system theming, persisted to ~/.config/wtm/config.toml
✅ Config trust prompt: a project's declared commands are shown verbatim and never run until approved
✅ Diagnostics: the resolved PATH and which project tools are reachable
✅ Credential-bearing .env values are withheld from the UI entirely until revealed one at a time — see Environment values
✅ Pressing Create — renders the branch and directory, runs preflight, fetches, calls git worktree add, then the project's setup command in a live terminal
✅ A review screen showing the exact git worktree add and setup argv, plus the cwd, before anything happens
✅ Adopting an existing branch instead of creating one — the GUI form of the shell's numbered stdin picker
✅ Removing a worktree: the project's teardown steps, then git worktree remove, then optionally the branch
✅ Live terminal pane for setup output and ad-hoc [[action]]s, with input routed back so a prompt is answerable
✅ Live Claude Code, Codex, and Cursor Agent sessions with normalized transcripts, approvals, model controls, resumable conversations, and internal MCP handoff
✅ Claude's high-speed mode, from the composer's Fast pill or /fast — wtm drives the CLI over the Agent SDK protocol, so it declares the opt-in the SDK requires
✅ Voice dictation into the prompt composer — hold or tap the mic, audio goes to Deepgram Nova-3, transcript lands in the draft unsent. Off by default; the only feature that sends anything off your machine
✅ Cross-model delegation from one chat: one visible child or a customizable run of up to 20 child agents, with per-child model/effort/mode and navigable session status
✅ Open in … — a split button that hands the worktree to your editor, a terminal, the file manager, or a fresh Claude Code session; see below
✅ Browser panes — a real web page tiled beside your sessions, driveable by the agents in that worktree, with element-anchored comments you can hand to an agent; see below
🚧 [remove] strategy = "command" — the native path is the default and the one that turns the branch prompt into a checkbox
🚧 A command palette, and notify-based auto-refresh

Verified end to end against a real repository, not just against fakes. Point the suite at your own checkout and it creates a worktree — real issue-tracker lookup, real branch naming, real git worktree add — asserts it against git, then removes it and deletes the branch:

WTM_TEST_REPO=~/code/myproject cargo test -p wtm-app -- --ignored --nocapture --test-threads=1

Every expectation there is derived from your config, so it verifies wtm rather than any one project's convention. With the variable unset the tests skip.


Contents

Install · Updating · Prerequisites · Setup · First run · Registering a project · Writing wtm.toml · Browser panes · Open in … · Settings · Dev workflow · Build & install · Troubleshooting · Logs · Dependencies · Architecture


Install

You do not need to clone this repository to use wtm. Everything below is the latest release.

macOS (Apple silicon, 13+)

brew install --cask takumihendricksdev/tap/wtm

wtm is not code-signed or notarized, so macOS would refuse to open it and report it as "damaged and can't be opened" — which is Gatekeeper's phrasing for "nobody paid Apple to vouch for this", not a claim about the download. The cask therefore clears the quarantine attribute after installing.

That is a deliberate Gatekeeper bypass, and it should be a thing you know is happening rather than a surprise. Homebrew used to expose --no-quarantine for exactly this case; as of Homebrew 6 the flag is rejected and the HOMEBREW_CASK_OPTS path is dead code, so a cask for an unsigned app has no supported opt-out left. If you would rather macOS made the call, take the zip below instead of the tap.

Updating an install you already have

brew update && brew upgrade --cask wtm

brew update is not optional here — it is what fetches the tap's new cask. Without it Homebrew still holds the recipe it last saw and will report wtm as up to date whatever has been released. The upgrade backs up the old .app, swaps in the new one, and re-runs the quarantine bypass, so nothing about Gatekeeper needs doing a second time.

brew list --cask --versions wtm says which version you actually have, which is worth checking against the latest release if a feature you expect is missing.

There is no in-app updater and no update check — wtm will not tell you a new version exists.

To remove it: brew uninstall --cask wtm, or brew uninstall --zap --cask wtm to take ~/.config/wtm (your preferences, trust decisions and log) with it.

Or the raw artifacts

The zip is on the releases page with a SHA-256 checksum. Fetch it with curl or gh rather than a browser — browsers set the quarantine attribute, the CLI does not, so a CLI download of an unsigned app opens without any of the above applying.

Everything from here on is about building it yourself.


Prerequisites

Tool Version Install
macOS 13+, Apple silicon —
Xcode Command Line Tools any xcode-select --install
Rust pinned to 1.97.1 by rust-toolchain.toml see below — rustup, not a package manager
Node 20.19+ / 22.12+ brew install node
bun 1.x curl -fsSL https://bun.sh/install | bash
just (optional) 1.50+ only needed by projects whose config calls it
acli (optional) any Atlassian CLI — only for Jira-backed form fields
gh, docker (optional) any only if a project's config uses them
sox (optional) any dictation only — brew install sox
curl (optional) any dictation only — already present on macOS

Full Xcode is not required. Command Line Tools is enough for desktop Tauri.

Install Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal

Then restart your shell, or . "$HOME/.cargo/env".

Do not install Rust from a package manager — not brew install rust, not apt install rustc. Those builds ignore rust-toolchain.toml, cannot add build targets, and upgrade themselves during unrelated system upgrades — which invalidates target/ and costs you a full rebuild. just doctor warns if a packaged rust is shadowing the rustup shims.

Setup

just setup

Idempotent, safe to re-run. It pins the toolchain, adds rustfmt/clippy/the wasm target, installs cargo-nextest/cargo-deny/bacon, installs frontend dependencies, and points core.hooksPath at .githooks.

Then check your machine:

just doctor

doctor reports each tool twice over: whether it's on your current PATH, and whether it's on your login-shell PATH. That second check matters — see the PATH note below.

First run

just dev

⏱️ The first build compiles ~800 crates: 3–6 minutes on an M-series Mac, and target/ grows to 3–6 GB. Later runs are seconds. Frontend edits hot-reload; Rust edits trigger a partial rebuild and a window restart.

On first launch wtm creates ~/.config/wtm/config.toml. Open it with just config.

Registering a project

Point wtm at any git repository. It reads the repo with git worktree list --porcelain -z and shows what's there — no config required, nothing written to the repo.

Config is resolved in four layers, most specific winning:

Layer Path Committed? Use it for
Local $(git rev-parse --git-common-dir)/wtm.local.toml no — lives inside .git machine-specific overrides, or configuring a repo you don't own
Repo <repo>/wtm.toml yes the shared team convention
User ~/.config/wtm/config.toml n/a registered projects, theme, defaults for every project
Built-in defaults/wtm.default.toml — a working New Worktree form with zero configuration

To try the bundled example against a repo without touching its tracked files:

just install-example REPO=~/code/myproject

That writes wtm.local.toml into the repo's git directory, which is untracked and shared by all of that repo's worktrees. Move it to a committed <repo>/wtm.toml when you want to share it with the team. Pass CONFIG= to install your own file instead of the bundled example — useful for a config that describes an internal project and should not live in this repository.

Writing wtm.toml

examples/webapp.wtm.toml is the fully-commented reference: a form with an issue-tracker lookup, a computed slug, branch/directory naming templates, a PTY setup command, Docker teardown steps, a per-worktree port table, and guards for the commands that cannot be run from a GUI. The short version:

schema_version = 1

[project]
name = "some-lib"

[[field]]                      # → a text input on the New Worktree form
key = "name"
label = "What are you working on?"
kind = "text"
required = true

[[field]]                      # → a dropdown whose options come from a shell command
key = "base"
label = "Base"
kind = "select"
default = "HEAD"
[field.options]
kind = "command"
run  = ["git", "for-each-ref", "--format=%(refname:short)", "refs/heads"]
cwd  = "repo_root"
parse = "lines"

[naming]                       # → what gets created
branch    = "{{ name | slugify }}"
directory = "{{ name | slugify | truncate(40, '') }}"
dir_base  = "repo_parent"

Everything else has a default. Field kinds are text, multiline, number, bool, select, multiselect, path.

One template gotcha worth knowing. An undefined token is not equal to '', so a when = "env.FOO != ''" guard is true when FOO is unset — the opposite of what it reads like. Write when = "env.FOO | default_if_empty('') != ''" instead.

Trust prompt

wtm.toml can run shell commands and declare database targets. wtm will not use either from a project until you explicitly approve it, and it re-asks whenever the file's contents change. The prompt shows command argv and credential-free database targets; passwords and credential-bearing URLs are never included.

Database viewer

The Database view is attached to the selected worktree. It provides a schema/table tree, column metadata, paged and sortable table data, and an independent SQL console with selection execution, bounded results and cancellation. PostgreSQL and SQLite are supported; an engine not included in this build is shown as unavailable rather than attempted.

A local service whose port and credentials vary per worktree belongs in the repo config:

[database.local]
label = "Local database"
engine = "postgres"
scope = "worktree"             # the default
environment = "local"          # the default
host = "127.0.0.1"
port = "{{ env.DB_PORT }}"
name = "{{ env.DB_NAME }}"
user = "{{ env.DB_USER }}"
password = "{{ env.DB_PASSWORD }}"

env.* comes from the worktree's existing [[display.source]] files, so selecting another worktree resolves a different port and opens a different session. A file-backed repository is the same shape with engine = "sqlite" and path = "var/app.sqlite3"; relative paths resolve inside that worktree.

Shared TEST/STAGING/PROD connections use scope = "project". Their tabs and live session persist while you move among that project's worktrees, but never into another project. Put machine-specific definitions in the untracked, git-common wtm.local.toml; project-scoped profiles deliberately cannot use env.*, because that would make one allegedly shared session depend on whichever worktree happened to be selected when it connected.

[database.production]
label = "Production (read only)"
engine = "postgres"
scope = "project"
environment = "production"
access = "read_only"
url = "postgres://readonly-user:password@db.example.invalid/app"
tls = "require"

Credentials are rendered and retained in Rust, never listed over IPC or logged. Read-only profiles are also enforced by the driver. A read/write production console has an additional per-session UI lock; table browsing remains available while it is locked.

Environment values

A worktree's .env often holds real credentials, and this app displays that file. How it is handled:

  • The webview cannot reach the network. The CSP permits only self and IPC; database protocols and opt-in dictation live behind narrow Rust commands. There is no telemetry.
  • Nothing is logged. No log line carries an environment value.
  • No value is sent to the window. Not "no secret" — no value. The listing carries key names only; the Environment tab shows •••••••• for every row with a per-key reveal, which fetches that one value on demand, reads it fresh from disk, and re-masks when you switch worktrees. A screenshot or a screen-share cannot leak what was never sent.

There is deliberately no attempt to work out which keys are secrets. An earlier version classified them by key name, by whether the value looked like scheme://user:pass@host, and by whether a value matched another key's secret. It worked, and it was still the wrong shape: guessing fails in two directions — under-match and a credential is published, over-match and a port number needs a click — and every project's .env gets a vote on which way. The type the listing uses can no longer hold a value at all, so this is a property of the design rather than a policy that has to be kept correct.

cargo test -p wtm-app --test env_masking proves it, against a repo whose .env is nothing but credentials. It runs as part of just check — it no longer needs a real checkout, because the guarantee no longer depends on the data.

Browser panes

A browser pane is a real web page — the platform's WebKit, the same engine the app itself runs on — tiled beside your shells and agent sessions in a worktree. Open one from the empty surface's Browser button, the worktree bar, or a pane's Split control. New panes open the first visible, openable HTTP(S) [[display.link]] in repository order, with templates resolved for that worktree. Without an available link, the pane starts empty; type an address (a bare localhost:5173 gets http://, anything else https://), or pick one of the worktree's [[display.link]] URLs from the empty state. The pane has Back, Forward, Reload, an address bar (⌘L), and opens the page in your real browser on request. It follows its tile when you split, drag or resize, disappears while a dialog is up or another worktree is selected, and comes back after a relaunch at the address it was on.

A project can override where new panes start with [browser].home:

[browser]
home = "http://localhost:{{ env.WEB_PORT }}"   # rendered per worktree, like a display link

Agents can use it. Every agent session in the worktree gets mcp__wtm__browser_* tools: browser_open, browser_list, browser_navigate, browser_snapshot (the page as an outline with element refs — the primary way an agent reads a page), browser_click, browser_type, browser_fill_form, browser_select_option, browser_press_key, browser_hover, browser_scroll, browser_wait_for, browser_screenshot, browser_get_content, browser_console, browser_evaluate, browser_read_comments, browser_resolve_comment and browser_close. Ask a session to "open the dev server and check the signup form" and a pane appears in the worktree; the element it acts on flashes, and the pane's header says who is driving. Each pane has an Agent access: on/off toggle to allow or block that, Settings has a global switch, and an agent can close only the panes it opened. Everything an agent reads from a page arrives wrapped as untrusted web content — see ARCHITECTURE §6c for the whole trust story.

Comments. Turn on Comment in the pane's toolbar (⌘⇧C), hover to see what you would pick, click an element and say what should change — "make this a blue button". A numbered pin stays on the element, the comments list opens beside the page, and Send N to Claude drafts them into the focused agent's composer with the element, its text and a CSS selector, for you to read and send. Agents can also fetch them with browser_read_comments and mark them resolved.

What has been verified, and how. On macOS, driven from the app's own log rather than by a hand on the mouse: a pane opened through the store's ordinary path is placed exactly over its tile and moves with the layout; an agent-opened pane is adopted into the worktree; a pane is restored after a relaunch and reloads its address; a remote page's attempt to invoke the app is refused and its attempt to navigate onto tauri://localhost goes nowhere; and the tool sequence open → snapshot → click (which navigated) → screenshot → console → evaluate → content → comments → history → close ran against a live page and returned what it should. Not yet exercised by a person: comment mode's in-page pins and popover, the comments panel, the hide-behind-a-dialog placeholder, and the keyboard chords — all wired, none clicked.

Limits worth knowing. Clicks and keys an agent sends are synthesized DOM events, not OS input: links, buttons, checkboxes, and framework handlers all work, but a native <select> popup, a file chooser, or a window.open that needs a real user gesture will not (use browser_select_option for selects). Downloads and popups are handed to your default browser and the same pane respectively. Four browser panes per worktree, eight in all — each is a WebContent process.

Open in …

A split button in the detail header. The left half hands the worktree's directory to your preferred tool; the right half is a menu of everything wtm knows about. Picking one launches it and makes it the default, stored as ui.opener in ~/.config/wtm/config.toml.

Supported: Claude Code (in a terminal, or handed to Claude Desktop), VS Code, Cursor, Windsurf, Zed, PyCharm, IntelliJ IDEA, WebStorm, Sublime Text, Fork, a terminal, and Finder / your file manager.

Tools you do not have are listed but disabled, with the reason — usually "no code on wtm's PATH". That is deliberate: it doubles as a diagnosis of this app's most likely failure, a GUI-launched process that cannot see your shell's PATH (see Troubleshooting). On macOS a tool is found either by its shell command or by its .app bundle, so VS Code works whether or not you ever ran Shell Command: Install 'code' command in PATH.

Two things worth knowing:

  • The two Claude entries go to different places, and are detected separately. Claude Session uses the claude-cli://open?cwd=… deep link the claude CLI registers, and that handler starts a session in a terminal emulator — iTerm2 if you have it, otherwise Ghostty, Kitty, Alacritty, WezTerm or Terminal.app, in that order. It needs claude on wtm's PATH. Claude Desktop uses claude://code/new?folder=…, which the desktop app itself registers, and lands in the app with no terminal involved; it needs the app installed, not the CLI. Either can be offered without the other.
  • Claude Desktop asks to trust the folder the first time. That prompt is the app's own, once per directory, and wtm neither suppresses nor pre-answers it.
  • Fork opens the worktree, not the repository it was cut from. Its CLI takes a command rather than a path (fork open), so wtm runs it with the worktree as the working directory — same mechanism the terminal opener uses. Verified against a real linked worktree, where .git is a file rather than a directory: Fork resolves it correctly and lands on that worktree's branch.
  • Nothing here is a project config concern. Openers are built in and identical in every repository, so they need no wtm.toml entry and trigger no trust prompt. [[action]] is still the place for a per-project button. The catalogue is compiled in, so adding a tool is currently a one-entry code change in src-tauri/src/openers.rs rather than something you can do from a config file.

Settings

⌘, or the sliders button in the title bar. Everything applies as you change it; there is no OK button.

Three sections, all of them backed by keys that already existed in ~/.config/wtm/config.toml and were previously reachable only by editing that file:

Section What is in it
Appearance Colour palette, and light / dark / follow-the-system
General Which tool Open in … defaults to
Advanced The PATH override, plus read-only diagnostics — the PATH wtm actually resolved, where it came from, and which of the common tools it can find

Advanced is where to look first when a project's commands work in your terminal and not in wtm. See the PATH problem.

Palettes

Nine ship with the app: Pine (the default), Clay (the terracotta wtm wore before v0.4), Slate (near-neutral, for no colour at all), Harbor, Plum and Rose. Each works in both light and dark.

The last three, Paper, Fog and Dusk, are softer. In dark mode they sit on charcoal (about #232323) rather than near-black, and their accents are quieter. Paper is cream and warm grey with an ink-blue accent, Fog is a cool blue-grey, and Dusk is lavender. Pick one of these if the other six feel harsh over a long session.

If none of them suit, declare your own. It appears in the picker beside the built-in nine:

[ui.palettes.nord]
name   = "Nord"          # optional; the table key is used when absent
hue    = 250             # oklch hue angle, 0–360
chroma = 0.9             # how strongly the greys are tinted. 1 is the reference, 0 is flat
brand  = ["#88c0d0", "#81a1c1", "#5e81ac", "#4c688f"]

hue and chroma are all the neutral ramp needs — every surface, border and text colour in the app is derived from them in oklch, at lightness values fixed by the stylesheet. That is what keeps a hand-written palette as readable as the built-in ones: you choose the hue, and the contrast ratios are not yours to get wrong. A custom palette uses the lightness of the original six, so its dark mode is the deep one.

brand is the accent, from lightest to darkest. Dark mode uses the first two and light mode the last two, so pick the first pair to read against near-black and the last pair against near-white — not to look good as a row of four.

A palette that cannot be used is still listed, greyed out, with the reason on hover. Bad hex, a hue outside 0–360, or anything other than exactly four #rrggbb colours will do it. The rest of your config still loads.

Dev workflow

just dev        # run the app with hot reload
just watch      # bacon: check → clippy → test, in a second pane
just fmt        # format Rust + web
just check      # everything CI runs — do this before pushing
just audit      # licenses + RUSTSEC advisories
just doctor     # what's installed, and the PATH the app will actually use
just icon       # redraw src-tauri/icons from assets/brand/wtm-icon.svg
just release 0.9.1  # test, tag, publish, and update the Homebrew tap

just release <version> starts only from a clean, up-to-date main. It updates every version field, runs just check and just audit, pushes the release commit, and waits for CI before creating the tag. The tag starts the release build; after the artifact passes its checksum and the bundle inspection — right version, right minimum OS, an arm64 Mach-O inside — the command publishes the draft and pushes the verified checksum to TakumiHendricksDev/homebrew-tap. A stopped command can be run again with the same version to resume completed stages safely.

Commits are signed through the 1Password SSH agent (commit.gpgsign=true globally), so 1Password must be running and unlocked or the commit will hang waiting on Touch ID.

The first commit in a fresh clone of this repo:

git add -A && git commit -m "Initial commit"

Build & install

just build         # the .app  (~35 s warm, ~70 s cold)
just run           # build, then launch it
just install-app   # build, then install it to /Applications
just build-dmg     # ⚠️ prompts for Finder Automation permission the first time

build produces target/release/bundle/macos/Worktree Manager.app, and install-app copies it into /Applications and re-registers it with LaunchServices.

Warm is ~35s no matter how little you changed, because tauri build regenerates the context wtm-app compiles against on every run, so that one crate always recompiles. That number used to be minutes; ARCHITECTURE.md § Build performance has the measurements and what moved them.

CI builds the same bundle on every push, so a break in bundling is caught without anyone building it by hand.

The build is unsigned, by design — this is a personal tool.

  • Built locally and copied locally: runs fine, no Gatekeeper prompt.
  • Sent to someone else, or downloaded through a browser: macOS quarantines it and reports "damaged and can't be opened." Fixing that means a paid Apple Developer account ($99/yr) plus APPLE_SIGNING_IDENTITY / APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD for signing and APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID for notarization. Ad-hoc signing (identity -) still leaves the recipient approving it under Privacy & Security.
  • Universal binary: rustup target add x86_64-apple-darwin && just build-universal. Roughly doubles build time.

Troubleshooting

Symptom Cause Fix
App works under just dev, then "program not found" once installed A .app launched from Finder inherits PATH=/usr/bin:/bin:/usr/sbin:/sbin. just, acli, docker, bun all live in /opt/homebrew/bin. wtm probes your login shell's PATH at startup and uses it for every spawn. If a tool is still missing, set exec.path in ~/.config/wtm/config.toml. just doctor flags any tool that isn't on the login PATH.
just: command not found /opt/homebrew/bin missing from a non-login shell (common in editor terminals and launchd) brew install just, and add /opt/homebrew/bin to PATH in .zprofile
error: rustup could not choose a version of cargo rustup absent, or a Homebrew rust shadowing the shims Install via rustup; which -a cargo must list ~/.cargo/bin/cargo first
First just dev seems hung It isn't — ~800 crates, with long silences on tao, wry, objc2-app-kit Wait 3–6 minutes. just watch in another pane shows progress on our own crates.
Jira fields come back empty, form still works acli not authenticated or offline. Lookups are on_error = "warn", so fallbacks apply and creation is never blocked. acli jira auth login --web
svelte-check errors about the TypeScript version typescript@7 is latest but svelte-check peers ^5 || ^6 TypeScript is pinned to ~6.0.3 in package.json — don't bump it to latest
vite build fails with "Failed to load transformWithEsbuild" Vite 8 uses Rolldown/Oxc; the esbuild minifier is now a separate install vite.config.ts sets minify: 'oxc'. Don't change it back to 'esbuild'.
The microphone prompt comes back after every just build wtm is unsigned, so macOS keys the microphone grant to an ad-hoc code signature that changes on each rebuild Expected, and only affects builds you make yourself. Approve it again; an installed copy that you stop rebuilding keeps its grant.
Dictation says it needs rec SoX is not on the resolved PATH. rec is SoX's recording front-end and ships with it. brew install sox, then reopen Settings so the check re-runs. See the PATH problem if it is installed and still not found.
Dictation inserts nothing and says nothing was recorded The microphone is muted or another application holds it. wtm records perfect silence happily and the service accepts it. Check the input device in System Settings → Sound, and that no other app is recording.
The transcription key is rejected The key is for a different Deepgram project, or was pasted with surrounding whitespace Re-paste it in Settings → Advanced. wtm trims it, but a key copied with a line break from a terminal can pick up more than whitespace.
The app window opens behind another app A bare binary launched from a shell does not activate Use just run, or open "…/Worktree Manager.app" — a bundled app activates properly
open fails with error -600 Rebuilding over the same bundle path leaves LaunchServices holding a stale record just run re-registers the bundle first. By hand: lsregister -f "…/Worktree Manager.app"
@tauri-apps/cli "cli-darwin-arm64 not found" bun didn't resolve the platform-specific optional dependency rm -rf node_modules bun.lock && bun install
Setup command hangs forever with no output The project's command is prompting on stdin, and a confirm()-style helper can loop forever on EOF rather than giving up. Every captured command has a mandatory timeout; PTY commands are interactive — answer in the Terminal tab, or Cancel. Add the command to [[guards.forbid]] so it can't be run again.
Worktree list is missing a worktree you just deleted by hand git keeps stale admin entries until pruned Refresh; wtm prunes on load. Or git worktree prune.
The app quit, or something failed with no visible reason A .app launched from Finder has no stderr anyone can read just logs tails ~/.config/wtm/wtm.log, which every run appends to — including panics. WTM_LOG=debug (RUST_LOG grammar) turns up the detail.
A checkbox in New Worktree seems to have no effect on what runs Nothing, now — but this was a real bug Confirm on the review screen, which shows the exact setup argv wtm will run. If a [[setup.args_when]] flag appears when its box is unticked, that's a bug worth reporting.

Logs

Every run appends to ~/.config/wtm/wtm.log, and also writes to stderr when you're running just dev. This exists because a bundled macOS app has nowhere else to put a diagnostic: launched from Finder, its stderr goes nowhere.

just logs

Panics are logged too, and a panic inside a command surfaces in the UI as an error rather than killing the app — [profile.release] deliberately does not set panic = "abort", and a test enforces that.

Dependencies

just audit

Runs cargo deny check over the Rust tree and bun audit over the frontend: RUSTSEC advisories, license allow-list, duplicate versions, and source registries. Kept out of just check because it refreshes the advisory database over the network, and a check that fails on a train is a check people stop running.

Two things the config decides deliberately, both written down in deny.toml:

  • Vulnerabilities fail. Always. cargo-deny removed the option to downgrade them, which is the right call and what this gate relies on.

  • unmaintained is scoped to crates this workspace chose. Seventeen unmaintained advisories come through Tauri: unic-* via urlpattern, whose advisory says outright that no safe upgrade exists. Denying those would mean a permanent ignore list by advisory ID, which is where a real advisory goes to hide. If we add an unmaintained crate, it still fails.

    The count used to be seventeen, ten of them gtk-rs crates Tauri pulls on Linux. Those are gone now — not because the dependency tree changed but because deny.toml names [targets], so cargo-deny evaluates the two Apple targets this app is built for instead of the union of every platform in the lockfile. A lockfile is a union; a shipped binary is not.

One version is pinned rather than current: TypeScript is held at ~6.0.3 because svelte-check@4.7.4 peers ^5 || ^6. latest is 7.x and would break the type gate.

Attack surface is deliberately small — worth knowing when judging the dependency list. wtm makes no network requests of its own: no HTTP client crate is in the tree, no fetch/XHR/WebSocket appears in the frontend, and the webview CSP admits no remote origin. Everything it runs comes from a config file you approved (see the trust prompt) or from git.

Architecture

Design decisions, the crate layout, the ports, and why the toolchain is pinned the way it is live in ARCHITECTURE.md.

About

A project-agnostic git worktree manager. Worktrees as tabs, and a New Worktree form each repository defines for itself in a wtm.toml.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages