Crush Multi-Agent Display Design
TL;DR: Claude Code uses tmux/iTerm2 for multi-agent display because Node.js agents need separate OS processes. Go agents are cheap goroutines with zero reason to shell out to tmux. Native Bubble Tea gives full visual control, zero-latency streaming via channels, and no external dependencies. The
tuiosproject proves you can build an entire terminal multiplexer in pure Bubble Tea if you really wanted to.
Multi-Agent Display: tmux/iTerm2 vs Native Bubble Tea
Status: Complete
Analysis of how Claude Code implements multi-agent display via tmux and iTerm2, and how native Bubble Tea compares as an alternative for Go-based agents.
How Claude Code Agent Teams Display Works
Three Display Backends
Claude Code's BackendRegistry detects the terminal environment and selects a display backend. The teammateMode setting controls this:
| Mode | Detection | Backend |
|---|---|---|
"auto" (default) | $ITERM_SESSION_ID → iTerm2; $TMUX → tmux; else → in-process | Auto-detect |
"tmux" | Forced | tmux split panes |
"iterm2" | Forced (auto-detected when $ITERM_SESSION_ID present) | Native iTerm2 split panes |
"in-process" | Forced | All agents in one terminal, Shift+Up/Down to switch |
Configuration:
{
"teammateMode": "tmux",
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}
Or via CLI flag: claude --teammate-mode tmux
tmux Backend
When the tmux backend is active:
- Claude Code spawns teammate panes with
tmux split-window -h/-v - Commands sent to panes via
tmux send-keys - Pane IDs tracked via tmux's
#{pane_id}format variables - Works on macOS, Linux, WSL
- Requires
tmuxbinary installed
iTerm2 Backend
When the iTerm2 backend is active:
- Claude Code invokes the
it2Python CLI tool it2connects to iTerm2 via Unix domain socket at~/Library/Application Support/iTerm2/private/socket- Calls
iterm2.Session.async_split_pane()via protobuf/websocket - iTerm2 creates a native split pane
- Commands sent via
iterm2.Session.async_send_text()
The chain:
Claude Code (Node.js)
→ spawns `it2 session split [--vertical]`
→ it2 (Python) connects to iTerm2 Unix socket
→ protobuf: iterm2.Session.async_split_pane()
→ iTerm2 creates native split pane
→ `it2 session send-text "claude ..."` launches teammate
Requirements: pip install it2, iTerm2 Python API enabled (Settings → General → Magic → Enable Python API), macOS only.
Important: iTerm2 has no escape sequences for pane splitting. The OSC 1337 proprietary sequences support cursor shape, clipboard, file transfer, annotations, and profile switching — but not pane management. The Python API via Unix socket is the only programmatic pane control mechanism.
iTerm2 tmux Control Mode (-CC)
There's a third path: running tmux -CC inside iTerm2. In control mode:
- tmux sends structured text protocol messages instead of rendering a TUI
- iTerm2 intercepts these and renders tmux windows as native tabs and tmux panes as native split panes
- No tmux prefix key needed — iTerm2 shortcuts work natively
- Sessions persist across disconnections
When a user runs tmux -CC then launches claude --teammate-mode tmux:
- Claude Code detects
$TMUX→ uses tmux backend - Claude Code runs
tmux split-windowto create teammate panes - iTerm2 intercepts the control mode notification and creates native split panes
- Result: teammates appear in native iTerm2 panes with full scrollback
This is the recommended approach in the official docs.
In-Process Backend (Default Fallback)
No external dependencies. All teammates run as separate processes inside a single terminal:
- Shift+Up/Down: switch between teammate views
- Ctrl+T: toggle task list
- Shift+Tab: toggle delegate mode
- Works in any terminal
The Coordination Layer (Display-Independent)
Critical insight: tmux and iTerm2 are ONLY display layers. All real coordination happens through the filesystem, regardless of which backend is active:
File-Based JSON Mailbox
~/.claude/teams/{team-name}/
config.json # Team metadata, member registry
inboxes/
{agent-name}.json # Per-agent JSON mailbox files
When Agent A messages Agent B:
SendMessagetool appends JSON to~/.claude/teams/{name}/inboxes/{agent-b}.json- Agent B picks up the message on its next polling check between turns
- Messages delivered as new conversation turns, waking idle agents
File-Based Task List
~/.claude/tasks/{team-name}/
{task-id}.json # Individual task files
Each task file contains id, subject, description, status, owner, blockedBy, blocks. Agents poll TaskList, find unowned pending tasks, and atomically claim them via file locking.
No Shared Context
Each agent is a full separate Claude Code process with its own context window. No shared memory, no shared state beyond the filesystem.
Known Issues with Claude Code's Display Backends
tmux Backend Issues
| Issue | Problem |
|---|---|
| #23615 | Splitting current window destroys user's existing tmux layout |
| #23615 | At 4+ agents, tmux send-keys commands become garbled (race conditions) |
| #23950 | Pane indexing assumes 0-based, but tmux allows user-configured pane-base-index |
iTerm2 Backend Issues
| Issue | Problem |
|---|---|
| #23572 | it2 split shortcut failed due to Click parameter naming bug in it2 CLI |
| #23815 | settings.json teammateMode ignored — requires CLI flag workaround |
| #24292 | teammateMode: "tmux" doesn't create iTerm2 panes |
| #24301 | Auto-detection silently falls back to in-process |
| #24385 | iTerm2 panes not closed on teammate shutdown (orphaned panes) |
| #24771 | Split panes created but teammates disconnected from messaging |
| #25772 | iTerm2 native split disabled by in-process fallback in auto mode |
Root Cause
The most pervasive bug: teammateMode is defined in the global config schema but read from the project-level settings, so it always resolves to undefined → "auto" → "in-process". The CLI flag --teammate-mode tmux bypasses this.
Feature Requests for Other Terminals
| Issue | Terminal | Status |
|---|---|---|
| #23574 | WezTerm | Open |
| #24122 | Zellij | Open |
| #24189 | Ghostty | Open |
| #24384 | Windows Terminal | Open |
Split-pane mode is NOT supported in VS Code's integrated terminal, Windows Terminal, or Ghostty.
tmux as Infrastructure: Technical Assessment
Go Libraries for tmux Control
| Library | Approach | Maturity |
|---|---|---|
GianlucaP106/gotmux | Subprocess (tmux CLI wrapper) | Most feature-complete |
jubnzv/go-tmux | Subprocess | Lighter-weight |
wricardo/gomux | Subprocess | Minimal |
All shell out to the tmux binary. None implement control mode or direct socket communication.
tmux IPC Mechanisms
| Mechanism | Direction | Limitation |
|---|---|---|
send-keys | App → Pane | Raw keystrokes only, no structured data, no confirmation |
capture-pane -p | Pane → App | Point-in-time snapshot, not streaming. Loses ANSI structure |
capture-pane -e | Pane → App | Preserves escape sequences but requires parsing |
pipe-pane | Pane → File/Pipe | Raw terminal output including escape sequences |
Control mode %output | Pane → Client | Real-time but octal-escaped, requires decoding |
| Format subscriptions | Server → Client | Max 1 update/second |
Why You Cannot Embed tmux Inside Bubble Tea
tmux panes write directly to the terminal's screen buffer using their own coordinate system. Bubble Tea also manages the entire terminal screen. Two independent programs cannot manage the same screen coordinates.
Approach A: tmux as outer container. Your app runs inside a tmux pane. You lose control of visual presentation.
Approach B: Headless tmux + capture-pane. Run detached tmux sessions, poll with capture-pane -p, render text in Bubble Tea. Polling-based, loses color, high latency.
Approach C: Control mode bridge. Connect via tmux -CC, parse %output notifications, feed through VT emulator, render in Bubble Tea. No existing Go library for this. Massive implementation effort.
None of these are clean.
Native Bubble Tea: Technical Assessment
Layout and Composition
Bubble Tea v2's model composition pattern for multi-pane layout:
type teamDashboard struct {
members map[string]*memberPane
tasks *taskListPane
activity *activityLogPane
focus string
width, height int
}
func (m teamDashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Route messages to focused pane
// Broadcast WindowSizeMsg to all panes
// Handle global keys (tab to switch focus)
}
func (m teamDashboard) View() string {
sidebar := m.renderMemberList()
content := m.members[m.focus].View()
return lipgloss.JoinHorizontal(lipgloss.Top,
sidebarStyle.Width(m.width/3).Render(sidebar),
contentStyle.Width(m.width*2/3).Render(content),
)
}
Lipgloss v2 provides:
JoinHorizontal()/JoinVertical()— string-based compositionPlace()— positioning in whitespaceWidth()/Height()/MaxWidth()/MaxHeight()— dimension constraintsBorder()— per-side borderslipgloss/table,lipgloss/list,lipgloss/tree— structured rendering
Does NOT have flexbox or grid layout natively (open issue #166).
Streaming Multiple Agent Outputs
Three patterns for multiplexing concurrent streams:
Pattern 1: Channel-based injection (recommended)
func listenForAgent(agentID string, ch <-chan AgentOutput) tea.Cmd {
return func() tea.Msg {
output := <-ch // blocks until output available
return agentOutputMsg{id: agentID, output: output}
}
}
func (m model) Init() tea.Cmd {
cmds := make([]tea.Cmd, 0, len(m.agents))
for id, agent := range m.agents {
cmds = append(cmds, listenForAgent(id, agent.outputChan))
}
return tea.Batch(cmds...)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case agentOutputMsg:
m.updatePane(msg.id, msg.output)
return m, listenForAgent(msg.id, m.agents[msg.id].outputChan)
}
}
Zero latency. Each agent goroutine writes to a channel. Bubble Tea commands block on channels and deliver output as messages. Re-subscribe after each message.
Pattern 2: Pub/sub integration (what Crush already does)
Crush's internal/pubsub/ broker already delivers typed events to the TUI. Agent output would be another event type. The TUI subscribes and routes to the correct pane.
Pattern 3: Tick-based polling
func pollAgents() tea.Cmd {
return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg {
return pollMsg{}
})
}
Simple but adds 100ms latency. Avoid.
Existing Multi-Pane Bubble Tea Components
| Library | Description |
|---|---|
john-marinelli/panes | Multi-pane grid using 2D slice of tea.Model. Vim-style focus navigation (Ctrl+H/J/K/L). Only active pane receives Update(). |
76creates/stickers | CSS flexbox-inspired responsive grid with ratio-based scaling. Built on lipgloss. |
Gaurav-Gosain/tuios | Full terminal multiplexer in Bubble Tea v2. 9 workspaces, vendored VT parser, 10K-line scrollback, daemon mode, floating/tiled windows, adaptive refresh (60Hz focused, 30Hz background). |
taigrr/bubbleterm | Headless terminal emulator as a Bubble Tea component. ANSI parsing, 256-color + truecolor, cursor positioning, resize support. For embedding external processes. |
tuios is proof that the entire tmux use case can be replicated natively in Bubble Tea v2.
How Existing Go TUIs Handle This
lazygit (gocui): Distinguishes between "views" (rendering units) and "windows" (logical screen sections). Multiple views can share one window with tab-like switching. Layout recalculated on every event cycle. Focus uses a context stack (side, main, temporary popup, persistent popup).
k9s (custom tview fork): Uses Pages component for stacking views. Navigation stack with push/pop semantics. Log streaming uses ANSIWriter for colored output.
The PTY Multiplexer Alternative
If you ever need to embed external CLI processes as agents (running separate binaries), there's a middle ground between tmux and pure native:
import "github.com/taigrr/bubbleterm"
// Each agent is an embedded terminal emulator
agent1, _ := bubbleterm.NewWithCommand(40, 20, exec.Command("crush", "--session", "agent1"))
agent2, _ := bubbleterm.NewWithCommand(40, 20, exec.Command("crush", "--session", "agent2"))
func (m model) View() string {
return lipgloss.JoinHorizontal(lipgloss.Top,
m.agent1.View(),
m.agent2.View(),
)
}
bubbleterm handles PTY allocation, VT100/VT220 emulation, ANSI parsing, and renders to a Bubble Tea view. No tmux needed.
Lower-level alternatives:
creack/pty— Pure Go PTY interface (open PTYs, start commands, handle resize)hinshun/vt10x— VT100 emulation backend (parse ANSI, maintain virtual screen buffer)micro-editor/terminal— VT emulation from the micro editor
Decision Matrix
| Factor | tmux/iTerm2 | Native Bubble Tea | PTY Multiplexer |
|---|---|---|---|
| Visual control | None (tmux/iTerm2 owns chrome) | Full | Full |
| Real-time streaming | Poor (polling or control mode) | Excellent (channels) | Good (PTY reads) |
| Process isolation | Free | Goroutines (cheap, less isolated) | PTY per agent |
| Platform support | tmux: no Windows. iTerm2: macOS only | Everywhere | Partial Windows |
| External deps | tmux binary / it2 CLI / iTerm2 Python API | None | None |
| Complexity | Low (tmux does layout) | Medium (manual layout) | High (VT emulation) |
| Session recovery | Free (tmux persists) | Must implement (SQLite) | Must implement |
| Input routing | Complex (send-keys translation) | Natural (Bubble Tea msgs) | Medium (PTY write) |
| Memory per agent | Full OS process | ~8KB goroutine | PTY + VT emulator |
| Shared state | Filesystem JSON | Go channels + SQLite | Go channels + SQLite |
| Crash isolation | Full (separate process) | Must use recover() | Per-PTY |
Analysis: Native Bubble Tea vs tmux for Go-Based Multi-Agent
Why Native Wins for Go
The fundamental reason: In a Go-based agent (like Crush), agents would be goroutines, not separate processes. Claude Code uses tmux because it's a Node.js application where each agent needs its own V8 runtime — a full OS process. Go agents share one process with Fantasy's Agent.Stream(), independent sessions, typed channels, and a shared SQLite connection pool. There is nothing to put in a tmux pane.
Specific advantages of native:
-
Crush already has the building blocks. Composed models, pub/sub event streaming, lipgloss layout, viewport components. The TUI architecture in
internal/ui/is designed for component composition. -
Zero-latency streaming. Channel-based message injection gives instant updates. No polling, no
capture-panesubprocess overhead, no octal-escaped control mode parsing. -
Full visual control. A team dashboard would be a Bubble Tea component with native borders, themes, status indicators, and layout — not tmux's chrome.
-
Proven at scale.
tuiosdemonstrates a complete terminal multiplexer (9 workspaces, floating windows, 10K scrollback, adaptive refresh) built entirely in Bubble Tea v2. -
No external dependencies. Works on macOS, Linux, Windows. No
tmuxbinary, noit2CLI, no iTerm2 Python API. -
Cheap agents. ~8KB per goroutine. Shared database pool, shared Catwalk cache, zero-copy pub/sub. Claude Code's filesystem JSON + separate processes is heavyweight by comparison.
-
Claude Code's display layer is buggy. 8+ open issues on the iTerm2 backend alone. Race conditions with
send-keysat scale. Orphaned panes. Silent fallbacks.
The One Exception
If a Go agent ever needs to orchestrate external CLI tools as agents (running claude, gemini, or other binaries), bubbleterm can embed terminal emulators inside the Bubble Tea view. This gives the process isolation of tmux with the visual control of native rendering, no tmux dependency.
Reference Architecture: Native Multi-Agent
Go Process (single binary)
│
├── Coordinator
│ ├── Team Manager
│ │ ├── Agent goroutine 1 (SessionAgent + fantasy.Agent)
│ │ ├── Agent goroutine 2 (SessionAgent + fantasy.Agent)
│ │ └── Agent goroutine N (SessionAgent + fantasy.Agent)
│ ├── Pub/Sub Broker (AgentMessage events)
│ └── Task Service (SQLite-backed)
│
├── TUI (single tea.Program)
│ ├── Team Dashboard (composed model)
│ │ ├── Member List Pane (agent status, current task)
│ │ ├── Focused Agent Chat Pane (streaming output)
│ │ ├── Task List Pane (shared tasks, dependencies)
│ │ └── Activity Log Pane (inter-agent messages)
│ ├── Permission Dialog Queue (concurrent requests)
│ └── Status Bar (team cost, agent count)
│
└── SQLite (shared)
├── sessions (per-agent sessions)
├── messages (per-agent message history)
├── files (per-agent file edit history)
├── team_tasks (shared task list)
└── read_files (per-agent file tracking)
Communication: Go channels + pub/sub (not filesystem JSON). Display: Lipgloss layout + composed Bubble Tea models (not tmux panes). Persistence: SQLite (not JSON files on disk). Process model: Goroutines (not OS processes).
Ecosystem: Other Tools Using tmux for Multi-Agent
For reference, these tools use tmux as infrastructure for managing multiple AI coding agents:
| Tool | Architecture | tmux Role |
|---|---|---|
| Claude Squad | tmux sessions + git worktrees per agent, TUI overlay | Session isolation + visual layout |
| Agent of Empires | Rust, tmux + optional Docker sandboxing | Process management + display |
| TmuxCC | TUI dashboard monitoring AI agents in tmux panes | Display and monitoring |
| NTM | Named tmux manager, text-based IPC via send-keys | Process management + command broadcast |
| Agent Deck | Session manager with status detection | Session management |
| CCManager | Multi-tool session manager | Session management |
| dmux | Git worktrees + Claude Code | Session isolation |
| go-go-golems/multi-agent-tmux | tmux for display, Unix domain sockets for IPC | Display only (real IPC over sockets) |
The common pattern: tmux provides session isolation and visual layout; actual coordination uses separate mechanisms. This confirms that tmux is a display convenience, not a coordination primitive.
Sources
Claude Code Agent Teams
- Official Documentation
- Addy Osmani — Claude Code Swarms
- From Tasks to Swarms
- Multi-Agent Orchestration Gist
- Anthropic — Building a C Compiler with Agent Teams
GitHub Issues
- #23572 — it2 CLI shortcut bug
- #23615 — Agent teams should spawn in new tmux window
- #23815 — iTerm2 split-pane spawns in-process
- #24292 — teammateMode tmux doesn't create panes
- #24301 — iTerm2 native split pane not working with auto
- #24384 — Windows Terminal backend request
- #24385 — iTerm2 panes not closed on shutdown
- #24771 — Split panes disconnected from messaging
iTerm2
- iTerm2 Escape Codes Documentation
- iTerm2 Python API — Session Class
- iTerm2 tmux Integration
- mkusaka/it2 — Python CLI
- tmc/it2 — Go CLI