# Understanding the Relationship Between Panes, Surfaces, and Panels in cmux

> Discover the cmux relationship between panes, surfaces, and panels. Understand how these components work together for effective UI management and rendering in this technical guide.

- Repository: [manaflow-ai/cmux](https://github.com/manaflow-ai/cmux)
- Tags: internals
- Published: 2026-03-29

---

**In cmux, a pane is a Bonsplit layout container that holds multiple tabbed surfaces, where each surface is a low-level rendering object wrapped by a high-level Panel model that manages UI behavior like focus and titles.**

The manaflow-ai/cmux terminal multiplexer uses a three-tier architecture to separate layout management from rendering and UI state. Understanding how **panes**, **surfaces**, and **panels** interact is essential for developers extending the codebase or debugging window management issues. This article breaks down each concept using the actual source implementation to show exactly how these components wire together.

## How Pane, Surface, and Panel Definitions Differ

### Pane: The Bonsplit Layout Container

A **pane** is a layout node managed by the `BonsplitController` split-tree. It functions as a container that can hold one or more tabs displayed side-by-side in the UI. The `Workspace` class identifies which pane hosts a specific UI component through the `paneId(forPanelId:)` method, implemented at [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 6390-6398. Panes represent the structural layout of the workspace, independent of the actual content being rendered.

### Surface: The Low-Level Rendering Object

A **surface** is the concrete rendering implementation—such as a Ghostty terminal surface (`ghostty_surface_t`), a `WKWebView` for browser panels, or a Markdown view. Each surface is identified by a **Bonsplit TabID** assigned when the tab is created. The translation from panel UUID to surface identifier happens in `surfaceIdFromPanelId(_:)`, found at [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 5949-5957. Surfaces handle the actual drawing and input capture, but know nothing about SwiftUI state or layout hierarchy.

### Panel: The SwiftUI Model Wrapper

A **panel** is the application-level Swift object that wraps a surface and exposes UI-level functionality. Conforming to the `Panel` protocol (defined in [`Panel.swift`](https://github.com/manaflow-ai/cmux/blob/main/Panel.swift) lines 18-28), concrete implementations like `TerminalPanel` ([`TerminalPanel.swift`](https://github.com/manaflow-ai/cmux/blob/main/TerminalPanel.swift) lines 9-14) and `BrowserPanel` ([`BrowserPanel.swift`](https://github.com/manaflow-ai/cmux/blob/main/BrowserPanel.swift) lines 2310-2312) manage titles, focus handling, flash animations, and other behaviors. Each panel maintains its own UUID and a back-reference to its underlying surface, stored in the `Workspace` private dictionary `panels: [UUID: Panel]`.

## Mapping the Three Layers in Workspace

The `Workspace` class serves as the central registry that binds these three concepts together using two bidirectional dictionaries:

```swift
// panelId → surfaceId (TabID)
private var surfaceIdToPanelId: [TabID: UUID] = [:]

// surfaceId → panel (any concrete Panel)
private var panels: [UUID: Panel] = [:]

```

### From Pane to Surface via Bonsplit

When creating new content, the `Workspace` asks `BonsplitController` to create a tab within a specific pane. The returned `TabID` becomes the surface identifier:

```swift
let tabId = bonsplitController.createTab(inPane: paneId)   // Bonsplit creates a surface
surfaceIdToPanelId[tabId] = panel.id                       // map surface → panel

```

This relationship means a single pane can contain many surfaces (as tabs), retrieved via `bonsplitController.tabs(inPane:)` (referenced at [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 6400-6405).

### From Surface to Panel

To retrieve the Panel instance from a surface ID, the `Workspace` performs a dictionary lookup:

```swift
func panel(forSurfaceId surfaceId: TabID) -> Panel? {
    guard let panelId = surfaceIdToPanelId[surfaceId] else { return nil }
    return panels[panelId]
}

```

This mapping is established during surface creation (e.g., `newTerminalSurface`), where the workspace instantiates both the low-level surface and its corresponding `TerminalPanel`, storing the reference at [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 6130-6137.

### From Panel to Pane

The reverse lookup requires finding which pane's tab array contains the surface ID associated with the panel:

```swift
func paneId(forPanelId panelId: UUID) -> PaneID? {
    guard let tabId = surfaceIdFromPanelId(panelId) else { return nil }
    return bonsplitController.allPaneIds.first { paneId in
        bonsplitController.tabs(inPane: paneId).contains(where: { $0.id == tabId })
    }
}

```

This implementation at [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 6390-6398 demonstrates that panes own surfaces, and surfaces map to panels, creating the indirect pane-to-panel relationship.

## Practical Code Examples

### Creating a Terminal Panel in a Specific Pane

```swift
let paneId = workspace.paneId(forPanelId: existingPanelId)!          // ↔ pane
let terminalPanel = workspace.newTerminalSurface(
    inPane: paneId,
    workingDirectory: "~/projects",
    focus: true
)                       // → creates surface (TabID) + TerminalPanel (panel)

```

This pattern, implemented in [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 535-546, simultaneously creates the Bonsplit tab (surface) and its wrapping `TerminalPanel`.

### Resolving a Panel's Hosting Pane

```swift
if let pane = workspace.paneId(forPanelId: somePanel.id) {
    print("Panel lives in pane \(pane.id)")
}

```

See the lookup implementation at [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 6390-6398 for the traversal logic that searches all pane IDs to find the containing layout node.

### Triggering Panel-Level UI Behavior

```swift
if let panel = workspace.panels[panelId] as? TerminalPanel {
    panel.triggerFlash(reason: .notificationArrival)   // Panel‑level API
}

```

This example from [`TerminalPanel.swift`](https://github.com/manaflow-ai/cmux/blob/main/TerminalPanel.swift) lines 16-30 shows how the Panel abstraction provides UI-specific methods unavailable on the raw surface.

## Summary

- **Panes** are Bonsplit layout containers managed by `BonsplitController` that hold stacks of tabbed surfaces; they represent the split-tree structure of the workspace.
- **Surfaces** are low-level rendering objects (Ghostty surfaces, web views, etc.) identified by `TabID` values, created when tabs are added to panes via `createTab(inPane:)`.
- **Panels** are SwiftUI-level models conforming to the `Panel` protocol that wrap surfaces and provide UI behavior like focus, titles, and flash animations.
- The `Workspace` class maintains bidirectional mappings—`surfaceIdToPanelId` and `panels`—to translate between Bonsplit `TabID`s, panel UUIDs, and pane identifiers.
- UI traversal in [`WorkspaceContentView.swift`](https://github.com/manaflow-ai/cmux/blob/main/WorkspaceContentView.swift) lines 392-405 iterates panes → tabs (surfaces) → panels to render the interface.

## Frequently Asked Questions

### What is the difference between a surface and a panel in cmux?

A **surface** is the low-level rendering object—such as a Ghostty terminal surface, `WKWebView` for browsers, or a Markdown view—identified by a Bonsplit `TabID`. A **panel** is the higher-level Swift object conforming to the `Panel` protocol (defined in [`Panel.swift`](https://github.com/manaflow-ai/cmux/blob/main/Panel.swift) lines 18-28) that wraps the surface and exposes UI-level functionality like title management, focus handling, and flash animations (see [`TerminalPanel.swift`](https://github.com/manaflow-ai/cmux/blob/main/TerminalPanel.swift) lines 16-30).

### How does cmux map a panel to its containing pane?

The `Workspace` class performs a two-step lookup: first it retrieves the surface ID via `surfaceIdFromPanelId(_:)` ([`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 5949-5957), then it searches `bonsplitController.allPaneIds` to find which pane contains that tab ID in its `tabs(inPane:)` array ([`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 6390-6398). This establishes that panes contain surfaces (as tabs), and surfaces map to panels.

### Why does cmux use Bonsplit TabIDs as surface identifiers?

According to the source in [`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift), the `BonsplitController` manages the split-tree layout and creates surfaces as **tabs** within panes. When `newTerminalSurface` or `newBrowserSurface` is called, the returned `TabID` from `bonsplitController.createTab(inPane:)` becomes the canonical identifier for the underlying surface, stored in the `surfaceIdToPanelId` dictionary ([`Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Workspace.swift) lines 535-546). This design unifies the layout tree (Bonsplit) with the rendering layer.

### Can multiple panels share the same surface in cmux?

No, the relationship is strictly one-to-one. The `Workspace` maintains a private dictionary `panels: [UUID: Panel]` where the key is the panel's UUID, and the surface-to-panel mapping `surfaceIdToPanelId: [TabID: UUID]` ensures each surface ID maps to exactly one panel ID. Attempting to create a new panel for an existing surface would overwrite the previous mapping in these dictionaries.