Skip to content

Fix pane tab width regression after #4290 - #4438

Merged
austinywang merged 2 commits into
mainfrom
issue-4433-pane-tab-width
May 20, 2026
Merged

austinywang merged 2 commits into
mainfrom
issue-4433-pane-tab-width

Conversation

@austinywang

@austinywang austinywang commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4433.\n\n## Summary\n- Adds a cmux regression test that renders a short pane tab and asserts compact visible chrome stays below the drag-hit minimum.\n- Updates Bonsplit to manaflow-ai/bonsplit#127, restoring compact visual pane-tab width while preserving #4290's minimal-mode drag routing through expanded hit-test rects.\n- Keeps empty tab-strip chrome available for explicit window dragging outside the expanded tab hit area.\n\n## Regression timing\n- #4290 merged at 2026-05-20 00:36 UTC and introduced the sizing side effect while fixing minimal-mode pane-tab drag routing.\n- This PR preserves the drag routing fix and moves the affordance back into hit testing instead of visual chrome width.\n\n## Testing\n- Not run locally: per repo/user instructions, I did not run reload.sh, bare xcodebuild, or local tests.\n- Bonsplit PR #127 remote CI: tests passed.\n- cmux CI is the validation gate for this branch.\n\n## Build handoff\n- HQ should run the tagged dev build after CI is green; I will report the exact command then.


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Note

Low Risk
Adds a rendering-based XCTest that relies on SwiftUI/AppKit snapshot-style pixel scanning, which could be mildly flaky in CI but does not affect production code paths.

Overview
Adds a new regression test in PortalTabDragRoutingTests that renders a minimal/short pane tab via TabBarView inside an NSHostingView and asserts the selected-tab indicator’s visible width stays compact (<= 80pt) to prevent tab chrome from expanding to satisfy drag hit targets.

The test measures width by caching the rendered view into a bitmap and scanning for high-saturation pixels, waiting briefly for SwiftUI layout/rendering to settle before asserting bounds.

Reviewed by Cursor Bugbot for commit f037070. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Restores compact pane tab width while keeping minimal‑mode drag routing intact. Adds a render-and-measure regression test to ensure the tab’s visible chrome stays narrow and drag comes from hit‑testing. Fixes #4433.

  • Bug Fixes
    • Updated bonsplit to restore compact tab chrome and preserve drag routing via expanded hit‑test rects; keeps empty tab‑strip area for window dragging.
    • Added SwiftUI/AppKit regression test that hosts TabBarView and measures the selected‑tab indicator (<= 80pt) via bitmap sampling to pin compact chrome.

Written for commit f037070. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Tests

    • Added an automated UI test that renders pane tabs and verifies the selected tab indicator width stays within expected bounds to ensure reliable hit-target sizing.
  • Chores

    • Updated a vendored component to a newer revision.

Review Change Stack

@vercel

vercel Bot commented May 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled May 20, 2026 10:10am
cmux-staging Building Building Preview, Comment May 20, 2026 10:10am

@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds a pixel-measurement test that renders TabBarView to verify the selected pane tab indicator is drawn and remains ≤80 pixels; it includes bitmap-sampling helpers and advances the vendored bonsplit submodule.

Changes

Tab Indicator Width Test and Bonsplit

Layer / File(s) Summary
Test method and rendering measurement setup
cmuxTests/PortalTabDragRoutingTests.swift
SwiftUI import added. New testCompactPaneTabChromeStaysBelowDragHitMinimum() renders a configured BonsplitController/TabBarView in a temporary NSWindow/NSHostingView and measures the indicator width via helper functions.
Pixel-level saturation detection
cmuxTests/PortalTabDragRoutingTests.swift
highSaturationWidth(in:sampleRect:) captures the view bitmap, maps the sample rect to pixels, scans columns for high-contrast/high-saturation pixels, counts active columns, and converts that to a rendered width.
Vendored bonsplit submodule update
vendor/bonsplit
Submodule pointer advanced to a new commit hash a80d5af... to align TabBarView behavior with the new test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Poem

🐰 A pixel-perfect hop today,
Rendering tabs in gentle play,
Width kept small, the chrome holds tight,
Pixels counted, measured right,
Bonsplit springs along—what a sight!

🚥 Pre-merge checks | ✅ 16 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title directly relates to the main change—fixing a pane tab width regression introduced in PR #4290—and clearly summarizes the primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR adds test file (exempt), SwiftUI UI types, and utility code. All fall within allowed actor isolation cases: tests, UI-bound types, and small structs in debug code.
Cmux Swift Blocking Runtime ✅ Passed Test-only scaffolding in cmuxTests/PortalTabDragRoutingTests.swift uses RunLoop.current.run() for deterministic UI rendering, which is explicitly allowed. No blocking patterns in production code.
Cmux No Hacky Sleeps ✅ Passed All changes are Swift code (test file + submodule). The rule scope explicitly excludes Swift, covering only TypeScript, JavaScript, shell, and non-Swift runtime scripts.
Cmux Swift Concurrency ✅ Passed Test file avoids DispatchQueue, Combine, and completion handlers. RunLoop in test helper for rendering sync is explicitly allowed by modernization rules.
Cmux Swift @Concurrent ✅ Passed All Swift code additions are synchronous functions on @MainActor with no async/await, @concurrent, or isolation violations.
Cmux Swift File And Package Boundaries ✅ Passed New test file cmuxTests/PortalTabDragRoutingTests.swift (485 lines) is a cohesive XCTest fixture. No production Swift files modified. Allowed under rules' test fixture exception.
Cmux Swift Logging ✅ Passed Test-only PR with zero logging violations; cmuxTests code is exempted per swift-logging.md; submodule update contains no code.
Cmux User-Facing Error Privacy ✅ Passed PR adds test file in cmuxTests/ (explicitly excluded) and submodule update. No user-facing error messages with forbidden content found.
Cmux Full Internationalization ✅ Passed Test file exempt from internationalization checks. Production code uses String(localized:) with matching catalog entries. All supported locales have complete translations in xcstrings files.
Cmux Swiftui State Layout ✅ Passed Test renders SwiftUI via NSHostingView (AppKit bridge). No new @Published/@ObservableObject/@StateObject, no problematic patterns. Complies with swiftui-state-layout.md rules.
Cmux Architecture Rethink ✅ Passed PR adds test-only code in cmuxTests/ with polling/RunLoop for test synchronization, which is explicitly allowed. No production code violates architectural rules.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed NSWindow creations in PortalTabDragRoutingTests.swift are test-only fixtures with defer { window.orderOut(nil) } cleanup, no cmux.* identifiers, and no registrations—explicitly allowed by the rule.
Description check ✅ Passed The pull request description covers all required sections: Summary (what changed and why), Testing (how it was tested), and relevant context about regression timing and build handoff.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 issue-4433-pane-tab-width

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 and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bee7f46. Configure here.

Comment thread cmuxTests/PortalTabDragRoutingTests.swift
@greptile-apps

greptile-apps Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a visual regression from #4290 by bumping the vendor/bonsplit submodule to restore compact pane-tab chrome width while keeping the expanded hit-test rects that #4290 introduced for minimal-mode drag routing. A new rendering-based regression test is added that hosts TabBarView in a live NSHostingView, samples a bitmap strip for high-saturation pixels, and asserts the visible indicator width stays between 40 pt and 80 pt.

  • Submodule update (vendor/bonsplit): advances the pointer to a80d5af7, separating visual chrome width from hit-test rect size so both concerns are handled independently.
  • New regression test (testCompactPaneTabChromeStaysBelowDragHitMinimum): renders TabBarView off a real NSWindow, polls up to 1 s for SwiftUI layout, then measures indicator width via a saturation heuristic; correctly guards against a vacuous zero result with XCTAssertGreaterThan(measuredWidth, 40).
  • Two-commit structure: the test commit (c8208301) precedes the fix commit (f0370709), satisfying the repo's regression-test commit policy.

Confidence Score: 4/5

Safe to merge once the sampleRect sampling edge is confirmed correct for the flipped NSHostingView coordinate system.

No production Swift is modified — only a test file and a submodule pointer. The one open question is whether sampleRect at y=0, height=4 samples the correct edge of the flipped NSHostingView: if TabBarView places the indicator at the bottom of the view, the current sampleRect will always miss it and the test will fail spuriously rather than catching real regressions. Everything else — the nil-guard, the polling loop, the two-commit structure — looks correct.

cmuxTests/PortalTabDragRoutingTests.swift — verify the sampleRect y-origin matches the actual rendering position of the selected-tab indicator inside TabBarView.

Important Files Changed

Filename Overview
cmuxTests/PortalTabDragRoutingTests.swift Adds testCompactPaneTabChromeStaysBelowDragHitMinimum with a bitmap-sampling rendering harness; test structure and guard logic are sound, with the two-commit policy followed correctly.
vendor/bonsplit Submodule pointer advanced to a80d5af; restores compact visual tab-chrome width while preserving drag hit-test rects added in #4290.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[testCompactPaneTabChromeStaysBelowDragHitMinimum] --> B[renderedSelectedPaneTabIndicatorWidth]
    B --> C[Create BonsplitController + pane + TabItem]
    C --> D[NSHostingView wrapping TabBarView]
    D --> E[NSWindow.makeKeyAndOrderFront]
    E --> F[waitForHighSaturationWidth loop\nup to 1s, 10ms ticks]
    F --> G{highSaturationWidth\nfinds saturated pixels?}
    G -- yes --> H[Return CGFloat width]
    G -- no, timeout --> I[Return nil]
    H --> J[XCTAssertGreaterThan width > 40\nproves indicator rendered]
    J --> K[XCTAssertLessThanOrEqual width <= 80\npins compact chrome]
    I --> L[XCTUnwrap throws - test fails]
Loading

Reviews (2): Last reviewed commit: "fix: restore compact pane tab width" | Re-trigger Greptile

Comment thread cmuxTests/PortalTabDragRoutingTests.swift
Comment thread cmuxTests/PortalTabDragRoutingTests.swift Outdated
@austinywang
austinywang force-pushed the issue-4433-pane-tab-width branch from bee7f46 to f3b1a78 Compare May 20, 2026 10:03
@austinywang
austinywang force-pushed the issue-4433-pane-tab-width branch from f3b1a78 to f037070 Compare May 20, 2026 10:03
coderabbitai[bot]
coderabbitai Bot previously requested changes May 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/PortalTabDragRoutingTests.swift`:
- Around line 32-44: The test passes a false negative because
highSaturationWidth returns 0 when it finds no pixels; update
highSaturationWidth to return an optional (or nil) when no qualifying pixels are
found and propagate that nil through renderedSelectedPaneTabIndicatorWidth so it
returns nil instead of 0, causing XCTUnwrap(measuredWidth) in the test to fail
when the pixel probe found nothing; modify the implementations of
highSaturationWidth and renderedSelectedPaneTabIndicatorWidth accordingly and
keep measuredWidth usage in the test unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35898848-9e10-42d3-a13f-65c68658d898

📥 Commits

Reviewing files that changed from the base of the PR and between e258f03 and bee7f46.

📒 Files selected for processing (2)
  • cmuxTests/PortalTabDragRoutingTests.swift
  • vendor/bonsplit

Comment thread cmuxTests/PortalTabDragRoutingTests.swift

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 2 files

Re-trigger cubic

@lawrencecchen
lawrencecchen dismissed coderabbitai[bot]’s stale review May 20, 2026 10:06

Stale bot review from an earlier head. The referenced pixel-measurement false-pass issue is fixed on f037070: highSaturationWidth now returns nil when no indicator pixels are detected, the test unwraps that optional, and it also requires the measured width to be > 40 before enforcing the compact upper bound.

Comment thread cmuxTests/PortalTabDragRoutingTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
cmuxTests/PortalTabDragRoutingTests.swift (1)

102-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wait for the width sample to settle before returning.

The helper returns the first non-nil width it sees. If SwiftUI/AppKit paints the tab chrome over multiple passes, a narrow early frame can satisfy the <= 80 check and let this regression test false-pass. Track the widest sample during the settle window, or require a couple of stable samples before returning.

Possible fix
     private func waitForHighSaturationWidth(
         in view: NSView,
         sampleRect: NSRect,
         timeout: TimeInterval = 1.0
     ) -> CGFloat? {
         let deadline = Date().addingTimeInterval(timeout)
+        var maxWidth: CGFloat?
         repeat {
             view.layoutSubtreeIfNeeded()
             view.displayIfNeeded()
             if let width = highSaturationWidth(in: view, sampleRect: sampleRect) {
-                return width
+                maxWidth = max(maxWidth ?? 0, width)
             }
             _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
         } while Date() < deadline
-        return highSaturationWidth(in: view, sampleRect: sampleRect)
+        if let width = highSaturationWidth(in: view, sampleRect: sampleRect) {
+            maxWidth = max(maxWidth ?? 0, width)
+        }
+        return maxWidth
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/PortalTabDragRoutingTests.swift` around lines 102 - 116, The helper
waitForHighSaturationWidth currently returns the first non-nil sample and can
early-exit on a narrow paint; change it to sample repeatedly until the deadline
and return the widest observed width (or require N consecutive identical
samples) to avoid transient narrow frames. Update waitForHighSaturationWidth to
call highSaturationWidth(in: sampleRect:) on each loop iteration, track the
maximum non-nil width seen (or count stable samples) while still performing
view.layoutSubtreeIfNeeded()/view.displayIfNeeded(), and after the timeout
return the tracked widest value (or nil if none observed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@cmuxTests/PortalTabDragRoutingTests.swift`:
- Around line 102-116: The helper waitForHighSaturationWidth currently returns
the first non-nil sample and can early-exit on a narrow paint; change it to
sample repeatedly until the deadline and return the widest observed width (or
require N consecutive identical samples) to avoid transient narrow frames.
Update waitForHighSaturationWidth to call highSaturationWidth(in: sampleRect:)
on each loop iteration, track the maximum non-nil width seen (or count stable
samples) while still performing
view.layoutSubtreeIfNeeded()/view.displayIfNeeded(), and after the timeout
return the tracked widest value (or nil if none observed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b8be9163-39ef-4090-8767-5fbf67e47ad7

📥 Commits

Reviewing files that changed from the base of the PR and between bee7f46 and f037070.

📒 Files selected for processing (2)
  • cmuxTests/PortalTabDragRoutingTests.swift
  • vendor/bonsplit

This branch was successfully deployed

1 active deployment
Preview – cmux — f0370709 Deployed May 20, 2026 by vercel[bot]
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.

Pane tabs render too wide after #4290 — tab strip fits ~3 tabs instead of 4+ at the same window width

1 participant