@@ -32,6 +32,8 @@ struct CodeBurnApp: App {
3232final class AppDelegate : NSObject , NSApplicationDelegate , NSPopoverDelegate {
3333 private var statusItem : NSStatusItem !
3434 private var popover : NSPopover !
35+ private var rightClickMonitor : Any ?
36+ private var lastContextMenuPresentedAt : Date = . distantPast
3537 fileprivate let store = AppStore ( )
3638 let updateChecker = UpdateChecker ( )
3739 /// Held for the lifetime of the app to opt out of App Nap and Automatic Termination.
@@ -50,6 +52,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
5052 private var codexQuotaRefreshTask : Task < Bool , Never > ?
5153 private var refreshLoopHeartbeatAt : Date = . distantPast
5254
55+ func applicationWillTerminate( _ notification: Notification ) {
56+ if let monitor = rightClickMonitor {
57+ NSEvent . removeMonitor ( monitor)
58+ rightClickMonitor = nil
59+ }
60+ }
61+
5362 func applicationWillFinishLaunching( _ notification: Notification ) {
5463 // Set accessory policy before the app's focus chain forms. On macOS Tahoe
5564 // (26.x), setting it after didFinishLaunching causes ghost status items
@@ -622,8 +631,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
622631
623632 button. target = self
624633 button. action = #selector( handleButtonClick ( _: ) )
634+ // Left-click drives the popover. We keep .rightMouseUp in the mask so the
635+ // legacy action path below still fires on macOS <= 26; on macOS 27 the
636+ // system consumes the right button entirely and this never fires, so the
637+ // global monitor (below) is what restores the right-click menu there.
625638 button. sendAction ( on: [ . leftMouseUp, . rightMouseUp] )
626639
640+ // macOS 27 no longer routes any right-mouse event to the status-item
641+ // button's target/action. A global monitor still observes right-mouse-down;
642+ // we hit-test it against our own status-item window and present the menu
643+ // ourselves. Harmless and stable on 15/26 too (the debounce in
644+ // showContextMenu prevents a double-present if the legacy path also fires).
645+ rightClickMonitor = NSEvent . addGlobalMonitorForEvents ( matching: [ . rightMouseDown] ) { [ weak self] _ in
646+ guard let self,
647+ let button = self . statusItem. button,
648+ let window = button. window,
649+ window. frame. contains ( NSEvent . mouseLocation) else { return }
650+ DispatchQueue . main. async { self . showContextMenu ( from: button) }
651+ }
652+
627653 // Defer the full attributed title setup to ensure initial render completes
628654 DispatchQueue . main. async { [ weak self] in
629655 self ? . refreshStatusButton ( )
@@ -783,6 +809,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
783809 guard let button = statusItem. button,
784810 let event = NSApp . currentEvent else { return }
785811
812+ // Legacy right-click path for macOS <= 26 (no-op on 27, where the action
813+ // never receives a right-mouse event — the global monitor handles it).
786814 if event. type == . rightMouseUp {
787815 showContextMenu ( from: button)
788816 return
@@ -816,6 +844,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
816844 }
817845
818846 private func showContextMenu( from button: NSStatusBarButton ) {
847+ // Debounce: on macOS <= 26 both the legacy action path and the global
848+ // monitor can fire for a single right-click. Present at most once per click.
849+ let now = Date ( )
850+ guard now. timeIntervalSince ( lastContextMenuPresentedAt) > 0.3 else { return }
851+ lastContextMenuPresentedAt = now
852+
819853 let menu = NSMenu ( )
820854
821855 let settingsItem = NSMenuItem ( title: " Settings… " , action: #selector( openSettings) , keyEquivalent: " , " )
@@ -835,9 +869,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
835869 quitItem. target = self
836870 menu. addItem ( quitItem)
837871
838- statusItem. menu = menu
839- button. performClick ( nil )
840- statusItem. menu = nil
872+ // Present directly. The previous `statusItem.menu = menu; button.performClick`
873+ // trick relies on the click -> action path that macOS 27 changed; popUp is
874+ // version-stable.
875+ menu. popUp ( positioning: nil , at: NSPoint ( x: 0 , y: button. bounds. height + 4 ) , in: button)
841876 }
842877
843878 private var settingsWindowController : NSWindowController ?
0 commit comments