← Back to blog

Crush Multi-Agent Display Design

·Crush
crushmulti-agenttuidesign

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 tuios project 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:

ModeDetectionBackend
"auto" (default)$ITERM_SESSION_ID → iTerm2; $TMUX → tmux; else → in-processAuto-detect
"tmux"Forcedtmux split panes
"iterm2"Forced (auto-detected when $ITERM_SESSION_ID present)Native iTerm2 split panes
"in-process"ForcedAll 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:

  1. Claude Code spawns teammate panes with tmux split-window -h / -v
  2. Commands sent to panes via tmux send-keys
  3. Pane IDs tracked via tmux's #{pane_id} format variables
  4. Works on macOS, Linux, WSL
  5. Requires tmux binary installed

iTerm2 Backend

When the iTerm2 backend is active:

  1. Claude Code invokes the it2 Python CLI tool
  2. it2 connects to iTerm2 via Unix domain socket at ~/Library/Application Support/iTerm2/private/socket
  3. Calls iterm2.Session.async_split_pane() via protobuf/websocket
  4. iTerm2 creates a native split pane
  5. 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:

  1. Claude Code detects $TMUX → uses tmux backend
  2. Claude Code runs tmux split-window to create teammate panes
  3. iTerm2 intercepts the control mode notification and creates native split panes
  4. 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:

  1. SendMessage tool appends JSON to ~/.claude/teams/{name}/inboxes/{agent-b}.json
  2. Agent B picks up the message on its next polling check between turns
  3. 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

IssueProblem
#23615Splitting current window destroys user's existing tmux layout
#23615At 4+ agents, tmux send-keys commands become garbled (race conditions)
#23950Pane indexing assumes 0-based, but tmux allows user-configured pane-base-index

iTerm2 Backend Issues

IssueProblem
#23572it2 split shortcut failed due to Click parameter naming bug in it2 CLI
#23815settings.json teammateMode ignored — requires CLI flag workaround
#24292teammateMode: "tmux" doesn't create iTerm2 panes
#24301Auto-detection silently falls back to in-process
#24385iTerm2 panes not closed on teammate shutdown (orphaned panes)
#24771Split panes created but teammates disconnected from messaging
#25772iTerm2 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

IssueTerminalStatus
#23574WezTermOpen
#24122ZellijOpen
#24189GhosttyOpen
#24384Windows TerminalOpen

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

LibraryApproachMaturity
GianlucaP106/gotmuxSubprocess (tmux CLI wrapper)Most feature-complete
jubnzv/go-tmuxSubprocessLighter-weight
wricardo/gomuxSubprocessMinimal

All shell out to the tmux binary. None implement control mode or direct socket communication.

tmux IPC Mechanisms

MechanismDirectionLimitation
send-keysApp → PaneRaw keystrokes only, no structured data, no confirmation
capture-pane -pPane → AppPoint-in-time snapshot, not streaming. Loses ANSI structure
capture-pane -ePane → AppPreserves escape sequences but requires parsing
pipe-panePane → File/PipeRaw terminal output including escape sequences
Control mode %outputPane → ClientReal-time but octal-escaped, requires decoding
Format subscriptionsServer → ClientMax 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 composition
  • Place() — positioning in whitespace
  • Width() / Height() / MaxWidth() / MaxHeight() — dimension constraints
  • Border() — per-side borders
  • lipgloss/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

LibraryDescription
john-marinelli/panesMulti-pane grid using 2D slice of tea.Model. Vim-style focus navigation (Ctrl+H/J/K/L). Only active pane receives Update().
76creates/stickersCSS flexbox-inspired responsive grid with ratio-based scaling. Built on lipgloss.
Gaurav-Gosain/tuiosFull 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/bubbletermHeadless 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

Factortmux/iTerm2Native Bubble TeaPTY Multiplexer
Visual controlNone (tmux/iTerm2 owns chrome)FullFull
Real-time streamingPoor (polling or control mode)Excellent (channels)Good (PTY reads)
Process isolationFreeGoroutines (cheap, less isolated)PTY per agent
Platform supporttmux: no Windows. iTerm2: macOS onlyEverywherePartial Windows
External depstmux binary / it2 CLI / iTerm2 Python APINoneNone
ComplexityLow (tmux does layout)Medium (manual layout)High (VT emulation)
Session recoveryFree (tmux persists)Must implement (SQLite)Must implement
Input routingComplex (send-keys translation)Natural (Bubble Tea msgs)Medium (PTY write)
Memory per agentFull OS process~8KB goroutinePTY + VT emulator
Shared stateFilesystem JSONGo channels + SQLiteGo channels + SQLite
Crash isolationFull (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:

  1. 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.

  2. Zero-latency streaming. Channel-based message injection gives instant updates. No polling, no capture-pane subprocess overhead, no octal-escaped control mode parsing.

  3. Full visual control. A team dashboard would be a Bubble Tea component with native borders, themes, status indicators, and layout — not tmux's chrome.

  4. Proven at scale. tuios demonstrates a complete terminal multiplexer (9 workspaces, floating windows, 10K scrollback, adaptive refresh) built entirely in Bubble Tea v2.

  5. No external dependencies. Works on macOS, Linux, Windows. No tmux binary, no it2 CLI, no iTerm2 Python API.

  6. 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.

  7. Claude Code's display layer is buggy. 8+ open issues on the iTerm2 backend alone. Race conditions with send-keys at 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:

ToolArchitecturetmux Role
Claude Squadtmux sessions + git worktrees per agent, TUI overlaySession isolation + visual layout
Agent of EmpiresRust, tmux + optional Docker sandboxingProcess management + display
TmuxCCTUI dashboard monitoring AI agents in tmux panesDisplay and monitoring
NTMNamed tmux manager, text-based IPC via send-keysProcess management + command broadcast
Agent DeckSession manager with status detectionSession management
CCManagerMulti-tool session managerSession management
dmuxGit worktrees + Claude CodeSession isolation
go-go-golems/multi-agent-tmuxtmux for display, Unix domain sockets for IPCDisplay 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

GitHub Issues

iTerm2

tmux

Bubble Tea Multi-Pane