I wrote The Local Agent Trio: cmux + Pi + Unsloth Studio about splitting your local stack into cmux, inference, and Pi. Git stayed in the shell.
Pi runs git status
and git diff
fine. You still lose the commit graph, the staged file list, inline diffs, and hunk buttons when you review a refactor. I kept Alt-Tabbing to Fork or squinting at git diff
output. I wanted Sourcetree in the same cmux grid as the agent.
So I built gitgui. One Rust binary paints a GUI into your terminal pane with the kitty graphics protocol. Pixels, not a TUI. Graph, sidebar, diff viewer, commit box. cmux, Ghostty, kitty, WezTerm. Over SSH it sends zlib plus base64 frames.
This is my daily layout:
Pi on the left edits the repo. gitgui on the right shows branch lanes, unstaged files, and hunk buttons. One workspace. One checkout. cmux + pi + gitgui = 😍
Why stay inside the terminal? #
cmux is a pane grid. You already split shell, Pi, and sometimes a browser. Fork or GitKraken pull you out: different font, different shortcuts, another app fighting for focus.
lazygit and gitui help for quick commits. They fall short when you read a three-file refactor with colored hunks, click Stage hunk on one block, and scan merge lanes on a graph. I wanted mouse clicks and a graph without leaving the grid.
terminal-browser and terminal-code proved the pattern: render pixels in the terminal, read pixel mouse events back. gitgui uses egui instead of Chromium.
Three steps:
- Draw the UI into an RGBA framebuffer with egui
- Send the framebuffer as a kitty graphics image each frame
- Map kitty keyboard and SGR mouse events into egui input
Your terminal never prints UI text. It shows a picture. Locally frames go through POSIX shared memory. On my Mac a 1600×1000 release build rasterizes in about 6 ms.
You get a commit graph with branch lanes, a sidebar for branches/tags/stashes, unstaged and staged lists, per-hunk stage and unstage, fetch/pull/push through your existing git
CLI, and a JSON-lines socket so Pi or Cursor queries status, selects commits, stages paths, or saves a PNG screenshot.
Keys are single letters and Ctrl combinations terminals do not steal. cmux keeps Cmd+*. Quit with q or Ctrl+C.
Install #
You need macOS or Linux and a kitty-graphics terminal.
Quick start:
curl -fsSL https://raw.githubusercontent.com/antonellof/gitgui/main/scripts/install.sh | bash
That one-liner works on public repos. Private repo? Clone with gh
and run the script locally:
gh repo clone antonellof/gitgui
cd gitgui
bash scripts/install.sh
The script pulls a release binary when GitHub has one. Otherwise it builds from source with cargo (Rust 1.95+). Success looks like:
installed: ~/.local/bin/gitgui
run in a kitty-graphics terminal (cmux, Ghostty, kitty):
gitgui open repo in current directory
gitgui /path/to/repo open a specific repo
gitgui --split right . open in a new terminal split
quit with q or Ctrl+C
Check terminal support:
gitgui --probe
cmux + Pi + gitgui #
Same workspace as the trio post. One extra split.
Pane A, Pi:
cd ~/Projects/my-app
pi
Pane B, gitgui, from the agent pane:
gitgui --split right .
cmux runs new-split right
and send
with the gitgui command. Or split by hand and type gitgui
in the new pane.
Give your agent the skill file:
mkdir -p ~/.cursor/skills/gitgui
ln -sf ~/path/to/gitgui/skill/SKILL.md ~/.cursor/skills/gitgui/SKILL.md
Drive the GUI from the shell:
gitgui ls
gitgui action '{"cmd":"status"}'
gitgui action '{"cmd":"stage","paths":["src/main.rs"]}'
gitgui action '{"cmd":"screenshot","path":"/tmp/frame.png"}'
Run action
from the pane where gitgui lives and it finds the instance via the controlling tty. Writes queue on a background git thread. Poll status until busy hits zero.
A typical session: Pi edits and runs tests on the left. You stage hunks and commit with Ctrl+Enter on the right. Or Pi stages through gitgui action
and you type the commit message yourself.
Inside the binary #
One Rust process. Three threads.
stdin reader blocks on terminal bytes and feeds a parser for kitty keys, SGR mouse, paste, resize.
main loop runs egui, tessellates meshes, rasterizes triangles, encodes kitty graphics.
git worker uses libgit2 for reads and index writes. fetch, pull, and push shell out to git
so your credential helper and SSH agent stay untouched.
The UI reads an immutable RepoSnapshot. The worker swaps in a new snapshot after each command. Rendering never calls git.
egui 0.36, a custom rasterizer, git2, libc for termios and shm, serde for the agent API. No Electron. No GPU backend. No tokio. Spec and protocol bytes live in docs/SPEC.md and docs/PROTOCOLS.md.
One refactor, start to finish #
Pi touches three files. You want the graph to update, one hunk staged and one left alone, and a commit message you wrote yourself. All inside cmux.
Before gitgui: git diff
in the shell for small diffs, Alt-Tab to Fork for a graph, lazygit when you want speed over pixels.
Now: gitgui --split right .
once per session. Tab between Pi and gitgui. cmux still rings when Pi waits on you. Mouse clicks hit the right widgets. SSH sessions use the direct transport and the same UI.
What v1 skips #
cmux and Ghostty are the main targets. kitty works. tmux and Zellij need graphics passthrough and fail today.
Ghostty on macOS has no stable split-from-child API. gitgui --split
prints a keybind hint and runs in the current pane when needed.
Merge conflict UI is out of scope for v1. Use git on the CLI or your desktop client for conflicts.
If you already run cmux and Pi, gitgui is the git pane I wished existed. Install it, split right, keep the graph beside the harness.
gitgui: github.com/antonellof/gitgui. cmux: cmux.dev. Pi: pi.dev. Related: The Local Agent Trio: cmux + Pi + Unsloth Studio.