How Window, Workspace, Pane, and Surface IDs Work in cmux: A Complete Technical Guide

cmux uses a hierarchy of UUID-based identifiers—Window IDs for top-level NSWindows, PaneIDs from Bonsplit for split containers, and Surface IDs (TabID/UUID) for individual terminal or browser panels—to enable precise UI manipulation, session persistence, and remote scripting.

cmux is a terminal multiplexer built on Bonsplit and Ghostty that relies on a sophisticated ID system to manage its UI hierarchy. Understanding how window, workspace, pane, and surface IDs work is essential for developers integrating with cmux's socket API or extending its functionality. According to the manaflow-ai/cmux source code, these identifiers form a complete addressing scheme that allows precise targeting of any UI element across the entire application.

Window IDs: The Top-Level UUIDs

A window in cmux corresponds to a top-level NSWindow that hosts exactly one Workspace. When a new window is created, AppDelegate assigns a fresh UUID and stores it in mainWindowContexts.

Creating and Storing Window IDs

The window ID generation occurs during window initialization. The AppDelegate maintains the mapping between window instances and their identifiers:

// AppDelegate.swift – Window ID retrieval for a TabManager
// https://github.com/manaflow-ai/cmux/blob/main/Sources/AppDelegate.swift#L4415-L4417
func windowId(for tabManager: TabManager) -> UUID? {
    mainWindowContexts.values.first(where: { $0.tabManager === tabManager })?.windowId
}

Bidirectional Window Lookup

cmux supports reverse resolution through AppDelegate.windowId(for window:), which extracts the UUID from an NSWindow instance. This bidirectional mapping enables the v2 socket protocol, UI-automation tests, and the scriptable main windows API (scriptableMainWindow(_:)).

Workspace and Pane IDs: Bonsplit Integration

A workspace is the logical container for panes and panels, maintaining a one-to-one relationship with a window's TabManager. The workspace stores mapping tables that connect pane IDs to surface IDs.

PaneID Structure and Origin

PaneID originates from the Bonsplit library and uniquely identifies a split pane within a workspace. It is a lightweight struct wrapping a UUID that Bonsplit returns from its tree API. When a split is created, Bonsplit generates a new PaneID that cmux uses for all subsequent operations.

Most Workspace methods accept an inPane paneId: PaneID argument for creating surfaces, moving panels, or focusing specific splits:

// Workspace.swift – Moving a surface between panes
// https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift#L8261-L8265
func moveSurface(panelId: UUID, toPane paneId: PaneID, atIndex index: Int? = nil, focus: Bool = true) -> Bool {
    // Implementation handles the surface relocation
}

Resolving Pane UUIDs to Context

When external commands provide a pane UUID (UUID), TerminalController resolves it to the complete context tuple:

// TerminalController.swift – Resolving pane UUIDs
// https://github.com/manaflow-ai/cmux/blob/main/Sources/TerminalController.swift#L3118-L3122
private func v2LocatePane(_ paneUUID: UUID) -> (
    windowId: UUID, tabManager: TabManager, workspace: Workspace, paneId: PaneID
)? {
    // Returns complete context for the specified pane
}

This method is the primary entry point for any external request targeting a specific pane, returning the window ID, tab manager, workspace reference, and the Bonsplit PaneID.

Surface IDs: Terminal and Browser Panels

A surface represents a concrete UI element—whether a terminal, browser, or markdown panel. Internally identified by TabID (which is simply a UUID alias in cmux), surfaces are mapped to their rendering panels through bidirectional dictionaries in the Workspace class.

The Surface-to-Panel Mapping

The Workspace class maintains surfaceIdToPanelId, a dictionary providing two-way lookups between surface identifiers and panel UUIDs:

// Workspace.swift – Surface ID to Panel UUID
// https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift#L5949-L5950
func panelIdFromSurfaceId(_ surfaceId: TabID) -> UUID? {
    surfaceIdToPanelId[surfaceId]
}
// Workspace.swift – Panel UUID to Surface ID
// https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift#L5945-L5946
func surfaceIdFromPanelId(_ panelId: UUID) -> TabID? {
    surfaceIdToPanelId.first { $0.value == panelId }?.key
}

Forward lookup (panelIdFromSurfaceId) enables surface-oriented commands like "focus surface X." Reverse lookup (surfaceIdFromPanelId) allows panels to notify the workspace of state changes, title updates, and remote-drop events.

Practical Surface Operations

When a terminal panel triggers a visual notification (such as a bell flash), it uses the reverse mapping to locate its containing pane:

// Workspace.swift – Pane flash triggered by surface event
// https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift#L53-L60
private func configureTerminalPanel(_ terminalPanel: TerminalPanel) {
    terminalPanel.onRequestWorkspacePaneFlash = { [weak self, weak terminalPanel] reason in
        guard let self, let terminalPanel else { return }
        self.triggerWorkspacePaneFlash(panelId: terminalPanel.id, reason: reason)
    }
}

ID Resolution in Practice: Socket Commands to UI Actions

The complete ID resolution chain enables cmux to handle complex operations like moving a browser surface from one pane to another across different windows.

Locating Context from External Commands

The typical resolution flow begins with a socket command containing a pane or surface UUID:

  1. Input: Socket payload provides a pane UUID
  2. Resolution: TerminalController.v2LocatePane(_:) resolves to (windowId, tabManager, workspace, paneId)
  3. Operation: The workspace performs the requested action using the resolved IDs

Moving Surfaces Between Panes

To relocate a surface, cmux chains the ID lookups:

// Example: Moving a browser surface between panes
let surfaceId: TabID = /* source surface UUID */
let targetPane = PaneID(id: UUID(uuidString: "target-uuid")!)

if let panelId = workspace.panelIdFromSurfaceId(surfaceId) {
    workspace.moveSurface(panelId: panelId, toPane: targetPane, focus: true)
}

This operation requires resolving the surface ID to its panel UUID, then invoking moveSurface with the target PaneID.

Code Examples for ID Manipulation

Resolving Window IDs from Workspaces

Retrieve the window UUID for a given workspace through the AppDelegate singleton:

import AppKit

if let tabMgr = workspace.tabManager,
   let winId = AppDelegate.shared?.windowId(for: tabMgr) {
    print("Current window UUID: \(winId)")
}

Source: AppDelegate.windowId(for:) – Sources/AppDelegate.swift lines 4415-4417

Finding Pane IDs for Specific Surfaces

Locate which pane contains a given surface by chaining the mapping methods:

let surfaceId: UUID = /* surface UUID from remote command */
if let panelId = workspace.panelIdFromSurfaceId(surfaceId),
   let paneId = workspace.paneId(forPanelId: panelId) {
    print("Surface \(surfaceId) lives in pane: \(paneId)")
}

Source: Workspace.paneId(forPanelId:) – Sources/Workspace.swift lines 7963-7965

Handling Socket Command Resolution

Process v2 socket protocol messages that reference panes by UUID:

// Payload from v2 socket command
let paneUUID = UUID(uuidString: payload["pane_uuid"] as! String)!

if let ctx = TerminalController.shared?.v2LocatePane(paneUUID) {
    // ctx contains windowId, tabManager, workspace, and paneId
    print("Pane belongs to window \(ctx.windowId)")
    print("Bonsplit PaneID: \(ctx.paneId)")
}

Source: TerminalController.v2LocatePane – Sources/TerminalController.swift lines 3118-3122

Summary

  • Window IDs are UUIDs stored in AppDelegate.mainWindowContexts, providing top-level window addressing via windowId(for:) methods.
  • Pane IDs (PaneID) originate from Bonsplit and identify split containers within workspaces, resolved through TerminalController.v2LocatePane.
  • Surface IDs (TabID/UUID) represent individual terminal or browser panels, mapped bidirectionally to panel UUIDs via Workspace.surfaceIdToPanelId.
  • Resolution chains allow socket commands to target specific UI elements by converting UUIDs into complete context tuples containing window, workspace, and pane references.
  • Session persistence serializes all three ID types to enable complete state restoration across application restarts.

Frequently Asked Questions

What is the difference between a Pane ID and a Surface ID in cmux?

A Pane ID (PaneID) identifies a split container within the Bonsplit tree structure and manages layout geometry, while a Surface ID (TabID/UUID) identifies the actual content panel (terminal, browser, or markdown view) rendered inside that pane. A single pane can contain multiple surfaces (in tabbed configurations), or a surface can move between panes while retaining its Surface ID.

How does cmux resolve a pane UUID from a socket command to the actual UI element?

cmux uses TerminalController.v2LocatePane(_:) (defined at Sources/TerminalController.swift:3118) to resolve a pane UUID into a complete context tuple containing the windowId, TabManager, Workspace, and PaneID. This method searches across all windows to locate the specific pane and returns the full addressing context needed to perform UI operations.

Can surfaces move between windows while maintaining their IDs?

Yes. Because Surface IDs are UUIDs and Pane IDs are separate Bonsplit identifiers, cmux can move a surface between different panes and windows using Workspace.moveSurface(panelId:toPane:atIndex:focus:). The surface retains its original TabID throughout the move operation, while the workspace updates the internal surfaceIdToPanelId mapping to reflect the new location.

Where are these IDs stored for session persistence?

Session snapshots store all three identifier types in Workspace.swift around lines 3525-3535. The serialization includes the window UUID, pane tree structure with PaneID references, and surface mappings. When cmux restores a session, it reconstructs the surfaceIdToPanelId dictionary and recreates the Bonsplit pane hierarchy with consistent IDs to restore the exact UI state.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →