Repository navigation
fix(next): menus opened inside a native popup keep the popup open (bookmark bubble's Folder picker) - #28704
Conversation
…(red before the fix) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f2206f5448805d85f14b
The bookmark edit bubble closed when its Folder picker opened: the picker's NSMenu took the keys and NativePopupHost read the key loss as a dismiss. The host now treats menus opened from inside the popup (and windows parented under its panel) as part of it: key loss, outside clicks and Escape while such a menu tracks close only the menu, and the popup takes the keys back when it ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f2206f5448805d85f14b
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Shipped in:
The feat-cmux-next push run https://github.com/manaflow-ai/cmux/actions/runs/41255987285 at 97d2420 failed: cmux-next fleet Release compile. |
|
Found 2 test failures on Blacksmith runners: Failures
|
Review: the early return for a key loss during a press in the popup kept it open whenever the press handed the keys to an unrelated window. The host now re-checks once the press is handled and closes the popup unless a child menu or window took the keys. Menu tracking is observed synchronously, so the order of resignKey and didBeginTracking no longer matters. Tests cover both orders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f2206f5448805d85f14b
NativePopupHost takes the NotificationCenter it observes (default .default, so the app is unchanged). The child-menu tests hand it a fresh center and post the menu tracking notices there, so no other observer in the test process gets them (check-test-notification-centers.sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f2206f5448805d85f14b
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Review (codex gpt-6-astra high, next-review): SHIP at cb464cd SHIP at cb464cd Static review found no blocking defect. AppKit tests could not run in this Linux box, which has no Swift toolchain.
Follow-ups:
|
A newer verdict blocks edd2696 (no independent SHIP at the head).
|
Review (next-review patch-id; carried): SHIP at edd2696 The independent SHIP at cb464cd carries to this head: the PR's own change ( |
|
Review (next-review patch-id; carried): SHIP at aeeea58 The independent SHIP at cb464cd carries to this head: the PR's own change ( |
…o assumeIsolated as nonisolated(unsafe) #28704 (a8502be) added NSMenu didBegin/didEndTracking observers that pass the notification's NSMenu into MainActor.assumeIsolated. Swift 6 refuses it: NativePopupHost.swift:235/239 'sending menu risks causing data races'. Compile check app fails at base db63437 in CmuxNextDesign, so nothing downstream compiles. The menu is read only on its own main-thread delivery, so it is bound nonisolated(unsafe) as MainActorObservation does for its note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-53d81bfaf9dd62794c6b
…without a sending error (a8502be; #28704) a8502be (#28704) made CmuxNextDesign fail to compile under Swift 6 region checks: "sending 'menu' risks causing data races" at NativePopupHost.swift:235 and :239 (fleet compile-check, aws-m4pro-8). The menu is now read from the (Sendable) notification inside the main-actor closure, after the same main-thread guard, so no non-Sendable value crosses into it. No behavior change; no new assumeIsolated or unchecked escape.
…o assumeIsolated as nonisolated(unsafe) #28704 (a8502be) added NSMenu didBegin/didEndTracking observers that pass the notification's NSMenu into MainActor.assumeIsolated. Swift 6 refuses it: NativePopupHost.swift:235/239 'sending menu risks causing data races'. Compile check app fails at base db63437 in CmuxNextDesign, so nothing downstream compiles. The menu is read only on its own main-thread delivery, so it is bound nonisolated(unsafe) as MainActorObservation does for its note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-53d81bfaf9dd62794c6b
…isolated(unsafe) before assumeIsolated (#29871) * base red: NativePopupHost's menu-tracking observers send the menu into assumeIsolated as nonisolated(unsafe) #28704 (a8502be) added NSMenu didBegin/didEndTracking observers that pass the notification's NSMenu into MainActor.assumeIsolated. Swift 6 refuses it: NativePopupHost.swift:235/239 'sending menu risks causing data races'. Compile check app fails at base db63437 in CmuxNextDesign, so nothing downstream compiles. The menu is read only on its own main-thread delivery, so it is bound nonisolated(unsafe) as MainActorObservation does for its note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-53d81bfaf9dd62794c6b * base red fix: main-proof on the assumeIsolated lines; record the two crash-allow waivers crash_ratchet reads a main-proof comment only on its own line or the one above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-53d81bfaf9dd62794c6b --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… (Swift 6 sending error from #28704)
…without a sending error (a8502be; #28704) a8502be (#28704) made CmuxNextDesign fail to compile under Swift 6 region checks: "sending 'menu' risks causing data races" at NativePopupHost.swift:235 and :239 (fleet compile-check on aws-m4pro-8). The observers must stay queue-less (delivered inside the press, as the child-menu tests post them), so the menu is handed to MainActor.assumeIsolated through a reviewed nonisolated(unsafe) local: NSMenu is a main-thread object and the block returns off the main thread before it. No behavior change. Two waived unchecked hits are recorded (crash-safety-baseline.json waived swift.unchecked CmuxNextDesign 3 -> 5).
… (Swift 6 sending error from #28704)
…apter (MenuTracking), so the popup host compiles and adds no assumeIsolated or observer (#29856) a8502be (#28704, "menus opened inside a native popup keep the popup open") broke the tip twice: - compile: NativePopupHost.swift:235:46 and :239:46 "sending 'menu' risks causing data races" (the NSMenu read from the notification was sent into MainActor.assumeIsolated); - arch: assume_isolated_outside_adapters +3 and observer_sites +2 (two raw NSMenu did{Begin,End}Tracking observers with assumeIsolated in the popup host, and a DispatchQueue.main.async + assumeIsolated). Root fix (PATTERNS.md P2/P3, no escape added): - MenuTracking (new, CmuxNextCompat) is the one seam for the two NSMenu tracking notices: it observes both through MainActorObservation and calls its subscribers on the main actor with `.began(menu)` / `.ended(menu)`. - MainActorObservation gains `atOnce:` (queue nil): the handler runs in the poster's own call, as #28704 needs (a press that opens a menu sees it before its key-loss check), and only for a post on the main thread. Its one reviewed main-delivery proof covers both modes; the NSMenu is read on the main actor inside that proof, never sent across isolation. - NativePopupHost keeps a MenuTracking while it watches (dropped in unwatch, as the raw observers were); its deferred dismiss check is a `Task { @mainactor }` instead of DispatchQueue.main.async + assumeIsolated. - The two other NSMenu tracking observers use it too, so those notices are observed in one place: ChromiumWarmup's wait for a menu to end (a raw observer with assumeIsolated) and TabMenuTracking's end of the strip's menu (it filters the ended menu in the subscriber instead of by `object:`). Both now hear the end in AppKit's post instead of right after it. CmuxNextChromiumWarmup declares its CmuxNextCompat dependency (core). arch-ratchet against the merge base: assume_isolated_outside_adapters -5, observer_sites -2, no rise. defaults_guard: exit 0. Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Bug
The bookmark edit bubble ("Bookmark added" / "Edit bookmark",
cmux.bookmarks.edit) closed as soon as its Folder picker opened.The bubble is a
NativePopupHostpanel. The Folder picker is aThemedPicker, a pop-up button whose menu is AppKit's ownNSMenu, which runs in a window of its own. When that menu opened it took the keys from the bubble, and the panel'sresignKey()called the host'sonLostKeys, which treated the key loss as a dismiss. The host's click and Escape monitors had the same gap: they only knew the panel itself and the toggle control, so a menu opened from inside the popup counted as "outside".Fix
NativePopupHostnow treats a menu opened from inside its popup, and a window parented under its panel, as part of the popup:NSMenu.didBeginTrackingNotificationanddidEndTrackingNotification. A menu counts as the popup's own when a view in the panel owns it (a pop-up button's menu, a context menu) or when the event that opened it was in the popup. The menu bar's menus never count.ActiveAppKeyPanelstill only takes them while the app is active.So a pick, a click elsewhere or Escape closes only the inner menu, and the bubble stays.
Unchanged:
NativePopupHostcallers (Page Info) close themselves before opening children, so they never hit the new path.The menu observers are added in
watchand removed inunwatch, together with the existing monitors, so they live only while the popup is shown.Tests
CmuxNextDesignTests/NativePopupHostChildMenuTestsbuilds a visible window with a bubble-shaped keyed popup that holds aThemedPopUpButton, the controlThemedPickermakes:thePickersMenuTakingTheKeysLeavesTheBubbleOpen: the picker's menu begins tracking and the panel resigns key, then a pick ends the menu. The bubble stays open andonDismissnever fires. It fails before the fix.losingTheKeysToSomethingElseStillClosesTheBubble: an unrelated menu begins tracking and the panel resigns key. The bubble closes once. It passes before and after.The first commit adds only the tests, so it shows the red; the second commit is the fix.
Risk: low. The change is contained in the shared native popup host, and nothing changes for popups that open no child menu.
🤖 Generated with Claude Code
Agent session: agent session