● A native macOS workspace for coding agents
Your fleet sits on one roster: a row per worktree, mustered by repo, by status, or by what moved last, one agent to a split pane. Seahelm draws every one of them on the Ghostty engine, watches who is working and who is waiting on you, and after a reboot every session is where you left it. Twelve agent CLIs work out of the box.
▶ Forty-seven seconds of it working
A real session, not a mock-up: worktrees down the sidebar, an agent working in one of them, and a suggestion card asking which way to go.
Deck one · on deck
Seahelm has no editor and no diff-review ceremony. The whole surface is about placing agents — which repo, which worktree, which pane — and then showing you which of them needs a human.
git worktree list --porcelain; a pane is a leaf of that worktree's split tree, and
holds exactly one agent. AgentRegistry.shared knows the status of every leaf in the picture.
seahelm-web, served by a gateway inside the app itself. Pair once with an eight-digit code: it lists the fleet and opens a real terminal on any pane — xterm.js over a single WebSocket, carrying the same control calls the desktop makes and the VT bytes beside them. Taking orders over Telegram and mail is experimental.A pill at the top of the screen that stays shut until a worktree needs you. It opens for one of three things: something is running, something is waiting, something broke. Agent suggestions arrive in it as clickable cards.
An autonomous supervisor on status transitions. Green-zone actions it just takes — watch, inspect, auto-commit. Red-zone actions — broadcast an order, return a worktree to port — queue up for your approval first.
cwd, so Seahelm matches the longest worktree prefix and moves that one pane —
keeping its Ghostty surface and its zmx session — instead of standing up a stranger beside it.
Two gates keep it honest: the destination must belong to the same repo, and after any move auto-follow
holds for ten minutes, because an agent's cwd bounces while it works.
| Agent | Status | Event hooks | Suggestion cards |
|---|---|---|---|
| Claude Code | ✓ manifest + hooks | ✓ native | ✓ |
| Codex | ✓ manifest + hooks | ✓ native | ✓ |
| opencode | ✓ manifest + hooks | ✓ plugin | model-volunteered |
| Nine more | ✓ screen scan | — | — |
agent · aider · amp · claude · cline · codex · cursor · gemini · goose · kiro · opencode · pi
Deck two · below deck
247 Swift files in four layers, over the Ghostty terminal engine. No Combine, no SwiftUI — AppKit and delegates, because the thing on screen is a Metal surface with a PTY behind it.
| Layer | Path | Files | What lives there |
|---|---|---|---|
| Coordinators | Sources/App/ | 15 | Window, tabs, split operations, side panels, the modal keyboard machine. |
| UI | Sources/UI/ | 75 | Dashboard layouts, the Island, split container, worktree sidebar, diff viewer, settings, onboarding. |
| Core services | Sources/Core/ · Status/ | 133 | AgentRegistry, the detection pipeline, the manifest engine, First Mate, the control socket, hook installers. |
| Terminal & git | Sources/Terminal/ · Git/ | 19 | The Ghostty C API bridge, Station surface lifecycle, the split tree, worktree discovery. |
NormalizedEvent, so the reducer stays a pure function and the UI never learns which lane
a status came from. Ask any pane which rule decided it: seahelm pane explain <pane>.
The same five states drive the dot, the card, the Island and the rollup. There is no sixth.
$SEAHELM_PANE_ID, can
call back and split a pane, run a command in a sibling, read its scrollback, or block until
another pane goes idle.
A python3 wrapper on PATH, newline-delimited JSON-RPC over a 0600 socket. Nothing to authenticate to, nothing on the network.
$ seahelm pane list $ seahelm pane read <pane> --lines 50 $ seahelm pane split <pane> --direction right $ seahelm pane run <pane> "npm test" $ seahelm wait agent-status <pane> --status Idle $ seahelm pane explain <pane> # which rule? $ seahelm layout export
Hook shims install non-destructively into each tool's own config — and stay out of the way. CwdChanged is deliberately not registered: taking it made Claude Code hand worktree creation to us and broke --worktree.
The status poll runs on a background queue while you type on the main one, and both call into the same C library. A single lock serializes them — except key input, which deliberately takes no lock: Ghostty is thread-safe for keys, and holding it there would deadlock on a synchronous callback.
The poll is not uniform either. The worktree you are looking at is read every cycle; the rest every third, so a fleet of twenty panes costs about what three do.
Ghostty for the terminal engine, linked as a C xcframework — zmx for session persistence — Sparkle 2 for updates, rendered into an inline banner instead of its own modals.
Deck three · the rendezvous
Six agents, six worktrees, six branches that are each green on their own. Whether they still work together is a question nobody answers until merge day. Seahelm answers it after every turn — without asking anyone to commit, and without touching a single working directory.
merge-tree at a time, and the checkout is moved onto the result. Everything
except the last arrow happens inside .git — so a round that conflicts,
holds, or fails outright leaves every working directory exactly as it was.
git add -A && git commit: the repo's pre-commit hooks, whatever -A sweeps up, and a history full of machine commits. So the snapshot is built in a scratch index instead — GIT_INDEX_FILE points read-tree and add -A at a temp file, write-tree makes a tree, commit-tree makes a commit. The worktree, the real index and HEAD are never touched, so the agent working in that directory cannot tell it happened. (git stash create looks like the answer and is not: it omits untracked files, which is most of what an agent produces.)merge-tree --write-tree merges two commits and writes a tree; commit-tree turns it into a commit; that commit is the next merge's left side. Nothing here touches a worktree or an index, so nothing here can leave one wedged mid-merge — which is exactly why it can run unattended. A git merge in a real checkout could not: one conflict and that directory sits in a conflicted state until a human clears it.git branch -a, which is what the new-branch dialog offers as a base. Start a worktree from that and you have inherited a half-tested mixture of everyone's work. Detached, there is no ref to pick.reset --hard onto a commit that is already merged, so it cannot fail partway and cannot leave a conflicted checkout. But reset --hard discards anything uncommitted — so if you have been editing files in the integration checkout, the round builds the commit, holds it, and offers it. That one destructive moment always needs a person.commit-tree stamps a timestamp, so an unchanged fleet rebuilt an hour later yields a different commit holding identical files. Compare commits and every round looks like a change, and the checkout gets reset for nothing. The tree is the change key.
One agent finishing triggers the round, but the round folds in everyone — and the others may be mid-write: half a file, half a rename. Those contribute their last self-consistent state instead of a torn one that would fail in a way that looks like a real conflict. They come in whole on the round their own turn ends.
An agent leaving Running — the same edge that drives the status dot — is a stage of work reaching a resting point. Agents finishing within two seconds of each other coalesce into one round rather than one round each.
Only repos that already have an integration checkout are touched. Running /integrate once is what opts a repo in; nothing creates a directory on its own, so turning this on cannot surprise you with new state on disk.