Skip to content

Publish the commits cmux main already pins (fork menu availability + divider drag sessions) - #188

Merged
azooz2003-bit merged 12 commits into
mainfrom
publish-main-pointer-10563e2
Jul 16, 2026
Merged

azooz2003-bit merged 12 commits into
mainfrom
publish-main-pointer-10563e2

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 16, 2026 •

Copy link
Copy Markdown

cmux main has pinned vendor/bonsplit at 10563e2 since manaflow-ai/cmux#8140 merged, but that commit was never pushed here — so every clean cmux checkout fails to build the Mac app (type 'Bool' has no member 'hidden' in Workspace.swift). This publishes the exact stack the pointer references: the tab context-menu fork-availability API (TabContextForkConversationAvailability) and the divider drag-session work. Fast-forward from current main, no rewrites.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Publishes the vendor bonsplit state pinned by cmux main so clean checkouts build, and adds divider drag-session support with safer imposed-extent behavior to stop divider fights and hangs.

  • New Features

    • Divider drag sessions tracked across the tree: BonsplitController.isDividerDragActive and noteDividerDragSession(_:).
    • Delegate hooks: splitTabBarDividerDragDidBegin and splitTabBarDividerDragDidEnd. Final geometry is delivered before drag-end and bypasses the external-update suppression window.
    • Debug logging sink: DebugEventLog.setExternalSink(_:) to route lines to a host writer; fallback keeps a single O_APPEND handle open.
  • Bug Fixes

    • Imposed extents never apply while a drag is active; they apply once when the session ends.
    • After a container resize, a parked imposed divider re-applies once (deferred) against the settled size to avoid recursive layout and keep exact extents.
    • Re-imposing the same extent re-arms one apply; retry APIs refuse during an active drag.
    • Nested imposed splits get a one-time renudge after a parent retry lands.

Written for commit 10563e2. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added callbacks for divider drag start and end events.
    • Added optional routing of debug log messages to an external destination.
  • Bug Fixes
    • Improved divider behavior during and after resizing or dragging.
    • Deferred layout updates during active drags to prevent unwanted divider movement.
    • Improved recovery when divider positions drift after container resizing.
    • Ensured final geometry updates are delivered when dragging ends.
  • Tests
    • Added coverage for drag lifecycle events, deferred updates, resize recovery, and geometry notifications.

ejc3 and others added 12 commits July 13, 2026 08:54
Drag start and end come from the divider's mouse-tracking lifecycle
(mouseDown brackets AppKit's tracking loop), not from which event happens
to be current inside a resize callback — the old inference missed any
drag that paused before release. BonsplitController.isDividerDragActive
and the dragDidBegin/End delegate calls expose the session to hosts.

An imposed split re-applies its extent only on a fresh imposition, or
when the divider moved while the split view's size did not — putting a
drifted divider back where nothing else will. Re-asserting against a
resized container fought AppKit from inside its own layout pass,
recursively; a main-thread sample showed the whole thread in that fight.
When a refused parent apply lands on a later retry, nested imposed
splits re-apply against their settled containers.
…any divider mouseDown

With overlapping sessions (a host-bracketed custom drag alongside the
built-in tracking), ending one must not announce the drag over while the
other still owns the divider. And the session hit test cannot faithfully
reconstruct the effective divider rect AppKit tracks (our expansion
unioned with AppKit's own proposal), so any mouseDown that reaches the
split view — content clicks never do — brackets a session; a session
around a non-drag click is a harmless no-op pair. setImposedFirstExtent
now documents that a stored extent is not re-asserted across container
resizes: the host imposes fresh values for new sizes.
retryImposedFirstExtent re-asserts a stored extent on host recovery
triggers (rebuilds, tab re-shows, reconnects). Fired mid-drag it would
move the divider under the pointer, so it now returns false while any
drag session is active for the tree; the host's drag-end sync imposes
fresh values once the session closes.

Also reconciles the drag-session line with the imposed-first-extent
rework it now sits on: renudgeImposedDescendants uses the renamed
syncDividerNow hook, and reapplyDividerForThicknessChange records the
epoch/outcome/avail memo instead of the removed lastImposedTarget.
On session end the controller notified the delegate first, so drag-end
arrived before the coordinator's final didChangeGeometry — inverting the
documented contract that the settled geometry has already been reported
when drag-end runs. A host that buffers geometry callbacks and commits
at drag-end would commit pre-drag state. The coordinator now leads on
both edges of the session; hosts that suppress geometry callbacks while
the session is live lose nothing, since the model is final at release
and drag-end is the signal to read it.
…tent

A container resize proportionally rescales dividers, so an imposed split
can drift off its extent. The host re-imposes after the resize, but the
fresh plan often computes the same extent as before — per-pane ideals do
not depend on the container — and setImposedFirstExtent deduped that
call by value, bumping no epoch. syncPosition then saw an unchanged
epoch with a changed container and, per the one-writer rule, refused to
re-apply: the divider stayed wedged at the drifted position until some
input happened to change the number. Live signature: a settled pane one
row short of its tmux assignment, persisting through reconfirm.

Every explicit non-nil call now bumps the epoch, matching syncPosition's
documented 'bounded by explicit calls' contract. A repeat while the
divider is already at target is memo-only (no layout pass), so the
re-arm cannot churn.

Regression: testReimposingSameExtentAfterContainerResizeRetargetsExactly
imposes 120 in 400pt, shrinks to 300 (drift to 90), re-imposes the same
120 — red without the fix, green with it.
The public retryImposedFirstExtent already refused during a drag
session, but the internal apply paths did not: a mid-drag
setImposedFirstExtent (or a drift renudge, a descendant renudge, or a
deferred syncDividerNow apply) still ran its main-queue apply during
AppKit's tracking loop and yanked the divider out from under the
pointer, re-marking the split imposed against the drag's own clear.

Gate the apply at its funnel instead of per API. Every imposed apply
runs through the coordinator's syncPosition imposed branch or its
deferred retry, so those two points now consult the tree's live drag
session count and refuse outright: no divider write, no memo or epoch
bookkeeping, no retry budget consumed. The pending extent stays armed.

When the session count crosses back to zero, the internal controller
gives every still-imposed split one deferred apply. There is no epoch
bump, so a split already at its target only refreshes its memos, and
an interrupted retry chain resumes from the same sync with the budget
it had left. Drift renudges with no session active keep working as
before.
The guaranteed final notification funneled through
notifyGeometryChange, whose first line drops everything within ~50ms
of a fromExternal update. A quick flick released inside that window
lost the one notification the delegate contract promises has already
been delivered when dragDidEnd runs.

Deliver it from the session counter's zero crossing in the controller
instead, with an explicit force that bypasses only the suppression
window and only for this path; every other caller keeps the gate. The
zero crossing also covers host-bracketed sessions via
noteDividerDragSession, which never ran the coordinator's drag-end
path at all, and it still fires before splitTabBarDividerDragDidEnd,
so the ordering the contract promises is unchanged. The coordinator's
session-end callback now only clears its drag flag.
…ider

A window resize rescales an imposed split proportionally, moving the
divider off its extent. Since the re-apply condition became
`renudged || (moved && availUnchanged)`, that drift was left for the
host to fix: the theory was that a resized container always gets a
fresh imposition. But a host whose per-pane ideals are
container-independent computes the SAME extent for any container size
and only re-imposes when its own inputs change. If the epoch-bumped
applies from the last imposition all expired against not-yet-final
bounds (retry budget spent while a concurrent window resize was still
committing), the park was permanent — a deterministic fuzz repro held
the divider at the proportional position (plan 1199pt vs view 984pt)
for 50+ seconds until an unrelated input.

Give the parked divider one deferred apply of its own instead of
waiting for the host. Both places that observe the size change — the
didResize callback and syncPosition's imposed branch — record the new
available size and arm a single retry against it. Recording the size
immediately bounds this to one re-arm per size change, the apply runs
a runloop turn later so it never fights AppKit from inside the
resize's own layout pass, and applying cannot resize the split view,
so it cannot re-trigger itself. A mid-drag retry still refuses without
consuming its budget; the session-end renudge resumes the chain. The
contract, now stated in the doc comments: an apply never terminates
off-target without re-arming on the next size change.

The new regression test imposes an extent, resizes the window with no
further imposition and no drag session, and asserts the first pane
returns to the extent; it fails without the fix (divider stays at
192pt instead of 120pt). The existing same-extent re-imposition test
asserted the drift persisted until the host's call — updated to the
new contract.
Both container-resize tests only checked the final divider width, so
they would pass even if AppKit never moved the divider off the imposed
extent. Capture the divider synchronously after setContentSize, before
any runloop turn — the heal is deferred a turn, so the proportional
drift (192pt for the 400 to 640 grow, under 116pt for the shrink) is
observable there — and only then pump and assert the recovery.

The same-extent re-imposition test also lost its teeth: the size-change
re-arm already restores the extent before the re-impose call runs, so
the final assertion could no longer tell whether the call was accepted
or deduped by value. Isolate it by perturbing the divider at constant
container size with a direct AppKit setPosition (the imposition stays
stored and no avail changes, so the size-change re-arm cannot fire),
pumping with no impose call and asserting the divider stays put —
nothing heals it autonomously — then re-imposing the identical extent
and asserting the divider returns. The recovery is attributable to the
explicit call alone.
dlog appended to its log file by opening a FileHandle per line and doing
seekToEnd()+write. When another writer in the same process appends to the
same file, the two race: lines interleave, clobber each other, and land
out of timestamp order.

A host app can now install an external sink that receives every line, so
all appends can go through one serialized writer. Without a sink, appends
keep a single O_APPEND handle open instead of reopening per line, and
dump() steps aside entirely when a sink owns the file.
@azooz2003-bit
azooz2003-bit merged commit d27b4c6 into main Jul 16, 2026
3 of 4 checks passed
@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54d5a23f-8098-43bd-a50a-1400ea392e9f

📥 Commits

Reviewing files that changed from the base of the PR and between 50a03f9 and 10563e2.

📒 Files selected for processing (6)
  • Sources/Bonsplit/Internal/Controllers/SplitViewController.swift
  • Sources/Bonsplit/Internal/Views/SplitContainerView.swift
  • Sources/Bonsplit/Public/BonsplitController.swift
  • Sources/Bonsplit/Public/BonsplitDelegate.swift
  • Sources/Bonsplit/Public/DebugEventLog.swift
  • Tests/BonsplitTests/BonsplitTests.swift

📝 Walkthrough

Walkthrough

Divider drag lifecycle callbacks now coordinate imposed divider synchronization across nested splits, resize retries, and final geometry notifications. Debug logging can target an external sink while retaining file-based fallback behavior.

Changes

Divider synchronization

Layer / File(s) Summary
Drag lifecycle contract and wiring
Sources/Bonsplit/Public/BonsplitDelegate.swift, Sources/Bonsplit/Internal/..., Sources/Bonsplit/Public/BonsplitController.swift
Divider tracking reports begin/end events, updates active-session state, notifies delegates, and forces geometry delivery when dragging ends.
Imposed extent gating and retry engine
Sources/Bonsplit/Internal/Views/SplitContainerView.swift, Sources/Bonsplit/Public/BonsplitController.swift
Imposed applications pause during tree-wide drags, track available size for retries, reapply nested descendants after settling, and accept repeated identical extents.
Divider behavior regression coverage
Tests/BonsplitTests/BonsplitTests.swift
Tests cover re-imposition, drag-time deferral, resize recovery, and drag-end geometry notifications.

Debug log sink

Layer / File(s) Summary
External log routing and file persistence
Sources/Bonsplit/Public/DebugEventLog.swift
DebugEventLog supports an external sink, reusable file appending with reopen-on-failure, and sink-aware dumping.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ThemedSplitView
  participant SplitViewController
  participant SplitContainerView
  participant BonsplitController
  User->>ThemedSplitView: drag divider
  ThemedSplitView->>SplitViewController: begin/end drag session
  SplitViewController->>SplitContainerView: update tree drag state
  SplitContainerView->>SplitContainerView: defer imposed sync and schedule retries
  SplitViewController->>BonsplitController: notify drag lifecycle
  BonsplitController->>BonsplitController: force final geometry notification
Loading

Possibly related PRs

Suggested reviewers: lawrencecchen

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch publish-main-pointer-10563e2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR publishes the bonsplit vendor state that cmux main has been pinned to since PR #8140, unblocking clean cmux checkouts that previously failed to build. It adds divider drag-session tracking (reference-counted, bracketed by ThemedSplitView.mouseDown/defer) and overhauled imposed-extent logic that gates all applies during a drag and introduces a lastImposedAvail mechanism to distinguish same-size drift (fix synchronously) from post-resize drift (defer one turn to avoid recursive layout fights).

  • Drag session API: BonsplitController.isDividerDragActive, noteDividerDragSession, and two new delegate callbacks (splitTabBarDividerDragDidBegin/End) with a force-bypass on notifyGeometryChange that guarantees the final geometry is delivered before dragDidEnd even inside a fromExternal suppression window.
  • Imposed-extent hardening: Identical setImposedFirstExtent calls now re-arm one apply (no longer idempotent by value); retryImposedFirstExtent refuses during active drags; a new renudgeImposedDescendants path nudges nested imposed splits once a parent retry lands.
  • DebugEventLog: Persistent O_APPEND handle replaces per-line open/seek/close, and an externalSink hook lets a host serialize all writes through its own appender.

Confidence Score: 4/5

Safe to merge; the drag-session and imposed-extent changes are well-tested and the new public API is additive with backward-compatible default delegate implementations.

The ordering of onDividerDragSessionChange before the session-end syncDividerNow loop means a host reading layoutSnapshot() inside splitTabBarDividerDragDidEnd sees sibling imposed splits at their proportionally-drifted positions rather than their re-imposed ones. The rest of the machinery — drag gating, lastImposedAvail resize tracking, renudgeImposedDescendants, the DebugEventLog sink — looks correct.

SplitViewController.swift (notification vs renudge ordering) and DebugEventLog.swift (ring buffer gap when sink is installed then cleared).

Important Files Changed

Filename Overview
Sources/Bonsplit/Internal/Controllers/SplitViewController.swift Adds activeDividerDragSessions reference counter and noteDividerDragSession with zero-crossing notifications; ordering of onDividerDragSessionChange vs syncDividerNow means dragDidEnd fires before sibling imposed splits are re-synced.
Sources/Bonsplit/Internal/Views/SplitContainerView.swift Adds ThemedSplitView.mouseDown override to bracket drag sessions, isTreeDragSessionActive gate in imposed-extent logic, and lastImposedAvail tracking replacing lastImposedTarget; onDividerDragSession is only wired in makeNSView (pre-existing concern noted in previous thread).
Sources/Bonsplit/Public/BonsplitController.swift Exposes isDividerDragActive, noteDividerDragSession, adds force bypass to notifyGeometryChange, guards retryImposedFirstExtent during active drags, and makes identical setImposedFirstExtent re-arm one apply; changes are well-reasoned and tested.
Sources/Bonsplit/Public/BonsplitDelegate.swift Adds splitTabBarDividerDragDidBegin and splitTabBarDividerDragDidEnd protocol requirements with empty default implementations; clean additive API change.
Sources/Bonsplit/Public/DebugEventLog.swift Adds externalSink routing and replaces per-call FileHandle open/seek/close with a persistent O_APPEND handle; when the sink is active the in-memory entries buffer stops updating, creating a silent gap if the sink is later cleared.
Tests/BonsplitTests/BonsplitTests.swift Adds four well-structured tests covering drag deferral, drag-end geometry ordering, container-resize re-arm, and the identical-extent re-imposition edge; each test isolates its trigger carefully.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant AppKit as AppKit (NSSplitView)
    participant TSV as ThemedSplitView
    participant Coord as Coordinator
    participant SVC as SplitViewController
    participant BC as BonsplitController
    participant Delegate as Host Delegate

    Note over AppKit,Delegate: Drag Begin
    AppKit->>TSV: mouseDown(event)
    TSV->>Coord: dividerDragSessionChanged(true)
    TSV->>SVC: noteDividerDragSession(true)
    SVC->>BC: onDividerDragSessionChange(true)
    BC->>Delegate: splitTabBarDividerDragDidBegin

    Note over AppKit,Delegate: During Drag
    AppKit->>Coord: splitViewDidResizeSubviews
    Note over Coord: imposed applies refused (isTreeDragSessionActive)

    Note over AppKit,Delegate: Drag End
    AppKit-->>TSV: super.mouseDown returns (defer)
    TSV->>Coord: dividerDragSessionChanged(false)
    TSV->>SVC: noteDividerDragSession(false)
    Note over SVC: zero crossing
    SVC->>BC: onDividerDragSessionChange(false)
    BC->>BC: notifyGeometryChange(force: true)
    BC->>Delegate: didChangeGeometry
    BC->>Delegate: splitTabBarDividerDragDidEnd
    Note over SVC: syncDividerNow for imposed splits (AFTER dragDidEnd)
    SVC->>Coord: syncDividerNow
    Coord->>AppKit: setPositionSafely
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant AppKit as AppKit (NSSplitView)
    participant TSV as ThemedSplitView
    participant Coord as Coordinator
    participant SVC as SplitViewController
    participant BC as BonsplitController
    participant Delegate as Host Delegate

    Note over AppKit,Delegate: Drag Begin
    AppKit->>TSV: mouseDown(event)
    TSV->>Coord: dividerDragSessionChanged(true)
    TSV->>SVC: noteDividerDragSession(true)
    SVC->>BC: onDividerDragSessionChange(true)
    BC->>Delegate: splitTabBarDividerDragDidBegin

    Note over AppKit,Delegate: During Drag
    AppKit->>Coord: splitViewDidResizeSubviews
    Note over Coord: imposed applies refused (isTreeDragSessionActive)

    Note over AppKit,Delegate: Drag End
    AppKit-->>TSV: super.mouseDown returns (defer)
    TSV->>Coord: dividerDragSessionChanged(false)
    TSV->>SVC: noteDividerDragSession(false)
    Note over SVC: zero crossing
    SVC->>BC: onDividerDragSessionChange(false)
    BC->>BC: notifyGeometryChange(force: true)
    BC->>Delegate: didChangeGeometry
    BC->>Delegate: splitTabBarDividerDragDidEnd
    Note over SVC: syncDividerNow for imposed splits (AFTER dragDidEnd)
    SVC->>Coord: syncDividerNow
    Coord->>AppKit: setPositionSafely
Loading

Reviews (1): Last reviewed commit: "Merge fork menu state with divider drag ..." | Re-trigger Greptile

Comment on lines +513 to +518
// Re-install alongside the identity stamps above so a reused
// coordinator keeps answering for the tree it currently renders.
let internalController = controller
context.coordinator.isTreeDragSessionActive = { [weak internalController] in
(internalController?.activeDividerDragSessions ?? 0) > 0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isTreeDragSessionActive is re-installed in updateNSView so a reused coordinator always references the live controller, but splitView.onDividerDragSession (which calls both coordinator?.dividerDragSessionChanged and internalController?.noteDividerDragSession) is only wired in makeNSView. If SwiftUI ever re-uses the split view for a different controller instance — the same scenario the defensive comment here acknowledges as a concern — drag-session begin/end would still forward to the old internalController, while isTreeDragSessionActive now answers for the new one. The counter and the gate would be mismatched: a drag could be invisible to isTreeDragSessionActive even though the coordinator correctly sets isDragging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants