Understanding the Relationship Between Panes, Surfaces, and Panels in cmux
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 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 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 lines 18-28), concrete implementations like TerminalPanel (TerminalPanel.swift lines 9-14) and BrowserPanel (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:
// 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:
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 lines 6400-6405).
From Surface to Panel
To retrieve the Panel instance from a surface ID, the Workspace performs a dictionary lookup:
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 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:
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 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
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 lines 535-546, simultaneously creates the Bonsplit tab (surface) and its wrapping TerminalPanel.
Resolving a Panel's Hosting Pane
if let pane = workspace.paneId(forPanelId: somePanel.id) {
print("Panel lives in pane \(pane.id)")
}
See the lookup implementation at 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
if let panel = workspace.panels[panelId] as? TerminalPanel {
panel.triggerFlash(reason: .notificationArrival) // Panel‑level API
}
This example from 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
BonsplitControllerthat 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
TabIDvalues, created when tabs are added to panes viacreateTab(inPane:). - Panels are SwiftUI-level models conforming to the
Panelprotocol that wrap surfaces and provide UI behavior like focus, titles, and flash animations. - The
Workspaceclass maintains bidirectional mappings—surfaceIdToPanelIdandpanels—to translate between BonsplitTabIDs, panel UUIDs, and pane identifiers. - UI traversal in
WorkspaceContentView.swiftlines 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 lines 18-28) that wraps the surface and exposes UI-level functionality like title management, focus handling, and flash animations (see 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 lines 5949-5957), then it searches bonsplitController.allPaneIds to find which pane contains that tab ID in its tabs(inPane:) array (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, 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →