How cmux Handles Split Pane Geometry and Resizing in macOS

cmux prevents AppKit re-entrancy crashes by deferring split pane geometry updates through a layout-follow-up system that uses UInt64 revision counters to synchronize Bonsplit's logical tree with Ghostty rendering surfaces.

cmux is a Swift-based terminal multiplexer that coordinates complex UI state between the Bonsplit layout engine, AppKit window frames, and Ghostty terminal surfaces. When users create or resize split panes, the application must keep three distinct geometry layers in perfect sync without triggering AppKit's layout re-entrancy limits or causing visual flashes in terminal and browser panels.

The Three-Layer Geometry Stack

Every split operation in cmux spans three distinct architectural layers that must remain consistent:

  • Bonsplit logical layout – The tree of panes and tabs maintained by the BonsplitController submodule
  • AppKit window geometry – The actual NSView frames hosting terminals or browsers in TerminalWindowPortal, BrowserPanelView, and GhosttyTerminalView
  • Ghostty surface geometry – The low-level rendering surface that draws terminal content or web views

According to the manaflow-ai/cmux source code, cmux coordinates these layers through a layout-follow-up system that watches for geometry changes, records revision counters, and schedules deferred layout passes to avoid crashes.

The Layout Follow-Up Architecture

The core mechanism preventing layout re-entrancy centers on asynchronous coordination rather than immediate updates.

Tracking Geometry with Revision Counters

Each host view maintains a geometryRevision counter to detect actual frame changes. In Sources/Panels/BrowserPanelView.swift and Sources/GhosttyTerminalView.swift, the view increments this UInt64 property every time its layout updates:

private(set) var geometryRevision: UInt64 = 0

override func viewDidLayout() {
    super.viewDidLayout()
    geometryRevision &+= 1          // bump revision on any frame change
}

This pattern ensures that cmux can identify exactly which panels have moved, avoiding unnecessary surface reattachments that would cause visual flashes or performance overhead.

Detecting Stale Geometry

When Bonsplit notifies cmux that geometry changed via splitTabBar(_:didChangeGeometry:) in Sources/Workspace.swift (lines 9028-9034), the system sets layoutFollowUpNeedsGeometryPass = true. However, because this callback occurs inside SwiftUI's .onChange(of: geometry)—still within an active layout pass—cmux cannot safely mutate view hierarchies immediately.

The Deferred Update Loop

To prevent NSGenericException crashes from exceeding AppKit's per-window layout-pass limit, scheduleLayoutFollowUpAttempt() defers work using DispatchQueue.main.asyncAfter(0):

private func scheduleLayoutFollowUpAttempt() {
    guard layoutFollowUpTimeoutWorkItem != nil,
          !layoutFollowUpAttemptScheduled else { return }

    layoutFollowUpAttemptScheduled = true
    let delay = layoutFollowUpBackoffDelay()
    let version = layoutFollowUpAttemptVersion

    DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
        guard let self,
              self.layoutFollowUpAttemptVersion == version else { return }
        self.layoutFollowUpAttemptScheduled = false
        self.attemptEventDrivenLayoutFollowUp()
    }
}

The layoutFollowUpAttemptVersion token acts as a cancellation mechanism, aborting stale attempts scheduled before newer geometry changes arrive.

Step-by-Step Implementation Flow

Creating a Split Pane

When users initiate a split, Workspace.newTerminalSplit in Sources/Workspace.swift (lines 28-42) first constructs the panel, registers a Bonsplit tab, and then requests the layout mutation:

let newPanel = TerminalPanel(
    workspaceId: id,
    context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
    configTemplate: inheritedConfig,
    workingDirectory: splitWorkingDirectory,
    portOrdinal: portOrdinal,
    initialCommand: remoteTerminalStartupCommand
)
configureTerminalPanel(newPanel)
panels[newPanel.id] = newPanel

let newTab = Bonsplit.Tab(
    title: newPanel.displayTitle,
    icon: newPanel.displayIcon,
    kind: .terminal,
    isDirty: newPanel.isDirty,
    isPinned: false
)
surfaceIdToPanelId[newTab.id] = newPanel.id

isProgrammaticSplit = true
defer { isProgrammaticSplit = false }
guard bonsplitController.splitPane(paneId,
                                   orientation: orientation,
                                   withTab: newTab,
                                   insertFirst: insertFirst) != nil else { return }

The split insertion occurs before Bonsplit mutates its internal layout, ensuring the UI never flashes an empty panel.

Synchronizing Across Layers

During attemptEventDrivenLayoutFollowUp(), cmux calls flushWorkspaceWindowLayouts() to force a layout pass, then walks every panel to compare revisions. In Sources/Panels/BrowserPanelView.swift and similar terminal views, the coordinator updates its tracked revision only when necessary:

if coordinator.lastSynchronizedHostGeometryRevision != host.geometryRevision {
    coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
    // Re‑attach web view or force a terminal redraw here
}

This check prevents redundant surface reattachments while guaranteeing that Ghostty surfaces match their host view frames exactly.

Handling Rapid Resize Events

During divider drags, geometry can churn multiple times per frame. The layoutFollowUpBackoffDelay() method in Sources/Workspace.swift (lines 44-48) implements an exponential backoff that prevents flooding the main queue with async attempts. If a follow-up timeout exceeds 2 seconds without progress, clearLayoutFollowUp() removes observers and resets state to prevent resource leaks.

Practical Usage Example

Programmatically create a split and let cmux handle the geometry synchronization automatically:

if let leftPaneId = workspace.activePaneId,
   let newPanel = workspace.newTerminalSplit(
       from: leftPaneId,
       orientation: .horizontal,
       insertFirst: false,
       focus: true) {
    
    // The split is created, Bonsplit notified, and layout follow‑up scheduled.
    // No further code is needed – cmux will asynchronously flush the layout
    // and synchronize the terminal surface when the host view’s frame changes.
    print("Created new split with panel id \(newPanel.id)")
}

Behind the scenes, this triggers the geometry-revision workflow, deferred layout pass, and eventual surface synchronization without requiring manual intervention.

Summary

  • Three-layer synchronization: cmux maintains consistency between Bonsplit's logical tree, AppKit NSView frames, and Ghostty rendering surfaces
  • Revision counters: Each host view tracks frame changes via a UInt64 geometryRevision property that increments in viewDidLayout()
  • Re-entrancy protection: The scheduleLayoutFollowUpAttempt() method uses DispatchQueue.main.asyncAfter(0) to break layout cycles and prevent NSGenericException crashes
  • Backoff handling: Rapid resize events are smoothed via layoutFollowUpBackoffDelay() to prevent main queue flooding while ensuring eventual consistency
  • Key files: Primary implementation resides in Sources/Workspace.swift, Sources/Panels/BrowserPanelView.swift, and Sources/GhosttyTerminalView.swift

Frequently Asked Questions

How does cmux prevent crashes during split pane resizing?

cmux avoids NSGenericException crashes by never calling displayIfNeeded() directly inside Bonsplit geometry callbacks. Instead, the scheduleLayoutFollowUpAttempt() method defers layout work to the next run loop iteration using DispatchQueue.main.asyncAfter(0), breaking the re-entrancy chain that would otherwise exceed AppKit's per-window layout-pass limit.

What is the purpose of the geometryRevision counter?

The geometryRevision counter provides stale-view protection by tracking exactly when host view frames change. Each BrowserPanelView and GhosttyTerminalView increments this UInt64 property in viewDidLayout(), allowing the coordinator to compare against lastSynchronizedHostGeometryRevision and update only panels that actually moved, eliminating unnecessary surface reattachments.

How does cmux handle rapid resize events during divider drags?

The layoutFollowUpBackoffDelay() method implements an exponential backoff strategy. When geometry churns rapidly during a divider drag, this function increases the delay between async layout attempts, preventing the main queue from flooding while still guaranteeing that the final geometry state eventually synchronizes across all three architectural layers.

Which source files manage split geometry synchronization?

According to the manaflow-ai/cmux source code, Sources/Workspace.swift lines 28-46 and 9028-9034 orchestrate the layout-follow-up system and Bonsplit delegate callbacks, while Sources/Panels/BrowserPanelView.swift and Sources/GhosttyTerminalView.swift implement the geometryRevision tracking and surface reattachment logic for browser and terminal panels respectively.

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 →