AI Agent Control Plane for macOS
Run a team of AI coding agents in parallel — each in its own sandboxed worktree, on this Mac or on any machine you can SSH into — from one native, GPU-accelerated terminal.
Website · Download · Features · Quick Start
Agents get physically isolated git worktree environments. Each agent session runs in its own directory, preventing accidental modifications to the main repository. Worktrees are automatically cleaned up when tabs close.
Built on Ghostty (libghostty) for Metal GPU-accelerated terminal rendering. Vertical sidebar tabs show git branch, working directory, and notification status for each agent session.
An agent does not have to be a terminal you read. A native pane renders the conversation itself: streaming answers, tool calls you can fold away, file edits drawn as a coloured diff, and the cost and elapsed time of each finished turn. Claude speaks the protocol directly; codex, kiro, cursor and agy go through a bridge. Prefer the raw terminal? Settings → Agent Teams → Agent Panes switches back.
One leader, several workers, and a task board they share. The leader delegates, the workers reply with a fixed five-field header (STATUS / FILES / VERIFY / NEXT / FULL_REPORT), and the app files each reply where the leader can collect it. Workers can sit on different machines than the leader.
tm-agent create 3 --adopt # this pane becomes the leader, three workers join
tm-agent add reviewer --cli codex # add one more, on a different CLI
tm-agent add builder --host jw-server --dir /root/build # …or a different machine
tm-agent delegate executor 'implement T1'
tm-agent wait --timeout 120 --mode anyPoint at a repository and a machine, and term-mesh does the rest: clone (or reuse a folder), lay out one worktree per agent, start the leader, and open the team in a workspace. The destination field browses the remote machine's folders, and the branch field completes with Tab.
A Project created on a peer belongs to that host, not to the Mac window that created it. As long as the host's term-meshd and its sessions are running, you can quit the local app, connect again from this or another Mac, select the Project in the sidebar, and reattach its exact leader and member panes. Closing a viewer only detaches that Mac; deleting the Project from its owner ends the remote resources.
Visual notification rings on sidebar tabs alert you when agents need attention — completed tasks, errors, or prompts waiting for input.
Open web pages, documentation, or dashboards directly in a split panel without leaving the terminal.
- CPU/Memory monitoring with automatic process discovery
- SIGSTOP/SIGCONT process control when thresholds are exceeded
- Real API cost tracking by parsing Claude Code's JSONL logs with incremental reads
- Model-specific pricing: Opus $5/$25, Sonnet $3/$15, Haiku $1/$5 per MTok
FSEvents-based file watcher tracks create/modify/remove events across watched directories. The dashboard renders a heatmap with top-10 hot files, recent events, and per-minute timeline buckets.
Monitoring dashboard available as a split panel in-app (Cmd+Shift+D) or standalone browser at http://localhost:9876.
Full control via Unix socket and HTTP REST API — automate tab creation, pane management, notifications, and more from scripts or other tools.
Attach another machine from your menu bar — its split layout opens in a relay window on your laptop, with live PTY streams in every pane. Drive remote panes with the usual keybindings (Cmd+D split, Cmd+T new tab, Cmd+W close, divider drag, click-to-focus, tab strip switching). ssh -L tunnels the wire, so any host you can SSH into works; Bonjour discovery and a recent-hosts list make reconnecting one keystroke, and auto-reconnect covers sleep/wake and network blips.
A Mac peer serves from the app itself. A Linux peer runs term-meshd, installed by one command:
curl -fsSL https://raw.githubusercontent.com/x-mesh/term-mesh/main/scripts/install-linux.sh | bashThat also installs tm-agent and the agent bridge, so the machine can host agents and not just terminals. Edit Peer Host runs a health check that reports what it found — which binaries, which versions, whether a pane's PATH can reach them — instead of leaving a silent failure to guess at. See docs/peer-linux-host.md and the peer federation primer.
An agent can run on a peer while its pane lives here. The far machine owns both the process and the Project manifest that maps it back to the right leader and member panes. Quitting term-mesh does not end the work — reconnect to the host, select the Project, and the app reattaches to the sessions still running there. Environment values an agent needs (API keys, for instance) travel as a file that the launcher sources and deletes, never as ssh command-line arguments, which every other process on that machine can read.
┌──────────────────────────────────────────────────────────┐
│ term-mesh (macOS App) │
│ │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ Native Shell │ │ Dashboard (WKWebView) │ │
│ │ Swift + AppKit │ │ Chart.js + HTTP Poll │ │
│ │ │ │ http://localhost:9876 │ │
│ │ Vertical Tabs │ │ │ │
│ │ Split Panes │ │ ┌────────┐ ┌────────────┐ │ │
│ │ Notifications │ │ │CPU/Mem │ │ File │ │ │
│ │ │ │ │Monitor │ │ Heatmap │ │ │
│ ├─────────────────┤ │ ├────────┤ ├────────────┤ │ │
│ │ Terminal Engine │ │ │API Cost│ │ Agent │ │ │
│ │ libghostty │ │ │Tracker │ │ Status │ │ │
│ │ (Metal GPU) │ │ └────────┘ └────────────┘ │ │
│ └─────────────────┘ └──────────────────────────────┘ │
│ │ │ │
│ │ Unix Socket / HTTP │ │
│ └───────────┬─────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ term-meshd (Rust Daemon) │ │
│ │ │ │
│ │ Worktree (git2) │ Monitor (sysinfo) │ │
│ │ Watcher (notify) │ Usage (JSONL parsing) │ │
│ │ Budget Guard (SIGSTOP/SIGCONT) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
A peer is the same picture with the terminal on the other end of an SSH tunnel. The pane is local; the shell, the agent, and the files are not:
this Mac peer host
┌──────────────┐ ┌────────────────────┐
│ pane / UI │ │ Mac: the app │
│ agent panel │◀── ssh -L ── peer ────▶│ Linux: term-meshd │
│ │ protocol │ │
│ leader ─────┼── delegate / collect ─▶│ shell · agent CLI │
└──────────────┘ │ worktrees │
└────────────────────┘
The far side owns the process. Quitting here does not end the work there.
brew install --cask x-mesh/tap/term-meshThe cask downloads the latest DMG from GitHub Releases, copies term-mesh.app into /Applications, and strips the Gatekeeper quarantine attribute automatically — so the unsigned build launches without a manual xattr step.
Bundled CLI helpers (tm-agent, term-mesh-run) are symlinked to $(brew --prefix)/bin.
Upgrade / uninstall:
brew upgrade --cask term-mesh
brew uninstall --cask term-mesh # remove the app
brew uninstall --cask --zap term-mesh # also remove ~/Library data and ~/.term-meshIf you previously installed term-mesh manually (e.g. by dragging the DMG) and then run brew install --cask, Homebrew refuses to overwrite the existing bundle:
Error: It seems there is already an App at '/Applications/term-mesh.app'.
Either pass --force to let Homebrew take over the existing app:
brew install --cask --force x-mesh/tap/term-mesh…or move the existing app out of the way first:
mv /Applications/term-mesh.app ~/Downloads/term-mesh.app.manual-backup
brew install --cask x-mesh/tap/term-meshQuit any running term-mesh instance before either command.
Grab term-mesh-macos-<version>.dmg from the latest release, open it, and drag term-mesh.app into /Applications. Because the build is not notarized, run once after copying:
xattr -dr com.apple.quarantine /Applications/term-mesh.appAuto-updates (Sparkle) handle subsequent versions.
| Component | Version | Notes |
|---|---|---|
| macOS | 14 Sonoma+ | Metal 2 required |
| Xcode | 15+ | Swift 5.9+ |
| Rust | stable 1.88+ | edition 2021 |
| Zig | 0.15+ | For libghostty build |
A peer host needs none of the above — see Peer hosts.
# 1. Clone and setup
git clone https://github.com/x-mesh/term-mesh.git && cd term-mesh
# 2. Build libghostty + native app
./scripts/setup.sh
./scripts/reload.sh --tag dev
# 3. Run daemon only (for development)
cd daemon && cargo run --bin term-meshdThe term-mesh CLI controls the app via Unix socket. Install location: ~/bin/term-mesh
Two more ship beside it: tm-agent drives agent teams (see Agent Teams), and term-mesh-run wraps a command in a PTY. Homebrew symlinks all three into $(brew --prefix)/bin.
term-mesh run claude code # Run Claude Code in PTY wrapper
term-mesh run -- kiro-cli chat "fix this" # Run any command
term-mesh run --sandbox claude code # Run in isolated git worktreeterm-mesh list-windows # List all windows
term-mesh new-window # Open a new window
term-mesh list-workspaces # List workspace tabs
term-mesh new-workspace # Create a new workspace tab
term-mesh new-workspace --command "htop" # New workspace running a command
term-mesh select-workspace --workspace 2 # Switch to workspace by index
term-mesh rename-workspace "My Project" # Rename current workspace
term-mesh close-workspace --workspace 3 # Close a workspace tab
term-mesh current-workspace # Show active workspace infoterm-mesh new-split right # Split right (terminal)
term-mesh new-split down # Split down
term-mesh new-split right --type browser --url https://example.com
# Split with browser panel
term-mesh list-panes # List panes in current workspace
term-mesh focus-pane --pane 2 # Focus a specific pane
term-mesh new-pane --type browser --url https://docs.dev
# Add browser tab to current pane
term-mesh close-surface --surface surface:5 # Close a surface (tab in pane)
term-mesh close-surface --surface surface:5 --close-pane
# Close surface and collapse paneterm-mesh send "ls -la" # Send text to terminal
term-mesh send-key Enter # Send a key press
term-mesh read-screen # Read visible terminal content
term-mesh read-screen --scrollback --lines 500
# Read scrollback buffer
term-mesh capture-pane # tmux-compatible captureterm-mesh browser open https://github.com # Open browser in new split
term-mesh browser navigate https://docs.dev # Navigate existing browser
term-mesh browser eval 'document.title' # Execute JavaScript
term-mesh browser snapshot # Get DOM snapshot
term-mesh browser snapshot --interactive # Interactive DOM with selectors
term-mesh browser click '#submit-btn' # Click an element
term-mesh browser type '#search' "query" # Type into an input
term-mesh browser wait --selector '.loaded' # Wait for element
term-mesh browser get title # Get page title
term-mesh browser get text '#content' # Get element text
term-mesh browser back # Navigate back
term-mesh browser reload # Reload page
term-mesh browser cookies get # Get all cookies
term-mesh browser console list # List console messagesterm-mesh notify --title "Done" --body "Build complete"
term-mesh set-status build "passing" --icon checkmark --color "#00ff00"
term-mesh clear-status build
term-mesh set-progress 0.75 --label "Building..."
term-mesh log --level info --source agent "Task completed"
term-mesh sidebar-state # Full sidebar state dumpterm-mesh resize-pane --pane 2 -R --amount 10 # Resize pane right
term-mesh swap-pane --pane 2 --target-pane 3 # Swap two panes
term-mesh break-pane # Break pane to new workspace
term-mesh join-pane --target-pane 2 # Join pane into another
term-mesh pipe-pane --command "tee log.txt" # Pipe pane output
term-mesh last-pane # Focus previous pane
term-mesh next-window # Next workspace
term-mesh find-window --content "error" # Search across workspacesCommands accept UUIDs, short refs (window:1, workspace:2, pane:3, surface:4), or indexes. Output defaults to refs; use --id-format uuids or --id-format both for UUIDs.
term-mesh --json list-workspaces # JSON output
term-mesh --id-format both list-panes # Include UUIDs in output
term-mesh identify # Show caller's workspace/surface| Variable | Description |
|---|---|
TERMMESH_WORKSPACE_ID |
Auto-set in term-mesh terminals; default --workspace |
TERMMESH_SURFACE_ID |
Auto-set in term-mesh terminals; default --surface |
TERMMESH_SOCKET_PATH |
Override socket path (default: /tmp/term-mesh.sock) |
TERMMESH_SOCKET_PASSWORD |
Socket authentication password |
Run term-mesh help or term-mesh <command> --help for full details.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/monitor |
System + process snapshots, budget config, usage summary |
| GET | /api/sessions |
Terminal sessions from the Swift app |
| GET | /api/watcher |
File heatmap snapshot (top files, events, timeline) |
| GET | /api/usage |
Per-session API cost and token usage |
| POST | /api/process/stop |
SIGSTOP a process {"pid": 1234} |
| POST | /api/process/resume |
SIGCONT a process {"pid": 1234} |
| POST | /api/budget/auto-stop |
Toggle auto-stop {"enabled": true} |
| POST | /api/watcher/watch |
Start watching a path {"path": "/..."} |
| POST | /api/watcher/unwatch |
Stop watching a path {"path": "/..."} |
Socket path: $TMPDIR/term-meshd.sock
| Method | Description |
|---|---|
ping |
Health check (returns "pong") |
worktree.create |
Create a sandboxed worktree |
worktree.remove |
Remove a worktree by name |
worktree.list |
List all term-mesh worktrees |
monitor.snapshot |
Get system/process resource snapshot |
monitor.track |
Track a PID for monitoring |
monitor.untrack |
Stop tracking a PID |
process.stop |
Send SIGSTOP to a process |
process.resume |
Send SIGCONT to a process |
budget.auto_stop |
Enable/disable auto-stop |
watcher.watch |
Watch a filesystem path |
watcher.unwatch |
Unwatch a filesystem path |
watcher.snapshot |
Get heatmap snapshot |
usage.snapshot |
Get API cost/token snapshot |
session.sync |
Push session list from Swift app |
session.list |
List terminal sessions |
Sources/ # the macOS app (Swift + AppKit + SwiftUI)
TeamOrchestrator*.swift # teams, delegation, remote leaders and agents
Peer*.swift # peer hosts: discovery, tunnels, health, panes
Panels/ # agent panes, terminal panes, browser panes
GhosttyTerminalView.swift # libghostty surface hosting
TerminalController*.swift # the socket API the CLI talks to
daemon/ # Rust
term-meshd/ # the daemon: monitoring, worktrees, peer serving
term-mesh-cli/ # `term-mesh` and `tm-agent`
term-mesh-peer-relay/ # the helper a remote pane runs
tm-agent-bridge/ # agent CLIs that do not speak the protocol natively
peer-proto/ # wire types, shared with Swift
swift/PeerProto/ # the peer protocol, Swift side
vendor/bonsplit/ # the split/tab layout engine (vendored, not a submodule)
ghostty/ # libghostty (submodule, JINWOO-J/ghostty fork)
scripts/ # setup.sh, reload*.sh, install-linux.sh, release tooling
docs/ # peer federation, Linux hosts, protocol notes
# Run all Rust tests
cd daemon && cargo test
# Run daemon in development mode
cd daemon && cargo run --bin term-meshd
# Performance benchmarks (requires running daemon + socat)
bash daemon/scripts/bench.shThis project is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
Built on Ghostty by Mitchell Hashimoto.



