libghostty surfaces
Every pane is a real GPU-rendered terminal hosted in AppKit — full keyboard routing, sided modifiers, a clipboard confirmation policy for paste. No Electron, no web view, no reimplemented VT parser.
Native macOS · Swift 6 + libghostty · MIT
A tiling terminal multiplexer for the room full of coding agents you're already running.
Codex, Claude Code, and OpenCode side by side in real GPU-rendered terminals. Every pane says what its agent is doing, and macOS tells you which one is waiting on you.
Live pane status
soprano ▸ window 1
Codexediting SplitNode.swift
Claude Codewaiting on you
OpenCodewriting tests
Terminal 1252 tests, 0 failures
Driven by real agent lifecycle hooks — not output scraping.
The problem
They run asynchronously, they all stop to ask you something eventually, and terminal tabs hide the one that is blocked. So you cycle through tabs looking for the agent that went quiet for the wrong reason.
Soprano is built around the question you actually have all day: which agent needs me right now? Panes answer it themselves — the launchers install lifecycle hooks per launch, so nothing scrapes output and nothing touches your global agent config.
The launcher is up, the agent has not reported in yet.
A turn is in flight. Leave it alone.
Idle and waiting for a prompt.
A question or a permission prompt. This is the one you came for.
The turn failed. The pane keeps the state until you look.
The process exited. The layout stays where you left it.
Attention
Notifications only fire for panes that are not focused, and they are subtitled window ▸ pane so the banner names the location. Clicking one activates Soprano and focuses that exact tab. A pane's banners group together instead of stacking up.
soprano▸Codex
soprano▸Claude Code
webapp▸OpenCode
webapp▸Terminal
Live agent states across every logical window. Navigate with J/K, open with Return.
The layout
Panes live in a recursive split tree, so a window is never a fixed grid you have to fight. Then there is depth: any pane can open a private workspace on the window's z-axis, so one pane becomes a whole layout while its siblings keep running behind it.
>_ OpenAI Codex (v0.46.0) model: gpt-5-codex directory: ~/git/private/soprano › Let any pane open a private workspace on the window's z-axis. • Explored └ Read SplitNode.swift, SplitTreeView.swift, AgentManager.swift • Edited Sources/Soprano/Layout/SplitNode.swift (+42 −6) 118 + case depth(DepthLayer) 119 + var isDepthRoot: Bool { layer != nil } 120 case leaf(PaneID) • Ran swift build └ Build complete! (38.12s) • Running swift test --filter DepthTests └ [212/538] Compiling Soprano SplitNode.swift • Working (1m 14s · esc to interrupt) › ⏎ send ⇧⏎ newline ⌃T transcript ⌃C quit
⏺ I'll drop the column in a new migration. ⏺ Bash(swift run migrate --drop users.legacy_name) ⎿ Waiting for permission… ▌ Drop the legacy column? [y/n] ▌ › 1. Yes ▌ 2. No, and tell Claude what to do differently esc to cancel
opencode ~/git/private/soprano > Cover depth navigation with tests ◇ resolving imports ◇ writing tests └ DepthTests.swift ◆ 42 files scanned
~/git/private/soprano on main ❯ swift test Test Suite 'All tests' passed Executed 252 tests, 0 failures in 4.21 s ~/git/private/soprano on main ❯
One pane becomes a layout — and every layout comes back on the next launch.
What it does
Every pane is a real GPU-rendered terminal hosted in AppKit — full keyboard routing, sided modifiers, a clipboard confirmation policy for paste. No Electron, no web view, no reimplemented VT parser.
Codex, Claude Code, and OpenCode start with lifecycle hooks scoped to that launch. Your global agent configuration is never modified.
Define your own launcher under agents — command, args, env, cwd or a launchScript, colour, and its own launch key. It shows up in the sidebar and settings like a built-in.
A WebKit pane with an address bar and history controls. Agents drive it from any terminal: "$SOPRANO_BIN" browser click @e1, with snapshots that assign element refs.
Vim motions over the scrollback — viewport and buffer jumps, character and line selection, clipboard yank.
Every action in one place, plus a project launcher and search over your configured project directories on ⇧⌘P.
Searches window titles and nested pane details at once, prioritising window-title matches.
The last workspace restores on launch. Save named sessions and toggle back to the previous one with ⌃A⇧L.
Files, folders, URLs, and macOS screenshot thumbnails dropped on a terminal become shell-safe paths at the cursor.
Every pane with its git branch, a spawn menu, a sessions footer, and jump hints that appear while ⌃ is held.
Pane navigation passes through to tmux at layout boundaries, scoped to the issuing process, pane, and tab — so nesting behaves.
One commented file at ~/.config/soprano/settings.json. Rebind any action by id, or set it to null to turn it off. Saving applies immediately — no restart.
Keymap
Every binding is rebindable by action id in settings.json.
The family
Soprano, Soprano Git, Soprano Cast and Soprano Tab share one design: paper and ink, a serif for titles, and one pencil mark where your keyboard is.
Themes
These are the real palettes from Theme.swift, applied live to this page. Themes recolour open terminals — background, ANSI palette, pane headers — without a restart. Folio is the default: Paper in light mode, Ink in dark.
Your choice is remembered while you browse. It does not change how Soprano is configured.
Install
Soprano links libghostty statically, so the first build compiles it once. After that you only repeat it when the ghostty submodule moves.
# 1 · Toolchain. Homebrew Swift is required — the system CLT Swift has broken SPM. $ brew install swift zig $ xcodebuild -downloadComponent MetalToolchain # 2 · Clone with the ghostty submodule $ git clone --recurse-submodules https://github.com/frknue/soprano.git $ cd soprano # 3 · Build libghostty once (a few minutes) $ ./scripts/build-ghostty.sh # 4 · Build the release app into /Applications $ ./install.sh
Requirements: macOS 14 Sonoma or newer · full Xcode with the Metal Toolchain (Command Line Tools alone cannot build libghostty) · Homebrew Swift 6.2+ · Zig 0.15.2+.