Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 56 additions & 1 deletion Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Comment on lines +45 to +54

#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()
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down