gwm
Kylian Bardini·kbrdn1.gwm
git worktree manager - a TUI and CLI over git worktrees
gwm is a terminal UI and CLI over git worktrees, with native libgit2 and a per-repo .gwm.toml bootstrap (file copies, regex guards, lifecycle hooks). The installed command is gwm.
winget install --id kbrdn1.gwm --exact --source wingetLatest 1.10.0·September 1, 2026
Release Notes
[1.10.0] - 2026-09-01
Added
• Ctrl+n opens the create form on an issue that already exists
(#625). gwm create --issue
(#617) derives the whole <type> <issue> <desc> triple from an issue on the
forge, and it was CLI-only. The TUI is where a worktree usually gets created,
so the one place already showing a worktree list and its linked issues was
the one place that still asked for the title to be retyped as a slug and the
branch type to be read off the labels by hand.
Ctrl+n (and create-from-issue in the command palette) opens the form on a
single field, the issue number. Enter looks the issue up rather than creating
anything; when the answer lands the form becomes the ordinary structured form
with the type, the number and the derived slug in it, and a second Enter
creates the worktree. Prefilling rather than creating is the point:the slug
is a guess about a title, and this is the surface that can show the guess
before committing to it. The derivation runs through the very functions the
CLI uses, so the two cannot produce different slugs for the same title.
Where the CLI has to refuse, the form asks. A non-interactive command has
nowhere to ask when the labels name no branch type or name two, which is what
--type is for; the form lands the cursor on the type selector with
everything else filled. A closed issue prefills with a warning rather than
refusing, since nothing is written until you confirm. A number that already
has a worktree closes the form and names it, matching the CLI's exit-0
behaviour, and reads the same link gwm list shows, so a worktree attached
by hand with gwm link counts too.
The lookup runs on the async task spine, never the render path, and a result is applied only when the form asked for that exact number:the form is a second consumer of a message that also fires for the sidebar prefetch and for an explicit refresh. An issue already in the cache prefills straight away rather than waiting for a message that would never arrive, since the fetch coalesces on a cache hit.
Ctrl+t is inert in this mode and the hint row does not offer it. The
toggle swaps between the structured triple and the free-form name, which
are two ways of typing the same worktree; this one is a two-step mode, left
by answering it or by cancelling, so a third stop on the cycle would only
make the key unpredictable.
• gwm create --issue <N> opens a worktree for an issue that already
exists (#617). gwm new
covered the issue that does not exist yet:it renders the issue from
[issue_template.by_type.<type>], creates it, and opens its worktree.
Nothing covered the other half, the issue a teammate, a bot, or you last
week already filed, where the way through was to open the issue in a
browser, read its title, decide which branch type its labels implied, and
type a kebab-case slug that restated the title. Three of those four steps
were transcription, and the transcription drifted.
--issue <N> fetches the issue and derives the triple. <desc> comes from
the title with the type's title_prefix taken back off, normalised through
the same kebab-case path a hand-typed <desc> goes through and truncated
on a word boundary rather than mid-word. The prefix is resolved through the
same combinator gwm new uses when it writes the title, falling back to
the issue form's own title:, so the two halves of the flow produce the
same slug for the same title. <type> comes from the labels:
[issue_template.by_type.*].labels is the type-to-labels map gwm new
writes with, read backwards. A type declaring no labels is never a
candidate, since an empty list says nothing about which issues belong to
it.
Nothing is guessed. Labels matching no type, labels matching two, and a
repo that never configured the map are three distinct refusals, each
naming what it saw and pointing at --type <TYPE>. A closed issue is
refused too, because a worktree for one is usually a wrong number, and
--force proceeds. A worktree that already carries the number is printed
and the command exits 0, so the command is safe to re-run; that check runs
before the closed-issue refusal, since an issue closes while its worktree
is still alive. It reads the same link gwm list shows, so a worktree
attached by hand with gwm link --issue counts too.
--issue is exclusive with the positional triple and with --name, the
way --name already is:the mode is chosen explicitly, never inferred from
how many arguments were supplied. Everything after the derivation is the
existing gwm create path unchanged, #{issue} in branch_pattern
included. An issue title is arbitrary text from the forge, so it reaches
the slug through the same normaliser as a hand-typed <desc> rather than
around it, and the echoed title, URL and labels are sanitised for the
terminal.
• W opens the Working Tree listing at full size
(#592). The sidebar's
Working Tree pane is one block among five in a column that is a fraction of
the screen, so a worktree with more than a handful of changed files could
only be read two rows at a time through J / K. W now opens the same
file-explorer tree as a full-size overlay:same icons, same per-category
colours, the same change counts on the bottom rule, scrolled with
j / k, g / G, closed with Esc / q (or W again, whatever W
gets rebound to, see #613 below), and rebindable under
[tui.keys.modal.working_tree].
The listing is read when the overlay opens rather than taken from the sidebar's cache, so it does not go blank in the two states where that cache is never built:sidebar hidden, or the Details panel showing stashes. The read runs on a worker and the overlay opens on a loader, so a repository whose untracked walk is slow does not freeze the event loop on the keypress.
The right of each row says how many lines the file gained and lost
(+120 -34), from one git diff against HEAD in the same read, so
staged and unstaged changes are counted together. A directory, an untracked
file and a binary file carry no counts:the first has no diff of its own,
and for the other two git counts no lines. The column rides its own rect on
the right and is dropped whole on a terminal too narrow to keep it and a
readable file name, so the name is never what goes. D / U page the
listing by half a screen, and the key is advertised in both pane footers,
matching the commit listing (#593).
• o on the agents overlay resumes the session in the multiplexer
(#591). The overlay told you
which agent was working where and then left you to get there by hand. a
did not help:it is a pin, it changes gwm's bookkeeping, not where the
session runs. o opens a pane running the selected session.
In the worktree the overlay is about, not in the session's recorded
directory. A pinned session is pinned precisely because that directory names
the wrong tree, and for a pinned Claude session it can be the slug directory
under ~/.claude/projects rather than a worktree at all.
Multiplexer only, deliberately. With none active the key says so and
does nothing, because the point is to put the session next to gwm and the
PTY overlay would cover gwm instead. It opens at the level mux_open_in
names, exactly as t does. One target stays refused:a zellij tab takes
no trailing command in any form.
herdr works too, in two steps. None of its levels accepts a trailing
command, so gwm opens the container, waits for its new shell to reach a
prompt, then types the line in through the pane id herdr's response carries.
All three of pane split, tab create and workspace create name a pane
to run in. The wait is load-bearing rather than defensive:herdr pane run
types into the interactive shell instead of exec'ing, so a line sent while
the shell is still running its rc files lands in the middle of that output
and is dropped, measured on a worktree with direnv and a nix flake where
it took about a minute to settle. The whole sequence therefore runs off the
event loop, the status bar reads opening agent pane… meanwhile, and it
gives up after two minutes rather than leave a worker running.
What the pane runs is [tui.agent_resume], defaulting to
claude -r {session}, codex resume {session}, opencode -s {session} and
vibe --resume {session}, measured against the installed binaries. They are
configuration rather than a hardcoded table because they are four
third-party CLIs on their own release cadence. The session id is read out of
each tool's own artefacts, so it reaches the shell quoted through a
single-pass expander, the same rule the hook placeholders learned in
GHSA-fffq-vg6f-gxqm.
A session that has ended resumes without comment; a live one is flagged on the status bar, since resuming it in a second pane while it runs elsewhere may fork or refuse depending on the tool.
• c opens the commit listing full size, with load-more
(#593). The sidebar's
Commits pane is a fraction of a sidebar shared with four other blocks, and
it stops at 300 commits:seeing further meant leaving gwm for lazygit. c
now paints the same graph on the whole canvas, and m re-reads one page
deeper, up to 1500 commits, so history is paged rather than capped. The
title carries the row count and a trailing + while a deeper page exists;
the load more hint disappears once the revwalk runs out of history or the
cap is reached, so the key is never advertised where it would do nothing.
The walk runs on
Installer type: zip
Details
- Homepage
- https://github.com/kbrdn1/gwm-cli
- License
- MIT
- Publisher
- Kylian Bardini
- Support
- https://github.com/kbrdn1/gwm-cli/issues
- Copyright
- Copyright 2026 Kylian Bardini
- Moniker
gwm
Tags
cligitgit-worktreelibgit2rustterminaltuiworktree