Fix menubar ghost status item on macOS Tahoe

Set accessory activation policy in willFinishLaunching before the
focus chain forms. Debounce observation tracking to coalesce rapid
property changes into a single status bar refresh.
This commit is contained in:
Resham Joshi 2026-05-02 10:14:14 -07:00 committed by GitHub
parent 3ea4a62408
commit db993196cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -30,14 +30,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
let updateChecker = UpdateChecker()
/// Held for the lifetime of the app to opt out of App Nap and Automatic Termination.
private var backgroundActivity: NSObjectProtocol?
private var pendingRefreshWork: DispatchWorkItem?
func applicationWillFinishLaunching(_ notification: Notification) {
// Set accessory policy before the app's focus chain forms. On macOS Tahoe
// (26.x), setting it after didFinishLaunching causes ghost status items
// because the policy gets baked into the initial focus chain.
NSApp.setActivationPolicy(.accessory)
}
func applicationDidFinishLaunching(_ notification: Notification) {
// On macOS Tahoe (26.x), accessory apps may fail to render their status item
// if the activation policy is set before the status item is created. Starting
// as a regular app and switching to accessory after setup works around this.
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
ProcessInfo.processInfo.automaticTerminationSupportEnabled = false
ProcessInfo.processInfo.disableSuddenTermination()
backgroundActivity = ProcessInfo.processInfo.beginActivity(
@ -48,11 +50,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
restorePersistedCurrency()
setupStatusItem()
setupPopover()
// Switch to accessory policy after status item is set up to hide from Dock
DispatchQueue.main.async {
NSApp.setActivationPolicy(.accessory)
}
observeStore()
startRefreshLoop()
setupWakeObservers()
@ -222,9 +219,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
_ = store.payload
_ = store.todayPayload
} onChange: { [weak self] in
Task { @MainActor in
self?.refreshStatusButton()
self?.observeStore()
DispatchQueue.main.async {
guard let self else { return }
self.pendingRefreshWork?.cancel()
let work = DispatchWorkItem { [weak self] in
self?.refreshStatusButton()
self?.observeStore()
}
self.pendingRefreshWork = work
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: work)
}
}
}