Skip to content

Fix #5099: keep right-sidebar titlebar buttons clickable (stop reparenting interactive controls) - #5101

Merged
austinywang merged 2 commits into
mainfrom
issue-5099-right-sidebar-button-clicks
Jun 1, 2026
Merged

austinywang merged 2 commits into
mainfrom
issue-5099-right-sidebar-button-clicks

Conversation

@austinywang

@austinywang austinywang commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5099.

Symptom

The right sidebar's top button row (Files / Search / Feed / Vault), plus its close and open-as-pane buttons, rendered normally but dead-clicked — mouse clicks were not registered (no hover/press, no panel switch). The keyboard shortcut still toggled/switched the sidebar, which isolated the regression to buttons receiving mouse events, not the RightSidebarMode action path.

Root cause (bisected to PR #5005, 03c937d6f / 4cdf383c3)

PR #5005 ("protect titlebar controls from resize drags", #5003) replaced each right-sidebar mode button's .background(MinimalModeTitlebarControlHitRegionView()) (a zero-impact sibling that only registered the control's hit region, leaving the button a normal SwiftUI control) with .titlebarInteractiveControl(). That modifier reparented the control into a nested NSHostingView (TitlebarInteractiveHostingView) to set mouseDownCanMoveWindow = false + acceptsFirstMouse and to claim drag/double-click immunity.

Reparenting works for controls hosted in a real NSTitlebarAccessoryViewController (the sidebar toggle / notifications / new-tab / focus-history / update pill — what #5003 actually targeted). But the right-sidebar mode bar lives in the full-size-content titlebar band (part of the SwiftUI content hierarchy, overlapping the window titlebar). Moving those controls into a nested hosting view dropped their mouse-downs there — visible-but-dead. The same modifier is also applied to the session-index header controls (SessionIndexView), which had the same latent break.

Fix — general, not a special-case (per rethink-architecturally)

The general class is "interactive titlebar controls reparented into a nested NSHostingView lose clicks in the full-size-content band." Fix the shared modifier instead of patching one row:

  • titlebarInteractiveControl() no longer reparents. It applies a transparent .background(...) marker, TitlebarInteractiveControlRegion, that registers the control's frame with MinimalModeTitlebarControlHitRegionRegistry and returns nil from hitTest (zero impact on the control's own clicks — .background does not change layout).
  • Every titlebar drag / resize-drag / double-click surface already consults that registry via isMinimalModeTitlebarControlHit and yields over registered regions. So the control keeps receiving clicks in place while staying immune to window drag, resize-drag (Sidebar toggle button in title bar sometimes starts a window resize-drag instead of toggling #5003), and double-click zoom/minimize.
  • windowDragHandleShouldCaptureHit checks the registry before its sibling-walk, so the reparenting-identifier fallback (windowDragHandleHitBelongsToTitlebarInteractiveControl) is redundant and is removed. The reparenting infrastructure (TitlebarInteractiveControlHost, TitlebarInteractiveHostingView) is deleted so the footgun can't be reused.

This is the same registry mechanism the right-sidebar buttons used and worked with before #5005, now generalized to every current and future titlebar control. #5003's resize-drag protection is preserved because it is the registry that makes the drag handle yield, not the reparenting.

First-mouse (inactive-window single click) — preserved generally

The nested hosting view also set acceptsFirstMouse, so dropping it would otherwise mean a click on a titlebar control while the window is inactive only activates the window (needs a second click). Restored generally without reparenting: MainWindowHostingView.acceptsFirstMouse(for:) now returns true exactly when the click lands in a registered MinimalModeTitlebarControlHitRegionRegistry region (every titlebarInteractiveControl()), and keeps the default (false) for all other content. Subclassing the SwiftUI content NSHostingView to govern acceptsFirstMouse is the standard way to make hosted SwiftUI controls first-mouse-clickable. (Addresses Cursor Bugbot + cubic P2.)

Tests

Hit-testing/event-routing in the full-size-content titlebar band depends on the real NSWindow theme-frame and is not cleanly unit-testable headlessly, so there is no red/green repro of the dead-click itself (stating this plainly rather than faking one). TitlebarInteractiveControlTests is rewritten to assert the protection contract behaviorally on the new mechanism:

  • a registered control region makes windowDragHandleShouldCaptureHit yield (click reaches the control) while empty chrome stays draggable;
  • a registered region reads as a control hit (isMinimalModeTitlebarControlHit) so the synthetic double-click zoom is suppressed;
  • the region marker is transparent to hit-testing (hitTest == nil) and does not move the window.

The existing WindowAndDragTests passive-host/sibling-walk and double-click tests are unaffected.

Validation

  • python3 scripts/normalize-pbxproj.py cmux.xcodeproj/project.pbxproj + ./scripts/check-pbxproj.sh pass (two source files removed from the project).
  • git diff --check clean.
  • Per repo policy, the app was not built/launched locally; CI validates the executable paths.

🤖 Generated with Claude Code

`titlebarInteractiveControl()` previously wrapped each control in a nested
`NSHostingView` (`TitlebarInteractiveHostingView`) to win window-drag,
resize-drag, and double-click-zoom routing over the control. Reparenting a
SwiftUI control into a nested hosting view works for controls hosted in a real
`NSTitlebarAccessoryViewController` (the sidebar toggle / notifications / new-tab
/ focus-history / update pill), but silently drops mouse-downs for controls that
live in the full-size-content titlebar band — the right-sidebar mode bar
(Files / Search / Feed / Vault), its close + open-as-pane buttons, and the
session-index header controls. The keyboard path was unaffected, matching the
report.

Fix the shared modifier instead of special-casing one row: stop reparenting.
`titlebarInteractiveControl()` now applies a transparent `.background(...)`
marker (`TitlebarInteractiveControlRegion`) that registers the control's region
with `MinimalModeTitlebarControlHitRegionRegistry` and returns `nil` from
hitTest. Every titlebar drag/double-click surface already consults that registry
via `isMinimalModeTitlebarControlHit` and yields over registered regions, so the
control keeps receiving clicks in place while staying immune to window
drag/resize/zoom. This is the same proven mechanism the right-sidebar buttons
used before PR #5005, generalized to every current and future titlebar control.

The drag handle's registry point-check runs before its sibling-walk, so the
now-removed reparenting identifier fallback
(`windowDragHandleHitBelongsToTitlebarInteractiveControl`) is unnecessary; the
reparenting infrastructure (`TitlebarInteractiveControlHost`,
`TitlebarInteractiveHostingView`) is deleted so it can't be reached again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 1, 2026 9:59am
cmux-staging Building Building Preview, Comment Jun 1, 2026 9:59am

@coderabbitai

coderabbitai Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replace the SwiftUI/AppKit hosting-wrapper approach with a transparent TitlebarInteractiveControlRegion marker that registers with MinimalModeTitlebarControlHitRegionRegistry; update modifier, drag-handle hit-routing, first-mouse activation, tests, and Xcode project entries accordingly.

Changes

Titlebar interactive control architecture refactor

Layer / File(s) Summary
TitlebarInteractiveControlRegion marker component
Sources/WindowDragHandleView.swift
Added TitlebarInteractiveControlRegion NSViewRepresentable with nested RegisteredView that registers/unregisters transparent marker views with MinimalModeTitlebarControlHitRegionRegistry on window attach/detach; marker returns nil from hitTest and disables mouseDownCanMoveWindow.
Modifier refactored to use region marker
Sources/TitlebarInteractiveControlModifier.swift
Updated modifier to attach TitlebarInteractiveControlRegion() as a transparent background(...) marker instead of wrapping content in TitlebarInteractiveControlHost; docs updated to describe registry-based hit-region behavior.
Simplified drag-handle hit-test logic
Sources/WindowDragHandleView.swift
Removed identifier-based ownership detection and private helper; windowDragHandleShouldTreatTopHitAsPassiveHost early logic now uses broader hosting-wrapper checks for pass-through handling.
acceptsFirstMouse override for interactive controls
Sources/App/CmuxMainWindow.swift
Added override func acceptsFirstMouse(for:) in MainWindowHostingView that consults isMinimalModeTitlebarControlHit to decide activation behavior when clicking registered interactive titlebar controls.
Test suite refactored for RegisteredView approach
cmuxTests/TitlebarInteractiveControlTests.swift
Updated tests to use TitlebarInteractiveControlRegion.RegisteredView, verifying prevention of window-drag capture, suppression of synthetic double-click zoom over control regions, and marker transparency to hit-testing; added a test asserting hitTest returns nil.
Build configuration updated for removed files
cmux.xcodeproj/project.pbxproj
Removed TitlebarInteractiveControlHost.swift and TitlebarInteractiveHostingView.swift entries from PBXBuildFile, PBXFileReference, PBXGroup Sources, and PBXSourcesBuildPhase compile sources.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#5005: Introduced the TitlebarInteractiveControlHost/TitlebarInteractiveHostingView shim that this PR replaces with the registry-marker approach.

Suggested reviewers

  • Ari4ka

Poem

🐰 I hopped where titlebar whispers met,

I placed a ghostly marker yet,
No wrapper binds, the clicks run free,
Drag gets stopped, controls can be.
A tiny hop, a clearer view — hooray for transparency!


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error PR introduces new Combine-based app state (MinimalModeSidebarChromeHoverState: ObservableObject with @Published property) when @Observable is available on supported macOS 14.0+. Replace MinimalModeSidebarChromeHoverState with @Observable macro-based design and use async observation instead of Combine subscribers.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing issue #5099 by stopping reparenting of interactive titlebar controls to restore click handling.
Linked Issues check ✅ Passed The code changes fully address #5099's objective: right-sidebar titlebar buttons are now clickable by eliminating reparenting while preserving drag/double-click protections via registry-based yielding.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing #5099: modifier refactoring, registry-based hit-testing, removal of reparenting infrastructure, test updates, and project file cleanup.
Cmux Swift Blocking Runtime ✅ Passed No new blocking patterns introduced. NSLock usage in WindowDragHandleView is pre-existing. No Task.sleep, Thread.sleep, semaphores, or DispatchQueue.main.sync found.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift files and project config; the rule scope covers TypeScript, JavaScript, shell, and non-Swift scripts—Swift code is covered by a separate rule.
Cmux Algorithmic Complexity ✅ Passed PR introduces no algorithmic complexity violations. Registry methods unchanged from commit 655d8f1; no new unbounded iterations or hot-path rescans added.
Cmux Swift @Concurrent ✅ Passed PR adds no async functions and properly annotates synchronous UI-bound work with @MainActor; nonisolated(unsafe) state access is protected by NSLock with synchronized getters/setters.
Cmux Swift File And Package Boundaries ✅ Passed +31 lines in oversized file (under 250-line limit). Focused bug fix with AppKit bridge glue. Removed 64 lines via file deletion. Meets existing-oversized-file and inherent-app-target exceptions.
Cmux Swift Logging ✅ Passed No print/debugPrint/dump/NSLog in runtime code. All cmuxDebugLog calls properly guarded with #if DEBUG. No ad hoc file/stdout logging, new Logger constants, or sensitive data exposure.
Cmux User-Facing Error Privacy ✅ Passed PR contains no user-facing error messages, alerts, or sensitive information. Changes are UI event-handling infrastructure modifications (registry-based hit detection) with test-only additions.
Cmux Full Internationalization ✅ Passed PR contains no user-facing text additions requiring localization; changes are technical refactoring for mouse event handling with only internal implementation details, test assertions, and comments.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations: no new @Observable/@Published/@StateObject, no GeometryReader layout changes, no lazy list store references, no render-time mutations. AppKit bridges properly separated.
Cmux Architecture Rethink ✅ Passed Fixes root cause (reparenting-induced dead clicks) with registry-based hit-testing. Single source of truth, proper platform bridges, no symptom patches, comprehensive behavioral tests.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not create or materially change standalone cmux windows. Changes are to view routing within existing windows (removing nested hosting, adding transparent NSView region marker).
Description check ✅ Passed PR description comprehensively covers all required sections: summary of what changed and why, testing approach with behavioral assertions, checklist items completed, and technical validation steps documented.
✨ 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-5099-right-sidebar-button-clicks

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 e162d54. Configure here.

Comment thread Sources/WindowDragHandleView.swift
…nting

Addresses Cursor Bugbot: dropping the nested NSHostingView also dropped its
acceptsFirstMouse(for:), so an inactive-window click on a titlebar control would
only activate the window. Restore that behavior generally via the registry:
MainWindowHostingView.acceptsFirstMouse now returns true exactly when the click
lands in a registered MinimalModeTitlebarControlHitRegionRegistry region (the
regions titlebarInteractiveControl() registers), and keeps the default (false)
for all other content. No reparenting — so active-window clicks keep working in
the full-size-content titlebar band.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/WindowDragHandleView.swift">

<violation number="1" location="Sources/WindowDragHandleView.swift:550">
P2: This transparent marker never becomes the hit view (`hitTest` returns `nil`), so the previous `acceptsFirstMouse` click-through guarantee is removed for titlebar controls. On an inactive window, affected controls can require a second click because the first click only activates the window.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
}

override func hitTest(_ point: NSPoint) -> NSView? { nil }

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: This transparent marker never becomes the hit view (hitTest returns nil), so the previous acceptsFirstMouse click-through guarantee is removed for titlebar controls. On an inactive window, affected controls can require a second click because the first click only activates the window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/WindowDragHandleView.swift, line 550:

<comment>This transparent marker never becomes the hit view (`hitTest` returns `nil`), so the previous `acceptsFirstMouse` click-through guarantee is removed for titlebar controls. On an inactive window, affected controls can require a second click because the first click only activates the window.</comment>

<file context>
@@ -534,6 +521,50 @@ enum MinimalModeTitlebarControlHitRegionRegistry {
+            }
+        }
+
+        override func hitTest(_ point: NSPoint) -> NSView? { nil }
+
+        override var mouseDownCanMoveWindow: Bool { false }
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 221150e. You're right the marker can't grant first-mouse (it's hitTest=nil by design, so it never blocks the control's own clicks). The first-mouse guarantee is instead restored on the content host: MainWindowHostingView.acceptsFirstMouse(for:) now returns true exactly when the click lands in a registered MinimalModeTitlebarControlHitRegionRegistry region (every titlebarInteractiveControl()), and the default (false) elsewhere. Subclassing the SwiftUI content NSHostingView to govern acceptsFirstMouse is the standard way to make hosted SwiftUI controls first-mouse-clickable — SwiftUI routes the button's mouse-down through the hosting view, so the hosting view's answer governs. This recovers single-click-on-inactive-window for all these controls without reparenting (reparenting is what dropped active-window clicks in the full-size-content titlebar band, the bug this PR fixes).

— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes dead-clicks on right-sidebar titlebar buttons (#5099) by replacing the reparenting-into-nested-NSHostingView approach with a registry-based marker. The root cause was that titlebarInteractiveControl() (introduced in #5005) wrapped controls in a TitlebarInteractiveHostingView, which dropped mouse events for controls living in the full-size-content titlebar band.

  • Core fix: TitlebarInteractiveControlModifier now applies a transparent .background(TitlebarInteractiveControlRegion()) that registers the control's frame with MinimalModeTitlebarControlHitRegionRegistry (via a RegisteredView subclass), without reparenting. Drag/double-click routing already yields over registry hits, so the control stays clickable in-place.
  • Deleted: TitlebarInteractiveControlHost.swift and TitlebarInteractiveHostingView.swift, along with the identifier-based sibling-walk fallback windowDragHandleHitBelongsToTitlebarInteractiveControl.
  • acceptsFirstMouse recovery: MainWindowHostingView now overrides acceptsFirstMouse(for:) to return true for registry-registered regions, recovering the single-click activation-plus-action behavior previously provided by the nested host's unconditional acceptsFirstMouse override.

Confidence Score: 5/5

Safe to merge. The change removes a footgun (reparenting NSHostingView) and replaces it with a simpler, registry-based marker that is already used by adjacent code; the fix is targeted and all changed paths are well-tested.

The root cause is correctly identified and eliminated. TitlebarInteractiveControlRegion.RegisteredView is transparent to hit-testing, registers/unregisters its frame via the existing thread-safe registry, and is backed by the same drag/double-click yield paths that the rest of the codebase already depends on. The acceptsFirstMouse override in MainWindowHostingView is correctly scoped to registered regions, recovering the single-click activate-and-trigger behavior without unconditionally opening every content click to first-mouse. The two deleted files are fully superseded, project references are updated, and the tests cover the three behavioral properties the mechanism depends on.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/CmuxMainWindow.swift Adds acceptsFirstMouse(for:) override to MainWindowHostingView scoped to registry-registered regions, recovering single-click activate-and-trigger for titlebar controls on inactive windows.
Sources/TitlebarInteractiveControlModifier.swift Simplified from wrapping content in TitlebarInteractiveControlHost to applying a transparent .background(TitlebarInteractiveControlRegion()) marker; no reparenting.
Sources/WindowDragHandleView.swift Adds TitlebarInteractiveControlRegion/RegisteredView (the marker that replaces the nested host), removes windowDragHandleHitBelongsToTitlebarInteractiveControl fallback, and cleans up windowDragHandleShouldTreatTopHitAsPassiveHost.
Sources/TitlebarInteractiveControlHost.swift Deleted — the reparenting NSViewRepresentable host is no longer needed.
Sources/TitlebarInteractiveHostingView.swift Deleted — the reparenting NSHostingView subclass is no longer needed.
cmuxTests/TitlebarInteractiveControlTests.swift Tests rewritten to assert registry-based drag-handle yield, double-click suppression, and hit-test transparency on RegisteredView directly; adds new regionMarkerIsTransparentToHitTesting test.
cmux.xcodeproj/project.pbxproj Removes build and file references for the two deleted source files; no other structural changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant AppKit
    participant MainWindowHostingView
    participant RegisteredView
    participant Registry as MinimalModeTitlebarControlHitRegionRegistry
    participant DragHandle as WindowDragHandleView
    participant Control as SwiftUI Control

    Note over RegisteredView,Registry: On view mount (viewDidMoveToWindow)
    RegisteredView->>Registry: register(self)

    Note over User,Control: Mouse click on titlebar control (active window)
    User->>AppKit: leftMouseDown
    AppKit->>DragHandle: hitTest(point)
    DragHandle->>Registry: isMinimalModeTitlebarControlHit?
    Registry-->>DragHandle: true (registered region hit)
    DragHandle-->>AppKit: nil (yields)
    AppKit->>Control: delivers mouseDown ✓

    Note over User,Control: Mouse click on titlebar control (inactive window)
    User->>AppKit: leftMouseDown (window inactive)
    AppKit->>MainWindowHostingView: acceptsFirstMouse(for: event)?
    MainWindowHostingView->>Registry: isMinimalModeTitlebarControlHit?
    Registry-->>MainWindowHostingView: true
    MainWindowHostingView-->>AppKit: true
    AppKit->>Control: activate + deliver mouseDown ✓

    Note over User,DragHandle: Mouse click on empty titlebar chrome
    User->>AppKit: leftMouseDown
    AppKit->>DragHandle: hitTest(point)
    DragHandle->>Registry: isMinimalModeTitlebarControlHit?
    Registry-->>DragHandle: false
    DragHandle-->>AppKit: self (captures)
    AppKit->>DragHandle: mouseDown → performDrag ✓
Loading

Reviews (1): Last reviewed commit: "fix: grant first-mouse to registered tit..." | Re-trigger Greptile

This branch was successfully deployed

1 active deployment
Preview – cmux — 221150e6 Deployed Jun 1, 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.

Right sidebar top buttons (Files/Search/Feed/Vault) no longer clickable — dead clicks (keyboard toggle still works)

1 participant