Skip to content

Fix #5003: protect titlebar controls from resize drags - #5005

Merged
austinywang merged 6 commits into
mainfrom
issue-5003-sidebar-toggle-resize-drag
May 31, 2026
Merged

austinywang merged 6 commits into
mainfrom
issue-5003-sidebar-toggle-resize-drag

Conversation

@austinywang

@austinywang austinywang commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5003.

Summary

  • Add a Swift Testing regression that models a SwiftUI titlebar button layered over the explicit WindowDragHandleView; it asserts the button blocks drag capture while empty titlebar chrome remains draggable.
  • Add a reusable titlebarInteractiveControl() SwiftUI modifier backed by an AppKit NSHostingView shim that opts out of mouseDownCanMoveWindow and accepts first mouse.
  • Teach WindowDragHandleView to treat hits inside that shim as interactive control hits, even when SwiftUI returns a private hosting subview from hitTest.
  • Apply the modifier to the shared TitlebarControlButton path, which covers the sidebar toggle, notifications, new workspace, and focus-history controls, plus sibling right-sidebar titlebar/toolbar controls.

Code Path

  • Main window setup: AppDelegate.makeWindow uses .titled, .resizable, .fullSizeContentView, transparent titlebar chrome, and cmux's explicit drag-handle behavior.
  • Sidebar toggle path: UpdateTitlebarAccessoryController -> TitlebarControlsAccessoryViewController -> TitlebarControlsView.controlsGroup -> TitlebarControlButton -> AppDelegate.toggleSidebarInActiveMainWindow.
  • Root cause: the SwiftUI button could appear to AppKit/cmux drag routing as a passive hosting hit, allowing titlebar drag/resize handling to win over the button action on some clicks.

Notes

Validation

  • git diff --check
  • python3 scripts/normalize-pbxproj.py --check cmux.xcodeproj/project.pbxproj
  • ./scripts/check-pbxproj.sh

Local app reload/build and local test execution were intentionally not run per the issue instructions; CI should validate the executable paths.


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


Note

Low Risk
Localized AppKit/SwiftUI titlebar hit-testing and gesture routing; no auth, data, or core business-logic changes.

Overview
Fixes titlebar controls losing clicks to window drag and titlebar double-click (zoom/minimize) by replacing the old passive MinimalModeTitlebarControlHitRegionView background with a reusable .titlebarInteractiveControl() modifier.

The modifier hosts the control in an AppKit TitlebarInteractiveHostingView that opts out of window-drag on mouse down, accepts first mouse, registers with MinimalModeTitlebarControlHitRegionRegistry, and exposes a stable view identifier. WindowDragHandleView now treats hits under that host as real controls (not passive NSHostingView pass-through), so explicit drag capture does not swallow button clicks.

The modifier is applied across shared titlebar UI (TitlebarControlButton / update accessory) and right-sidebar chrome (mode pills, close, open-as-pane, vault grouping, scope toggle, reload). Swift tests cover drag vs. empty titlebar chrome and double-click suppression on interactive controls.

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


Summary by cubic

Fixes #5003. Prevents window drag/resize and titlebar double-click from stealing clicks on titlebar controls by adding a SwiftUI modifier that marks controls as interactive. Empty titlebar space remains draggable and still responds to double-click.

  • Bug Fixes
    • Added .titlebarInteractiveControl() that hosts content in an AppKit NSHostingView subclass, disables window-drag on mouse down, accepts first mouse, sets a stable identifier, and registers as a titlebar-control hit region to suppress titlebar double-click actions.
    • Updated WindowDragHandleView to skip drag capture for hits inside that host (even if a nested SwiftUI subview is the top hit).
    • Replaced per-control .background(MinimalModeTitlebarControlHitRegionView()) with the new modifier and removed the old view.
    • Applied the modifier to TitlebarControlButton and right-sidebar controls: mode bar, grouping, scope toggle, reload, Open as Pane, and header controls.
    • Added tests for drag capture and double-click suppression; empty titlebar chrome still drags and double-clicks.

Written for commit 42c4078. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • UI controls can be marked as titlebar-interactive so they receive clicks without triggering window drags.
  • Bug Fixes

    • Titlebar controls (buttons, toggles, mode pills) reliably receive clicks and no longer cause unintended window movement.
  • Improvements

    • Consistent interaction treatment across sidebar and titlebar controls for predictable click behavior.
  • Tests

    • Added tests validating titlebar hit-testing, drag behavior, and double-click handling.

Review Change Stack

@vercel

vercel Bot commented May 29, 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 May 31, 2026 12:59am
cmux-staging Building Building Preview, Comment May 31, 2026 12:59am

@coderabbitai

coderabbitai Bot commented May 29, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds TitlebarInteractiveHostingView plus an NSViewRepresentable and ViewModifier to mark SwiftUI controls as interactive titlebar controls, updates window-drag hit-testing to respect those markers, applies the modifier to multiple controls, and adds tests + project wiring.

Changes

Titlebar Interactive Control System

Layer / File(s) Summary
Core hosting & SwiftUI bridge
Sources/TitlebarInteractiveHostingView.swift, Sources/TitlebarInteractiveControlHost.swift, Sources/TitlebarInteractiveControlModifier.swift
Defines TitlebarInteractiveHostingView (NSHostingView subclass with mouseDownCanMoveWindow = false and acceptsFirstMouse = true), TitlebarInteractiveControlHost (NSViewRepresentable that preserves \.isEnabled), and titlebarInteractiveControl() ViewModifier.
Hit-testing integration
Sources/WindowDragHandleView.swift
windowDragHandleShouldTreatTopHitAsPassiveHost(_:) now early-returns for views that belong to the titlebar interactive hosting identifier; adds helper that walks the superview chain comparing identifiers.
Apply modifier to existing controls
Sources/RightSidebarPanelView.swift, Sources/SessionIndexView.swift, Sources/Update/UpdateTitlebarAccessory.swift
Adds .titlebarInteractiveControl() to open-as-pane, close header, mode buttons, session scope toggle, reload button, session grouping pill buttons, and TitlebarControlButton so those controls are hosted with the interactive identifier.
Project wiring & tests
cmux.xcodeproj/project.pbxproj, cmuxTests/TitlebarInteractiveControlTests.swift
Adds Xcode project entries for new sources and a test suite that asserts hosted SwiftUI titlebar controls block window drag handles and receive clicks.

Sequence Diagram

sequenceDiagram
  participant User
  participant Button as Titlebar Button
  participant Host as TitlebarInteractiveHostingView
  participant WindowDragHandle

  User->>Button: Click on control in titlebar
  Button->>Host: mouseDown event delivered
  Host->>Host: mouseDownCanMoveWindow = false
  Host->>Host: acceptsFirstMouse = true
  Host->>Button: Event reaches control logic

  WindowDragHandle->>WindowDragHandle: windowDragHandleHitBelongsToTitlebarInteractiveControl?
  WindowDragHandle->>WindowDragHandle: Walk superview chain for identifier
  WindowDragHandle-->>WindowDragHandle: return false (not passive) if found

  Button->>User: Control handles click, window does not drag
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#4309: Modifies the same titlebar sidebar toggle/control area and related accessory behavior.

Suggested reviewers

  • Ari4ka

Poem

"I’m a rabbit in the titlebar, light on my paws,
I hop on buttons and outsmart drag laws.
With hosts and modifiers I guard every click,
I nudge events their way and keep the window slick.
Hooray — clicks find homes, and drags stay away! 🐰"

🚥 Pre-merge checks | ✅ 17 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding protection for titlebar controls from resize drags, with a direct reference to the issue being fixed.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses all coding requirements from issue #5003: adds a reusable titlebarInteractiveControl() modifier, updates WindowDragHandleView to recognize interactive controls, applies the modifier to titlebar controls, provides Swift Testing regressions, and preserves empty titlebar drag behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing #5003: the new titlebar interactive control infrastructure, application to specific titlebar controls, test coverage, and project file updates. No unrelated modifications or changes to window style defaults.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation issues. TitlebarInteractiveHostingView correctly @MainActor. NSViewRepresentable/ViewModifier types implicitly MainActor-aware. File helpers in appropriate AppKit context.
Cmux Swift Blocking Runtime ✅ Passed No new blocking runtime primitives introduced. New files contain only UI setup code. WindowDragHandleView additions use view traversal only, no synchronization primitives added.
Cmux No Hacky Sleeps ✅ Passed Rule applies only to TypeScript, JavaScript, and shell scripts. This PR contains only Swift files and Xcode config—no non-Swift runtime scripts changed.
Cmux Algorithmic Complexity ✅ Passed Modifiers applied to tiny fixed-size enums (5 RightSidebarMode, 2 SessionGrouping cases); superview traversal is bounded-depth event handling, not scalable data processing.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: new code uses SwiftUI @Environment, @MainActor, and AppKit overrides only. No DispatchQueue, Combine, Tasks, or completion handlers added.
Cmux Swift @Concurrent ✅ Passed PR adds only synchronous functions with appropriate @MainActor isolation for UI NSView subclass; no missing or invalid @concurrent annotations.
Cmux Swift File And Package Boundaries ✅ Passed All new files under 400 lines with single responsibilities; minimal additions to oversized files; no mixed responsibilities; appropriate app-target placement for AppKit/SwiftUI glue.
Cmux Swift Logging ✅ Passed No logging violations found. All new/modified Swift code avoids print, debugPrint, dump, NSLog, and improper Logger declarations. Tests are properly allowed.
Cmux User-Facing Error Privacy ✅ Passed PR contains no user-facing error messages, alerts, command output, API errors, or sensitive information. All changes are technical modifiers and AppKit integration for titlebar control handling.
Cmux Full Internationalization ✅ Passed No new user-facing strings, localization keys, or catalog changes. New files contain only technical code; modifications apply existing modifiers without adding localized text.
Cmux Swiftui State Layout ✅ Passed No ObservableObject/@Published/@StateObject; uses proper @Environment reading in NSViewRepresentable; no GeometryReader, lazy containers, or render-time mutations.
Cmux Architecture Rethink ✅ Passed PR uses required platform bridge (NSViewRepresentable/NSHostingView) with no timing patterns, state duplication, or split lifecycle; addresses root cause via mouseDownCanMoveWindow override.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds NSHostingView and ViewModifier for titlebar controls, not standalone windows. No NSWindow/NSPanel/NSWindowController/Window/WindowGroup code introduced; test-only fixtures allowed.
Description check ✅ Passed The PR description provides a comprehensive summary of changes, detailed code paths, validation steps, and important context about affected controls and preserved behavior.

✏️ 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-5003-sidebar-toggle-resize-drag

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.

Comment thread Sources/SessionIndexView.swift Outdated
@greptile-apps

greptile-apps Bot commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes #5003 by replacing per-control MinimalModeTitlebarControlHitRegionView backgrounds with a new .titlebarInteractiveControl() SwiftUI modifier backed by TitlebarInteractiveHostingView — an NSHostingView subclass that sets mouseDownCanMoveWindow = false, accepts first mouse, and self-registers with MinimalModeTitlebarControlHitRegionRegistry for double-click suppression. WindowDragHandleView gains a superview-walk guard that short-circuits the passive-host classification when any ancestor carries the TitlebarInteractiveHostingView identifier, and a regression test suite is added.

  • New TitlebarInteractiveHostingView + modifier: wraps controls in an AppKit NSHostingView subclass that opts out of window-drag and titlebar double-click promotion; registration/unregistration is lifecycle-driven via viewDidMoveToWindow and deinit.
  • WindowDragHandleView hit routing: windowDragHandleHitBelongsToTitlebarInteractiveControl walks the superview chain up to contentView to detect wrapped controls, stopping the passive-host shortcut from swallowing interactive-control clicks.
  • Callsite updates: .titlebarInteractiveControl() replaces .background(MinimalModeTitlebarControlHitRegionView()) on all right-sidebar and session-index controls, and is appended to TitlebarControlButton.

Confidence Score: 5/5

Safe to merge — changes are confined to titlebar hit-testing and mouse-routing plumbing with no data, auth, or state-management paths affected.

The new code paths (superview walk, registry registration, mouseDownCanMoveWindow override, acceptsFirstMouse) are well-scoped and the failure modes are limited to visual/interaction edge cases rather than data loss or security issues. The pre-existing deinit-calls-registry pattern is maintained consistently. Tests cover both drag-capture blocking and double-click suppression.

No files require special attention; TitlebarInteractiveControlHost.swift has a minor redundant registry call on every SwiftUI update that could be cleaned up.

Important Files Changed

Filename Overview
Sources/TitlebarInteractiveHostingView.swift New NSHostingView subclass that disables window-drag, accepts first mouse, and self-registers/unregisters with MinimalModeTitlebarControlHitRegionRegistry; deinit calls unregister following the same pre-existing pattern from the removed RegisteredView.
Sources/TitlebarInteractiveControlHost.swift NSViewRepresentable bridge; calls MinimalModeTitlebarControlHitRegionRegistry.register in updateNSView on every SwiftUI update even though viewDidMoveToWindow already handles registration — redundant but idempotent.
Sources/WindowDragHandleView.swift Adds windowDragHandleHitBelongsToTitlebarInteractiveControl superview walk (bounded by contentView) as an early-exit in windowDragHandleShouldTreatTopHitAsPassiveHost; removes MinimalModeTitlebarControlHitRegionView struct; logic is correct.
cmuxTests/TitlebarInteractiveControlTests.swift Two @mainactor Swift Testing tests: one verifies drag capture is blocked by a TitlebarControlButton, the other verifies isMinimalModeTitlebarControlHit returns true inside the host; both rely on NSHostingView laying out its SwiftUI tree synchronously during layoutSubtreeIfNeeded().

Sequence Diagram

sequenceDiagram
    participant SUI as SwiftUI Body
    participant Mod as titlebarInteractiveControl()
    participant Host as TitlebarInteractiveControlHost
    participant HV as TitlebarInteractiveHostingView
    participant Reg as MinimalModeTitlebarControlHitRegionRegistry
    participant DH as WindowDragHandleView

    SUI->>Mod: .titlebarInteractiveControl()
    Mod->>Host: makeNSView / updateNSView
    Host->>HV: create + set identifier
    HV->>Reg: register(self) via viewDidMoveToWindow

    Note over DH: Mouse-down hit test
    DH->>DH: windowDragHandleShouldCaptureHit(point)
    DH->>DH: sibling.hitTest → hitView
    DH->>DH: windowDragHandleShouldTreatTopHitAsPassiveHost(hitView)
    DH->>DH: windowDragHandleHitBelongsToTitlebarInteractiveControl(hitView)
    Note right of DH: Walk superview chain to contentView, find viewIdentifier
    DH-->>SUI: "capture=false, control gets the click"

    Note over DH: Synthetic double-click check
    DH->>Reg: isMinimalModeTitlebarControlHit(window, point)
    Reg->>Reg: containsWindowPoint finds HV
    Reg-->>DH: true, suppress zoom/minimize
Loading

Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +4 to +14
let titlebarInteractiveControlViewIdentifier = NSUserInterfaceItemIdentifier("cmux.titlebarInteractiveControl")

/// Hosts SwiftUI controls that must receive titlebar mouse-downs instead of moving the window.
@MainActor
final class TitlebarInteractiveHostingView<Content: View>: NSHostingView<Content> {
override var mouseDownCanMoveWindow: Bool { false }

override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The identifier is declared as a module-level let constant, but WindowDragHandleView.viewIdentifier is namespaced as a static let inside the struct that owns it. Putting the identifier on the class that sets and reads it avoids polluting the module namespace and makes the ownership explicit.

Suggested change
let titlebarInteractiveControlViewIdentifier = NSUserInterfaceItemIdentifier("cmux.titlebarInteractiveControl")
/// Hosts SwiftUI controls that must receive titlebar mouse-downs instead of moving the window.
@MainActor
final class TitlebarInteractiveHostingView<Content: View>: NSHostingView<Content> {
override var mouseDownCanMoveWindow: Bool { false }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}
}
/// Hosts SwiftUI controls that must receive titlebar mouse-downs instead of moving the window.
@MainActor
final class TitlebarInteractiveHostingView<Content: View>: NSHostingView<Content> {
static let viewIdentifier = NSUserInterfaceItemIdentifier("cmux.titlebarInteractiveControl")
override var mouseDownCanMoveWindow: Bool { false }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +432 to +441
private func windowDragHandleHitBelongsToTitlebarInteractiveControl(_ view: NSView) -> Bool {
var candidate: NSView? = view
while let current = candidate {
if current.identifier == titlebarInteractiveControlViewIdentifier {
return true
}
candidate = current.superview
}
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The superview walk has no upper-bound guard: for every sibling hit that lands in empty chrome (and therefore has no TitlebarInteractiveHostingView ancestor), it traverses all the way up to the root of the AppKit hierarchy before returning false. Stopping at the drag handle's own window content view makes the scope explicit and avoids crossing any unexpected hosting boundaries.

Suggested change
private func windowDragHandleHitBelongsToTitlebarInteractiveControl(_ view: NSView) -> Bool {
var candidate: NSView? = view
while let current = candidate {
if current.identifier == titlebarInteractiveControlViewIdentifier {
return true
}
candidate = current.superview
}
return false
}
private func windowDragHandleHitBelongsToTitlebarInteractiveControl(_ view: NSView) -> Bool {
let contentView = view.window?.contentView
var candidate: NSView? = view
while let current = candidate {
if current.identifier == TitlebarInteractiveHostingView<AnyView>.viewIdentifier {
return true
}
if let contentView, current === contentView {
break
}
candidate = current.superview
}
return false
}

@austinywang
austinywang force-pushed the issue-5003-sidebar-toggle-resize-drag branch 2 times, most recently from 5e58fb5 to 835e9b7 Compare May 29, 2026 23:47

@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 `@Sources/RightSidebarPanelView.swift`:
- Around line 590-591: The titlebar control modifier order is inconsistent: in
this location the .titlebarInteractiveControl() is applied before
.background(MinimalModeTitlebarControlHitRegionView()), while in
openAsPaneButton and closeButton the background comes first; update this
occurrence in RightSidebarPanelView so the
.background(MinimalModeTitlebarControlHitRegionView()) is applied before
.titlebarInteractiveControl(), matching the ordering used in openAsPaneButton
and closeButton to improve consistency and readability.
🪄 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: 188ab046-a687-49a0-910c-df89edb654aa

📥 Commits

Reviewing files that changed from the base of the PR and between 835e9b7 and 655d8f1.

📒 Files selected for processing (8)
  • Sources/RightSidebarPanelView.swift
  • Sources/SessionIndexView.swift
  • Sources/TitlebarInteractiveControlHost.swift
  • Sources/TitlebarInteractiveControlModifier.swift
  • Sources/TitlebarInteractiveHostingView.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • Sources/WindowDragHandleView.swift
  • cmux.xcodeproj/project.pbxproj

Comment thread Sources/RightSidebarPanelView.swift Outdated
austinywang and others added 2 commits May 30, 2026 16:28
A control wrapped with titlebarInteractiveControl() must register as a
titlebar control hit region so the synthetic titlebar double-click
monitors skip it instead of zooming/minimizing the window. This test
fails until the host self-registers with
MinimalModeTitlebarControlHitRegionRegistry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
titlebarInteractiveControl() suppressed window drags (mouseDownCanMoveWindow
= false) but not the synthetic titlebar double-click action, which is run by
local-monitor surfaces (TitlebarDoubleClickMonitorView, the minimal-mode
titlebar surfaces, the window drag handle, and WindowDecorationsController).
Those surfaces all defer to MinimalModeTitlebarControlHitRegionRegistry via
isMinimalModeTitlebarControlHit, but the interactive-control host never
registered, so double-clicking a titlebar icon (e.g. the sidebar toggle)
zoomed/minimized the window in addition to running the control's action.

Make TitlebarInteractiveHostingView register itself with the registry so a
single titlebarInteractiveControl() grants both drag- and double-click-
immunity. This makes the per-call-site .background(MinimalModeTitlebarControlHitRegionView())
registrations redundant, so remove them and the now-unused view.

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

@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 39f60a7. Configure here.

Comment thread Sources/TitlebarInteractiveHostingView.swift
Comment on lines +78 to +99

let container = NSView(frame: NSRect(x: 0, y: 0, width: 240, height: 48))
window.contentView = container

// Mirror how `titlebarInteractiveControl()` hosts a control: the host must
// register itself as a titlebar control hit region so the synthetic
// double-click monitors skip it instead of zooming/minimizing the window.
let host = TitlebarInteractiveHostingView(rootView: AnyView(Color.clear))
host.identifier = TitlebarInteractiveHostingView<AnyView>.viewIdentifier
host.frame = NSRect(x: 12, y: 14, width: 24, height: 24)
container.addSubview(host)
host.layoutSubtreeIfNeeded()

let insideControl = NSPoint(x: host.frame.midX, y: host.frame.midY)
#expect(
isMinimalModeTitlebarControlHit(window: window, locationInWindow: insideControl),
"A double-click on a titlebarInteractiveControl must register as a control hit so the synthetic titlebar double-click (zoom/minimize) is suppressed."
)

let emptyTitlebar = NSPoint(x: 220, y: 24)
#expect(
!isMinimalModeTitlebarControlHit(window: window, locationInWindow: emptyTitlebar),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Second test assertion will fail — isMinimalModeTitlebarControlHit doesn't check TitlebarInteractiveHostingView

isMinimalModeTitlebarControlHit only queries MinimalModeTitlebarControlHitRegionRegistry.containsWindowPoint, which iterates over views explicitly registered via MinimalModeTitlebarControlHitRegionView.RegisteredView. TitlebarInteractiveHostingView never calls MinimalModeTitlebarControlHitRegionRegistry.register, so the #expect(isMinimalModeTitlebarControlHit(…) == true) assertion on line 91 will evaluate to false and fail CI.

This also exposes a production gap: in minimal mode, a double-click on any control that only receives .titlebarInteractiveControl() (e.g. TitlebarControlButton — sidebar toggle, notifications, new workspace, focus history — and the SessionIndexView scope/reload controls) would not suppress the synthetic titlebar double-click, so the window would still zoom or minimize on a double-click landing on those buttons.

The RightSidebarPanelView controls happen to have MinimalModeTitlebarControlHitRegionView() applied alongside .titlebarInteractiveControl(), so they are covered. But TitlebarControlButton has no MinimalModeTitlebarControlHitRegionView.

The simplest fix is to have TitlebarInteractiveHostingView self-register with the registry in viewDidMoveToWindow/deinit, matching the pattern used by MinimalModeTitlebarControlHitRegionView.RegisteredView.

@austinywang
austinywang merged commit 03c937d into main May 31, 2026
19 checks passed
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request May 31, 2026
Includes detachable SSH PTY persistence (manaflow-ai#4807), titlebar polish,
Hermes session restore + pre_tool_call notif, omnibar lag fix,
sidebar extension polish, blank sidebar icon fallback, browser
extension pane integration, GitHub GraphQL rate-limit handling,
sidebar resize-toggle drag, large diff viewer pre-render.

Conflicts resolved:
- Workspace.swift: keep fork's layoutTabs scaffolding while adopting
  upstream's snapshotWorkspaceId-aware restore signature, persistent
  SSH PTY restored session-id path, browser tab mute (manaflow-ai#5005), and
  the relayPort-aware reverse-relay control-master invocation.
- AppDelegate.swift, ShortcutBareStartRouting.swift: combine fork's
  toggleQuickTerminal exception with upstream's browser-content
  shortcut filter.
- UpdateTitlebarAccessory.swift: keep both fork's
  rightSidebarToggleControllers + sidebarTrailingEdgesByWindow and
  upstream's titlebar control changes.
- TerminalControllerSocketSecurityTests.swift: keep both fork's v2
  surface report-pwd remote workspace test and upstream's
  preserve_after_terminal_exit + remote PTY resize tests.
- SessionBlueprintEncoder.swift: extend StableLayout encoder with the
  new PanelType.extensionBrowser case.
- CmuxSettingsUI/SettingsWindowScene.swift: rename the local
  `anchor` binding so fork's host-injected extra-section path
  doesn't shadow upstream's new function-scope `anchor` parameter.
pull Bot pushed a commit to TheTechOddBug/cmux that referenced this pull request Jun 1, 2026
`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 manaflow-ai#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>

This branch was successfully deployed

1 active deployment
Preview – cmux — 42c40787 Deployed May 31, 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.

Sidebar toggle button in title bar sometimes starts a window resize-drag instead of toggling

1 participant