# Common Issues with Nested Split Layouts in cmux: Focus, Drift, and Ordering Fixes

> Solve common cmux nested split layout issues like focus drift and ordering mismatch. Learn how to fix stale focus, sync dividers, and ensure correct pane order with our expert solutions.

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

---

**Nested split layouts in cmux suffer from stale focus states, divider position drift, and pane ordering mismatches, which are mitigated through multi-turn focus re-assertion, explicit divider synchronization, and recursive tree traversal algorithms.**

cmux (from manaflow-ai/cmux) builds its workspace UI on top of **Bonsplit**, a split-pane library that supports nested splits inside splits. When developers create splits programmatically via `newTerminalSplit` or `newTerminalSurface`, subtle synchronization issues emerge between Bonsplit’s internal model and AppKit’s first-responder chain, requiring defensive coding patterns throughout [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift).

## Stale Focus State and Re-entrancy Issues

When creating nested split layouts in cmux, the most common failure mode is **focus divergence**: Bonsplit’s `splitPane` immediately focuses the newly created pane, but delayed `didSelect` or `didFocus` callbacks can arrive one or two run-loop turns later, causing the AppKit first-responder to diverge from Bonsplit’s internal focus record.

### Suppressing Re-parent Focus During Split Creation

When a pane is split, the old view is about to be re-parented. Without suppression, macOS sends a `becomeFirstResponder` event to the old view, stealing focus from the new pane. The solution in [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) temporarily suppresses focus events during the mutation:

```swift
// Workspace.swift – during split creation
if focus {
    previousHostedView?.suppressReparentFocus()
    focusPanel(newPanel.id, previousHostedView: previousHostedView)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
        previousHostedView?.clearSuppressReparentFocus()
    }
}

```

*Source:* [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) (lines 25-33)

### Preserving Focus After Non-Intent Splits

When `newTerminalSplit` is called with `focus: false`, cmux must keep the previously focused panel active rather than allowing the new split pane to steal focus. The `preserveFocusAfterNonFocusSplit` method schedules a focus-reconcile and a multi-turn re-assertion:

```swift
private func preserveFocusAfterNonFocusSplit(
    preferredPanelId: UUID?,
    splitPanelId: UUID,
    previousHostedView: GhosttySurfaceScrollView?
) {
    guard let preferredPanelId, panels[preferredPanelId] != nil else {
        clearNonFocusSplitFocusReassert()
        scheduleFocusReconcile()
        return
    }

    let generation = beginNonFocusSplitFocusReassert(
        preferredPanelId: preferredPanelId,
        splitPanelId: splitPanelId
    )

    // Re-assert focus over three run-loop turns.
    reassertFocusAfterNonFocusSplit(generation: generation, …)
    DispatchQueue.main.async { … reassert … }
    DispatchQueue.main.async { … reassert … scheduleFocusReconcile(); clear… }
}

```

*Source:* [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) (lines 88-136)

### Re-asserting Focus Over Multiple Run-Loop Turns

The `reassertFocusAfterNonFocusSplit` method checks whether the split pane has mistakenly become the focused pane and flips focus back to the intended panel. This prevents the "highlighted but typing goes to the wrong pane" bug:

```swift
private func reassertFocusAfterNonFocusSplit(
    generation: UInt64,
    preferredPanelId: UUID,
    splitPanelId: UUID,
    previousHostedView: GhosttySurfaceScrollView?,
    allowPreviousHostedView: Bool
) {
    // If the split pane is currently focused, move focus back.
    if focusedPanelId == splitPanelId {
        focusPanel(
            preferredPanelId,
            previousHostedView: allowPreviousHostedView ? previousHostedView : nil
        )
        return
    }

    // Otherwise ensure the terminal’s view is the AppKit first responder.
    if let terminalPanel = terminalPanel(for: preferredPanelId) {
        terminalPanel.hostedView.ensureFocus(for: id, surfaceId: preferredPanelId)
    }
}

```

*Source:* [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) (lines 140-166)

## Divider Position Drift in Deeply Nested Trees

After a series of nested splits, divider positions can become out-of-sync with the visual layout because Bonsplit stores divider positions per split node, and programmatic changes must target exact split IDs.

### Equalizing Split Dividers Programmatically

The `TabManager.equalizeSplits` method walks the Bonsplit tree, obtains the current snapshot via `splitNodes(in:)`, and explicitly sets each divider position to `0.5`:

```swift
let initialSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot())
for (index, split) in initialSplits.enumerated() {
    let target = index.isMultiple(of: 2) ? 0.2 : 0.8
    workspace.bonsplitController.setDividerPosition(target, forSplit: splitId)
}
XCTAssertTrue(manager.equalizeSplits(tabId: workspace.id))

```

*Source:* [`cmuxTests/TabManagerUnitTests.swift`](https://github.com/manaflow-ai/cmux/blob/main/cmuxTests/TabManagerUnitTests.swift) (lines 84-100)

### Resizing Specific Splits by ID

To resize a specific divider without drift, `TabManager.resizeSplit` fetches the current tree snapshot, updates the divider position explicitly, and verifies the change:

```swift
guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first,
      let splitId = UUID(uuidString: split.id) else { … }

workspace.bonsplitController.setDividerPosition(0.5, forSplit: splitId)

XCTAssertTrue(manager.resizeSplit(tabId: workspace.id,
                                   surfaceId: leftPanelId,
                                   direction: .right,
                                   amount: 120))

```

*Source:* [`cmuxTests/TabManagerUnitTests.swift`](https://github.com/manaflow-ai/cmux/blob/main/cmuxTests/TabManagerUnitTests.swift) (lines 124-144)

## Pane Ordering and Visual Layout Synchronization

Bonsplit’s split node stores `first` and `second` children, but a naïve traversal produces a left-to-right order that diverges from the UI after mixed horizontal/vertical splits. The `orderedPaneIds` function in [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) recursively walks the tree and concatenates IDs in visual order:

```swift
static func orderedPaneIds(tree: ExternalTreeNode) -> [String] {
    switch tree {
    case .pane(let pane):
        return [pane.id]
    case .split(let split):
        // Bonsplit split order matches visual order for both horizontal and vertical splits.
        return orderedPaneIds(tree: split.first) + orderedPaneIds(tree: split.second)
    }
}

```

*Source:* [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) (lines 7776-7784)

## Nested Surface Creation Workflow

A **nested surface** (e.g., a browser panel inside a split pane) requires careful coordination between the `Panel` model and Bonsplit’s tab creation. The `newTerminalSurface` method creates the `Panel` first, then calls `bonsplitController.createTab`, and finally forces deterministic selection and focus:

```swift
let newPanel = TerminalPanel(...)
panels[newPanel.id] = newPanel

let newTabId = bonsplitController.createTab(
    title: newPanel.displayTitle,
    icon: newPanel.displayIcon,
    kind: .terminal,
    isDirty: newPanel.isDirty,
    isPinned: false,
    inPane: paneId
)!

surfaceIdToPanelId[newTabId] = newPanel.id

// Force deterministic selection/focus so the surface is ready immediately.
if shouldFocusNewTab {
    bonsplitController.focusPane(paneId)
    bonsplitController.selectTab(newTabId)
    newPanel.focus()
    applyTabSelection(tabId: newTabId, inPane: paneId)
}

```

*Source:* [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) (lines 8050-8070)

## Debugging Under-Flow Events

During rapid split mutations, Bonsplit may emit **under-flow** warnings when its arranged-subview count briefly drops to zero. cmux exposes a debug counter via the socket interface for troubleshooting:

```bash

# Query the under-flow count via the cmux socket REPL

> debug.bonsplit_underflow.count

# Reset after inspection

> debug.bonsplit_underflow.reset

```

The implementation in `TerminalController` handles these commands:

```swift
case "bonsplit_underflow_count":
    return .integer(bonsplitUnderflowCount)
case "reset_bonsplit_underflow_count":
    bonsplitUnderflowCount = 0
    return .ok

```

*Source:* [`Sources/TerminalController.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/TerminalController.swift) (lines 1887-1900)

## Summary

- **Stale focus** is prevented by `suppressReparentFocus`, `preserveFocusAfterNonFocusSplit`, and multi-turn `reassertFocusAfterNonFocusSplit` calls that reconcile AppKit and Bonsplit state.
- **Re-entrant focus calls** are skipped via `shouldSuppressReentrantRefocus` checks in `focusPanel`.
- **Divider drift** is fixed by `TabManager.equalizeSplits` and `resizeSplit`, which explicitly set positions using current Bonsplit tree snapshots.
- **Pane ordering** guarantees are enforced by `orderedPaneIds`, which recursively traverses the split tree in visual order.
- **Nested surfaces** are created by instantiating the `Panel` before calling `bonsplitController.createTab`, then forcing immediate selection and focus.
- **Under-flow diagnostics** are exposed via socket commands for CI and local debugging.

## Frequently Asked Questions

### Why does focus flicker when creating new splits in cmux?

Focus flicker occurs when Bonsplit's delayed `didSelect` callback arrives after the split creation, causing AppKit's first-responder to diverge from the intended pane. cmux mitigates this via `suppressReparentFocus` during the split operation and `reassertFocusAfterNonFocusSplit` across multiple run-loop turns to ensure focus converges on the correct panel.

### How does cmux prevent divider positions from drifting in complex nested layouts?

cmux prevents drift by never assuming divider indices remain stable. The `TabManager` fetches a fresh tree snapshot via `splitNodes(in:)` before calling `setDividerPosition`, ensuring each resize targets the correct Bonsplit node ID rather than a positional index that might shift during mutations.

### What causes the "under-flow" warning in cmux debug logs?

Under-flow warnings appear when Bonsplit's arranged-subview count temporarily drops to zero during rapid split mutations. This is a diagnostic signal used primarily in testing; cmux tracks these events via the `bonsplitUnderflowCount` counter accessible through the socket REPL's `debug.bonsplit_underflow.*` commands.

### How does cmux determine the correct visual order of panels in a nested split tree?

The `orderedPaneIds` function in [`Sources/Workspace.swift`](https://github.com/manaflow-ai/cmux/blob/main/Sources/Workspace.swift) recursively traverses the Bonsplit tree, concatenating `split.first` before `split.second` at each node. This produces a left-to-right, top-to-bottom ordering that matches the visual UI layout regardless of the split orientation (horizontal or vertical).