diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 02efb26555..225331c5b2 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1109,9 +1109,13 @@ extension StatusItemController { } private func scheduleOpenMenuRefresh(for menu: NSMenu) { - // Queue refresh work until the menu closes. AppKit menu tracking is modal; starting provider refreshes - // while it is active can make the menu feel frozen and can block keyboard focus from returning. - self.deferMenuInteractionRefreshIfNeeded() + // Queue refresh work only when visible menu data is missing or stale. Here "stale" means the last + // provider fetch failed and needs a retry; periodic freshness is handled by the refresh timer. + // AppKit menu tracking is modal, so starting provider refreshes while it is active can make the menu + // feel frozen and can block keyboard focus from returning. + if self.menuNeedsDelayedRefreshRetry(for: menu) { + self.deferMenuInteractionRefreshIfNeeded() + } let key = ObjectIdentifier(menu) self.menuRefreshTasks[key]?.cancel() self.menuRefreshTasks[key] = Task { @MainActor [weak self, weak menu] in diff --git a/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift b/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift index 2f3855c22a..3d622dd3d0 100644 --- a/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift @@ -6,7 +6,7 @@ import Testing extension StatusMenuTests { @Test - func `menu open defers automatic refresh until tracking ends`() async { + func `opening fresh menu does not schedule deferred refresh`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false @@ -42,6 +42,59 @@ extension StatusMenuTests { let menu = controller.makeMenu() controller.menuWillOpen(menu) + for _ in 0..<20 { + await Task.yield() + } + #expect(providerRefreshCount == 0) + #expect(!controller.deferredMenuInteractionRefreshPending) + + controller.menuDidClose(menu) + for _ in 0..<40 { + await Task.yield() + } + + #expect(providerRefreshCount == 0) + #expect(refreshInteractions.isEmpty) + } + + @Test + func `menu open with missing data defers automatic refresh until tracking ends`() async { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = false + self.enableOnlyCodex(settings) + + let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) + store._setSnapshotForTesting(nil, provider: .codex) + var providerRefreshCount = 0 + var refreshInteractions: [ProviderInteraction] = [] + store._test_providerRefreshOverride = { provider in + guard provider == .codex else { return } + refreshInteractions.append(ProviderInteractionContext.current) + providerRefreshCount += 1 + } + defer { store._test_providerRefreshOverride = nil } + + let controller = StatusItemController( + store: store, + settings: settings, + account: UsageFetcher().loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + StatusItemController.setClosedMenuPreparationDelayForTesting(.zero) + defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() } + + controller.menuRefreshEnabledOverrideForTesting = true + StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero) + defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } + + let menu = controller.makeMenu() + controller.menuWillOpen(menu) + for _ in 0..<20 { await Task.yield() } @@ -776,6 +829,7 @@ extension StatusMenuTests { self.enableOnlyCodex(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) + store._setSnapshotForTesting(nil, provider: .codex) store.openAIDashboard = nil store.lastOpenAIDashboardSnapshot = nil let providerBlocker = BlockingStatusMenuProviderRefresh() @@ -832,6 +886,7 @@ extension StatusMenuTests { self.enableOnlyCodex(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) + store._setSnapshotForTesting(nil, provider: .codex) store.openAIDashboard = nil store.lastOpenAIDashboardSnapshot = nil let providerBlocker = BlockingStatusMenuProviderRefresh()