Skip to main content
Oh My OpenCode provides seamless tmux integration for managing background agent sessions in separate panes. The system uses a state-first architecture with automatic pane lifecycle management.

Architecture

The tmux integration follows a query-decide-execute-update pattern:
1

QUERY

Get actual tmux pane state (source of truth)
2

DECIDE

Pure function determines actions based on state
3

EXECUTE

Execute actions with verification
4

UPDATE

Update internal cache only after tmux confirms success
Source: src/features/tmux-subagent/manager.ts:42
/**
 * State-first Tmux Session Manager
 * 
 * Architecture:
 * 1. QUERY: Get actual tmux pane state (source of truth)
 * 2. DECIDE: Pure function determines actions based on state
 * 3. EXECUTE: Execute actions with verification
 * 4. UPDATE: Update internal cache only after tmux confirms success
 * 
 * The internal `sessions` Map is just a cache for sessionId<->paneId mapping.
 * The REAL source of truth is always queried from tmux.
 */
export class TmuxSessionManager {
  // ...
}

Core Components

TmuxSessionManager

Source: src/features/tmux-subagent/manager.ts:54 Main class managing session-to-pane mapping and lifecycle.
export class TmuxSessionManager {
  // Session tracking
  private sessions = new Map<string, TrackedSession>()
  private pendingSessions = new Set<string>()
  
  // Deferred session queue (when at capacity)
  private deferredSessions = new Map<string, DeferredSession>()
  private deferredQueue: string[] = []

  // Create pane for session
  async handleSessionCreated(event: SessionCreatedEvent): Promise<void>
  
  // Close pane when session ends
  async handleSessionDeleted(sessionId: string): Promise<void>
  
  // Check if tmux is enabled
  private isEnabled(): boolean
}
Configuration:
interface TmuxConfig {
  enabled: boolean
  layout: "vertical" | "horizontal" | "grid"
  main_pane_size: number      // Percentage (e.g., 60)
  main_pane_min_width: number // Characters (default: 100)
  agent_pane_min_width: number // Characters (default: 52)
}

DecisionEngine

Source: src/features/tmux-subagent/decision-engine.ts Pure function evaluating window state to produce spawn/close actions.
export function decideSpawnActions(
  windowState: WindowState,
  sessionMappings: SessionMapping[],
  capacityConfig: CapacityConfig
): SpawnDecision

export function decideCloseAction(
  sessionId: string,
  sessionMappings: SessionMapping[]
): CloseAction | null
SpawnDecision:
type SpawnDecision = 
  | { type: "spawn"; targetPaneId: string }
  | { type: "replace"; targetPaneId: string; victimSessionId: string }
  | { type: "defer"; reason: string }
  | { type: "skip"; reason: string }

ActionExecutor

Source: src/features/tmux-subagent/action-executor.ts Executes spawn/close/replace actions with verification.
export async function executeActions(
  actions: PaneAction[],
  serverUrl: string,
  client: OpencodeClient
): Promise<ExecutionResult[]>

export async function executeAction(
  action: PaneAction,
  serverUrl: string,
  client: OpencodeClient
): Promise<ExecutionResult>
PaneAction types:
type PaneAction =
  | { type: "spawn"; sessionId: string; title: string; targetPaneId: string }
  | { type: "close"; paneId: string }
  | { type: "replace"; sessionId: string; title: string; targetPaneId: string; victimSessionId: string }

GridPlanning

Source: src/features/tmux-subagent/grid-planning.ts Calculates pane layout given window dimensions.
export function calculateCapacity(
  windowState: WindowState,
  capacityConfig: CapacityConfig
): {
  canSplit: boolean
  reason?: string
  availableSpace?: { width: number; height: number }
}
Layout constraints:
  • MIN_PANE_WIDTH: 52 characters
  • MIN_PANE_HEIGHT: 11 lines
  • Main pane preserved (never split below minimum)
  • Agent panes split from remaining space

Layout Options

Vertical Layout (Default)

┌─────────────┬────────┐
│             │ Agent 1│
│    Main     ├────────┤
│    Pane     │ Agent 2│
│             ├────────┤
│             │ Agent 3│
└─────────────┴────────┘
Config:
{
  "tmux": {
    "layout": "vertical",
    "main_pane_size": 60  // 60% width for main pane
  }
}

Horizontal Layout

┌──────────────────────┐
│      Main Pane       │
├──────────────────────┤
│ Agent 1 │ Agent 2    │
└──────────────────────┘
Config:
{
  "tmux": {
    "layout": "horizontal",
    "main_pane_size": 50  // 50% height for main pane
  }
}

Grid Layout

┌─────────┬─────┬─────┐
│         │ Ag1 │ Ag2 │
│  Main   ├─────┼─────┤
│         │ Ag3 │ Ag4 │
└─────────┴─────┴─────┘
Config:
{
  "tmux": {
    "layout": "grid",
    "main_pane_size": 55,
    "agent_pane_min_width": 52
  }
}

Session Lifecycle

Session Created

Source: src/features/tmux-subagent/session-created-handler.ts
1

Event Received

session.created event with sessionId, parentID, title
2

Decision

DecisionEngine evaluates: spawn, replace, defer, or skip
3

Spawn Pane

Split from target pane, start OpenCode client in new pane
4

Track Session

Add to sessions Map with sessionId → paneId mapping
5

Start Polling

PollingManager monitors pane health
// src/features/tmux-subagent/session-created-handler.ts
await tmuxManager.handleSessionCreated({
  type: "session.created",
  properties: {
    info: {
      id: "session-123",
      parentID: "parent-456",
      title: "Task: Build feature"
    }
  }
})

Session Deleted

Source: src/features/tmux-subagent/session-deleted-handler.ts
1

Event Received

session.deleted event with sessionId
2

Find Pane

Lookup paneId from sessions Map
3

Close Pane

Execute tmux kill-pane -t <pane-id>
4

Untrack Session

Remove from sessions Map
5

Stop Polling

PollingManager stops monitoring

Pane Management

Spawn Action

When: New background session, pane capacity available
# Executed command
tmux split-window -t <target-pane-id> -h \
  "cd <working-dir> && opencode-url <server-url> <session-id>"

Replace Action

When: New background session, no pane capacity (at limit)
1

Select Victim

Oldest agent pane (by creation time)
2

Close Victim

Terminate old session’s pane
3

Spawn New

Create pane for new session
Source: src/features/tmux-subagent/oldest-agent-pane.ts

Defer Action

When: Temporary capacity constraint (window too small)
1

Queue Session

Add to deferredQueue (max 20)
2

Start Loop

Poll every 5s for capacity availability
3

Retry Spawn

Attempt spawn when capacity returns
4

TTL Check

Drop from queue after 5 minutes
Constants:
const DEFERRED_SESSION_TTL_MS = 5 * 60 * 1000  // 5 minutes
const MAX_DEFERRED_QUEUE_SIZE = 20

Polling

Source: src/features/tmux-subagent/polling-manager.ts Monitors pane health and auto-cleans dead sessions.
export class TmuxPollingManager {
  private pollingInterval?: ReturnType<typeof setInterval>
  
  startPolling(): void {
    this.pollingInterval = setInterval(
      () => this.pollAllSessions(),
      POLL_INTERVAL_BACKGROUND_MS  // 3000ms
    )
  }
  
  private async pollAllSessions(): Promise<void> {
    // Check each tracked session's pane still exists
    // If pane missing → close session
  }
}
Polling intervals:
const POLL_INTERVAL_BACKGROUND_MS = 3000        // 3s background health check
const SESSION_READY_POLL_INTERVAL_MS = 100      // 100ms session ready wait
const SESSION_READY_TIMEOUT_MS = 10000          // 10s session ready timeout

Configuration

Enable Tmux Integration

{
  "tmux": {
    "enabled": true,
    "layout": "vertical",
    "main_pane_size": 60,
    "main_pane_min_width": 100,
    "agent_pane_min_width": 52
  }
}

Disable Tmux Integration

{
  "tmux": {
    "enabled": false
  }
}
Note: Auto-disables if not inside tmux ($TMUX unset)

Interactive Sessions

Source: src/hooks/interactive-bash-session/ Hook integrates tmux with interactive tools (e.g., vim, htop).
// Detects interactive tool usage
if (toolName === "Bash" && isInteractiveCommand(args.command)) {
  // Spawn in dedicated tmux pane
  await tmuxManager.spawnInteractiveSession(sessionId, command)
}

Source Files

Location: src/features/tmux-subagent/ (27 files, ~3.6k LOC)
  • manager.ts - TmuxSessionManager main class
  • decision-engine.ts - Spawn/close decision logic
  • action-executor.ts - Execute pane actions
  • grid-planning.ts - Layout capacity calculation
  • spawn-action-decider.ts - Decide spawn vs replace vs defer
  • spawn-target-finder.ts - Find best pane to split
  • oldest-agent-pane.ts - LRU victim selection
  • polling-manager.ts - Health polling
  • types.ts - Type definitions
  • pane-state-querier.ts - Query tmux pane state
  • session-status-parser.ts - Parse session status
  • session-ready-waiter.ts - Wait for session ready
  • session-message-count.ts - Track message count
  • pane-split-availability.ts - Check split capacity
  • session-created-handler.ts - Handle session.created
  • session-deleted-handler.ts - Handle session.deleted
  • session-created-event.ts - Event type definitions
  • tmux-grid-constants.ts - MIN_PANE_WIDTH, MIN_PANE_HEIGHT
  • polling-constants.ts - Polling intervals
  • layout-config.test.ts - Layout config tests
  • cleanup.ts - Session cleanup logic
  • event-handlers.ts - Event routing
  • index.ts - Barrel exports
  • action-executor-core.ts - Core action execution
  • Various test files

Background Agent

Background task system that uses tmux panes

Hooks

interactive-bash-session hook for tmux integration

Build docs developers (and LLMs) love