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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixed
- Antigravity: exclude model quotas without a remaining fraction from family summaries so they no longer mask tracked usage in the automatic menu-bar metric (#1369). Thanks @Martin-Hausleitner!
- Claude: add bundled Fable 5 pricing, account for native 1-hour cache-write usage, and refresh Sonnet 4.6 full-context rates (#1368). Thanks @MoollaMore!
- Menu bar: defer data-refresh rebuilds until the tracked menu closes, avoiding multi-second WindowServer stalls with slower providers such as Grok (#1376). Thanks @jangisaac-dev!

## 0.32.5 — 2026-06-09

Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBar/StatusItemController+Actions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate {
}

func refreshOpenMenusAfterExplicitStoreAction() {
self.invalidateMenus(refreshOpenMenus: true)
self.invalidateMenus(
refreshOpenMenus: true,
deferOpenParentMenuRebuild: true,
allowStaleContentDuringDataRefresh: true)
}

@objc func refreshNow() {
Expand Down
15 changes: 13 additions & 2 deletions Sources/CodexBar/StatusItemController+MenuTracking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,25 @@ extension StatusItemController {
}

func refreshOpenMenuIfStillVisible(_ menu: NSMenu, provider: UsageProvider?) {
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: provider)
let key = ObjectIdentifier(menu)
guard self.openMenus[key] != nil else { return }
if self.isHostedSubviewMenu(menu) {
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: provider)
return
}
self.invalidateMenus(
refreshOpenMenus: true,
deferOpenParentMenuRebuild: true,
allowStaleContentDuringDataRefresh: true)
Comment on lines +249 to +252

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the menu after account-switch fetches finish

When a user switches a token/Codex account while the menu is open, the callers in StatusItemController+Menu.swift await the provider/account refresh and then call refreshOpenMenuIfStillVisible to update the visible menu with the newly fetched account-scoped data. This path now only invalidates with deferOpenParentMenuRebuild: true, which refreshOpenMenuIfNeeded handles by recording a deferral and returning, so the open parent menu can stay on the stale/intermediate account state until the user closes and reopens it instead of showing the completed refresh result.

Useful? React with 👍 / 👎.

}

func rebuildOpenMenuIfStillVisible(_ menu: NSMenu, provider: UsageProvider?) {
guard self.openMenus[ObjectIdentifier(menu)] != nil else { return }
let key = ObjectIdentifier(menu)
guard self.openMenus[key] != nil else { return }
guard self.isHostedSubviewMenu(menu) || !self.hasOpenHostedSubviewMenu() else { return }
self.populateMenu(menu, provider: provider)
self.markMenuFresh(menu)
self.parentMenuRebuildsDeferredDuringTracking.remove(key)
self.applyIcon(phase: nil)
#if DEBUG
self._test_openMenuRebuildObserver?(menu)
Expand Down
20 changes: 18 additions & 2 deletions Sources/CodexBar/StatusItemController+ProviderNavigation.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AppKit
import CodexBarCore

extension StatusItemController {
Expand Down Expand Up @@ -34,7 +35,10 @@ extension StatusItemController {
self.applyIcon(phase: phase)
}

func navigateProviderSwitcher(_ direction: StatusItemMenuProviderNavigationDirection) {
func navigateProviderSwitcher(
_ direction: StatusItemMenuProviderNavigationDirection,
menu: NSMenu? = nil)
{
guard self.shouldMergeIcons else { return }
let enabledProviders = self.store.enabledProvidersForDisplay()
guard enabledProviders.count > 1 else { return }
Expand All @@ -59,6 +63,12 @@ extension StatusItemController {
let delta = direction == .next ? 1 : -1
let nextIndex = (currentIndex + delta + selections.count) % selections.count
let selection = selections[nextIndex]
let menuProvider: UsageProvider = switch selection {
case .overview:
self.navigationResolvedProvider(enabledProviders: enabledProviders) ?? .codex
case let .provider(provider):
provider
}
self.preservingMergedSwitcherContentCachesDuringInvalidation {
switch selection {
case .overview:
Expand All @@ -70,7 +80,13 @@ extension StatusItemController {
self.lastMenuProvider = provider
}
self.lastMergedSwitcherSelection = selection
self.refreshProviderSelectionDependentUI(refreshOpenMenus: true, deferRendering: true)
self.refreshProviderSelectionDependentUI(deferRendering: true)
}
let trackedMenu = menu ?? self.providerSwitcherShortcutMenuID.flatMap { self.openMenus[$0] }
if let trackedMenu {
self.requestProviderSwitcherMenuRebuild(
trackedMenu,
provider: menuProvider)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ extension StatusItemController {
return self.selectProviderSwitcherSegment(at: index, menu: menu)
}
if let direction = StatusItemMenu.providerNavigationDirection(for: event) {
self.navigateProviderSwitcher(direction)
self.navigateProviderSwitcher(direction, menu: menu)
return true
}
return false
Expand Down
27 changes: 15 additions & 12 deletions Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ extension StatusMenuTests {
}

@Test
func `explicit store actions refresh a visible open menu`() async {
func `explicit store actions defer visible parent menu rebuild`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
Expand Down Expand Up @@ -660,17 +660,18 @@ extension StatusMenuTests {
defer { controller._test_openMenuRebuildObserver = nil }

controller.refreshOpenMenusAfterExplicitStoreAction()
for _ in 0..<20 where rebuildCount == 0 {
for _ in 0..<20 {
await Task.yield()
}

#expect(controller.menuContentVersion != openedVersion)
#expect(rebuildCount == 1)
#expect(controller.menuVersions[key] != openedVersion)
#expect(rebuildCount == 0)
#expect(controller.menuVersions[key] == openedVersion)
#expect(controller.parentMenuRebuildsDeferredDuringTracking.contains(key))
}

@Test
func `repeated explicit store actions coalesce to one open menu rebuild`() async {
func `repeated explicit store actions keep parent rebuild deferred`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
Expand Down Expand Up @@ -703,16 +704,17 @@ extension StatusMenuTests {
controller.refreshOpenMenusAfterExplicitStoreAction()
controller.refreshOpenMenusAfterExplicitStoreAction()

for _ in 0..<20 where rebuildCount == 0 {
for _ in 0..<20 {
await Task.yield()
}

#expect(rebuildCount == 1)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
#expect(rebuildCount == 0)
#expect(controller.menuVersions[key] != controller.menuContentVersion)
#expect(controller.parentMenuRebuildsDeferredDuringTracking.contains(key))
}

@Test
func `explicit refresh rebuilds stale parent after hosted submenu closes`() async {
func `explicit refresh keeps stale parent deferred after hosted submenu closes`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
Expand Down Expand Up @@ -755,13 +757,14 @@ extension StatusMenuTests {
#expect(controller.menuVersions[menuKey] == openedVersion)

controller.menuDidClose(submenu)
for _ in 0..<20 where rebuildCount == 0 {
for _ in 0..<20 {
await Task.yield()
}

#expect(controller.openMenus[submenuKey] == nil)
#expect(rebuildCount == 1)
#expect(controller.menuVersions[menuKey] == controller.menuContentVersion)
#expect(rebuildCount == 0)
#expect(controller.menuVersions[menuKey] == openedVersion)
#expect(controller.parentMenuRebuildsDeferredDuringTracking.contains(menuKey))
}

@Test
Expand Down
21 changes: 19 additions & 2 deletions Tests/CodexBarTests/StatusMenuSwitcherClickTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -494,20 +494,37 @@ struct StatusMenuSwitcherClickTests {
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
controller.menuRefreshEnabledOverrideForTesting = true
defer { controller.releaseStatusItemsForTesting() }

let menu = try #require(controller.makeMenu() as? StatusItemMenu)
controller.menuWillOpen(menu)
#expect(menu.items.first?.view is ProviderSwitcherView)
store.tokenRefreshInFlight.insert(.codex)
defer { store.tokenRefreshInFlight.remove(.codex) }
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }

#expect(try menu.performKeyEquivalent(with: Self.arrowKeyEvent(keyCode: 124)) == true)
await Task.yield()
for _ in 0..<100 where rebuildCount == 0 {
await Task.yield()
try? await Task.sleep(for: .milliseconds(5))
}
#expect(settings.mergedMenuLastSelectedWasOverview == false)
#expect(settings.selectedMenuProvider == .claude)
#expect(rebuildCount == 1)

#expect(try menu.performKeyEquivalent(with: Self.arrowKeyEvent(keyCode: 123)) == true)
await Task.yield()
for _ in 0..<100 where rebuildCount == 1 {
await Task.yield()
try? await Task.sleep(for: .milliseconds(5))
}
#expect(settings.mergedMenuLastSelectedWasOverview == false)
#expect(settings.selectedMenuProvider == .codex)
#expect(rebuildCount == 2)
}

@Test
Expand Down