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 @@ -15,6 +15,7 @@

### Fixed
- Menus: stop completed provider cards and plan-utilization rows from remaining in “Refreshing…” while unrelated provider or token-cost work is still running. Thanks @Yuxin-Qiao!
- Menu: keep native hover highlights aligned by deferring geometry-changing open-menu rebuilds until the pointer leaves the row. Thanks @Zihao-Qi!
- Codex accounts: isolate authenticated OAuth and browser-cookie requests from shared URL caches and cookie stores, preventing one account's cached quota or identity response from appearing under another account and triggering false reset alerts (#1987, #2019). Thanks @harjothkhara!
- Claude OAuth: honor the app's never-prompt policy in the bundled CLI and defer stale cache cleanup without touching Keychain until access is re-enabled. Thanks @Yuxin-Qiao!
- CLI server: retain timed-out route and provider work until it actually exits, preventing repeated requests or config changes from stacking background fetches. Thanks @Yuxin-Qiao!
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ extension StatusItemController {
let key = ObjectIdentifier(menu)
let previous = self.highlightedMenuItems[key]
guard previous !== item else { return }
let previousWasNative = self.isNativeMenuItemHighlighted(in: menu)

if let previous {
(previous.view as? MenuCardHighlighting)?.setHighlighted(false)
Expand All @@ -216,6 +217,10 @@ extension StatusItemController {
} else {
self.highlightedMenuItems.removeValue(forKey: key)
}

if previousWasNative, !self.isNativeMenuItemHighlighted(in: menu) {
self.resumeMenuRebuildDeferredForNativeHighlightIfNeeded(menu)
}
}

func populateMenu(_ menu: NSMenu, provider: UsageProvider?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ extension StatusItemController {
private struct ScheduledOpenMenuRebuild {
let provider: UsageProvider?
let shouldCloseHostedSubviewMenus: Bool
let resyncReadinessBaselineAfterRebuild: Bool
let beforeRebuild: (@MainActor () -> Bool)?
}

Expand Down Expand Up @@ -279,7 +278,6 @@ extension StatusItemController {
request: ScheduledOpenMenuRebuild(
provider: provider,
shouldCloseHostedSubviewMenus: true,
resyncReadinessBaselineAfterRebuild: false,
beforeRebuild: beforeRebuild))
}
}
Expand All @@ -293,6 +291,9 @@ extension StatusItemController {
beforeRebuild: (@MainActor () -> Bool)? = nil)
{
let key = ObjectIdentifier(menu)
if resyncReadinessBaselineAfterRebuild {
self.pendingMenuBaselineResyncs.insert(key)
}
if closeHostedSubviewMenusBeforeRebuild {
self.openMenuRebuildsClosingHostedSubviewMenus.insert(key)
}
Expand Down Expand Up @@ -321,7 +322,6 @@ extension StatusItemController {
request: ScheduledOpenMenuRebuild(
provider: provider,
shouldCloseHostedSubviewMenus: shouldCloseHostedSubviewMenus,
resyncReadinessBaselineAfterRebuild: resyncReadinessBaselineAfterRebuild,
beforeRebuild: beforeRebuild))
}
}
Expand All @@ -345,7 +345,8 @@ extension StatusItemController {
self.closeHostedSubviewMenusForParentSwitch()
}
self.rebuildOpenMenuIfStillVisible(menu, provider: request.provider)
if request.resyncReadinessBaselineAfterRebuild, !self.menuNeedsRefresh(menu) {
if self.pendingMenuBaselineResyncs.contains(key), !self.menuNeedsRefresh(menu) {
self.pendingMenuBaselineResyncs.remove(key)
self.resyncMenuAdjunctReadinessBaseline()
}
}
Expand Down
61 changes: 55 additions & 6 deletions Sources/CodexBar/StatusItemController+MenuTracking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,14 @@ extension StatusItemController {
self.openMenuRebuildTasks.removeValue(forKey: key)?.cancel()
self.openMenuRebuildRequests.cancel(for: key)
self.openMenuRebuildsClosingHostedSubviewMenus.remove(key)
self.pendingMenuBaselineResyncs.remove(key)
}

func clearMenuHighlight(_ key: ObjectIdentifier) {
if let highlightedView = self.highlightedMenuItems.removeValue(forKey: key)?.view {
(highlightedView as? MenuCardHighlighting)?.setHighlighted(false)
}
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
}

func removeMenuLifecycleState(_ key: ObjectIdentifier) {
Expand Down Expand Up @@ -394,16 +396,48 @@ extension StatusItemController {
func rebuildOpenMenuIfStillVisible(_ menu: NSMenu, provider: UsageProvider?) {
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.menuSession.clearParentRebuildDeferral(key)
self.applyIcon(phase: nil)
let isHostedSubviewMenu = self.isHostedSubviewMenu(menu)
guard isHostedSubviewMenu || !self.hasOpenHostedSubviewMenu() else { return }
guard !self.isNativeMenuItemHighlighted(in: menu) else {
self.nativeHighlightDeferredMenuRebuilds[key] = NativeHighlightDeferredMenuRebuild(provider: provider)
return
}
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
if isHostedSubviewMenu {
self.refreshHostedSubviewMenu(menu)
} else {
self.populateMenu(menu, provider: provider)
self.markMenuFresh(menu)
self.menuSession.clearParentRebuildDeferral(key)
self.applyIcon(phase: nil)
}
#if DEBUG
self._test_openMenuRebuildObserver?(menu)
#endif
}

func isNativeMenuItemHighlighted(in menu: NSMenu) -> Bool {
let key = ObjectIdentifier(menu)
guard let item = self.highlightedMenuItems[key], item.menu === menu else { return false }
return item.isEnabled && item.view == nil
}

func resumeMenuRebuildDeferredForNativeHighlightIfNeeded(_ menu: NSMenu) {
let key = ObjectIdentifier(menu)
guard let deferredRebuild = self.nativeHighlightDeferredMenuRebuilds[key] else { return }
guard self.openMenus[key] === menu else {
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
self.pendingMenuBaselineResyncs.remove(key)
return
}
let isHostedSubviewMenu = self.isHostedSubviewMenu(menu)
guard isHostedSubviewMenu || !self.hasOpenHostedSubviewMenu() else { return }
Comment thread
steipete marked this conversation as resolved.
self.nativeHighlightDeferredMenuRebuilds.removeValue(forKey: key)
self.scheduleOpenMenuRebuildIfStillVisible(
menu,
provider: deferredRebuild.provider)
}

func refreshOpenMenusIfNeeded() {
guard self.isMenuRefreshEnabled else { return }
guard !self.openMenus.isEmpty else { return }
Expand All @@ -423,6 +457,21 @@ extension StatusItemController {
}
self.parentMenuRebuildPendingAfterHostedSubviewClose = false
self.refreshOpenMenusIfNeeded(allowsParentRebuild: true)
self.resumeParentMenuRebuildsDeferredForNativeHighlightAfterHostedSubviewClose()
}

private func resumeParentMenuRebuildsDeferredForNativeHighlightAfterHostedSubviewClose() {
guard !self.hasOpenHostedSubviewMenu() else { return }
let deferredParents = self.openMenus.values.filter { menu in
let key = ObjectIdentifier(menu)
return !self.isHostedSubviewMenu(menu) &&
self.nativeHighlightDeferredMenuRebuilds[key] != nil
}
// Schedule the saved explicit request after the generic dirty-menu pass, even when the native
// highlight is still active. The scheduled rebuild will defer again, preserving its provider.
for menu in deferredParents {
self.resumeMenuRebuildDeferredForNativeHighlightIfNeeded(menu)
}
}

func completeParentMenuRebuildAfterHostedSubviewCloseIfNeeded() {
Expand Down Expand Up @@ -485,7 +534,7 @@ extension StatusItemController {
hasOpenHostedSubviewMenu: Bool)
{
if self.isHostedSubviewMenu(menu) {
self.refreshHostedSubviewMenu(menu)
self.scheduleOpenMenuRebuildIfStillVisible(menu, provider: self.menuProvider(for: menu))
return
}
guard allowsParentRebuild else { return }
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/StatusItemController+Shutdown.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ extension StatusItemController {
self.menuSession.clearMenuTracking()
self.openMenus.removeAll(keepingCapacity: false)
self.highlightedMenuItems.removeAll(keepingCapacity: false)
self.nativeHighlightDeferredMenuRebuilds.removeAll(keepingCapacity: false)
self.pendingMenuBaselineResyncs.removeAll(keepingCapacity: false)
self.menuCardHeightCache.removeAll(keepingCapacity: false)
self.measuredStandardMenuWidthCache.removeAll(keepingCapacity: false)
self.mergedSwitcherContentCaches.removeAll(keepingCapacity: false)
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ extension StatusItemControlling {
func prepareForAppShutdown() {}
}

struct NativeHighlightDeferredMenuRebuild {
let provider: UsageProvider?
}

@MainActor
final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControlling {
// Disable SwiftUI menu cards + menu refresh work in tests to avoid swiftpm-testing-helper crashes.
Expand Down Expand Up @@ -168,6 +172,10 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
var deferredOpenAIDashboardRefreshReason: String?
var deferredMenuInteractionRefreshTask: Task<Void, Never>?
var highlightedMenuItems: [ObjectIdentifier: NSMenuItem] = [:]
/// Open-menu rebuilds paused so AppKit's native selection background cannot retain stale geometry.
var nativeHighlightDeferredMenuRebuilds: [ObjectIdentifier: NativeHighlightDeferredMenuRebuild] = [:]
/// Baseline resync intent survives rebuild coalescing and any native-row or hosted-submenu deferral.
var pendingMenuBaselineResyncs: Set<ObjectIdentifier> = []
var providerSwitcherShortcutEventMonitor: ProviderSwitcherShortcutEventMonitor?
var providerSwitcherShortcutMenuID: ObjectIdentifier?
var providerSwitcherPointerInteractionMenuID: ObjectIdentifier?
Expand Down
4 changes: 4 additions & 0 deletions Tests/CodexBarTests/StatusItemControllerShutdownTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ struct StatusItemControllerShutdownTests {
controller.menuRefreshTasks[key] = Task { try? await Task.sleep(for: .seconds(30)) }
controller.menuReadinessSignatures[key] = "readiness"
controller.menuIdentitySignatures[key] = "identity"
controller.nativeHighlightDeferredMenuRebuilds[key] = .init(provider: .codex)
controller.pendingMenuBaselineResyncs.insert(key)

#expect(controller.openMenus[key] === menu)
#expect(controller.mergedMenu != nil)
Expand All @@ -56,6 +58,8 @@ struct StatusItemControllerShutdownTests {
#expect(controller.menuRefreshTasks.isEmpty)
#expect(controller.menuReadinessSignatures.isEmpty)
#expect(controller.menuIdentitySignatures.isEmpty)
#expect(controller.nativeHighlightDeferredMenuRebuilds.isEmpty)
#expect(controller.pendingMenuBaselineResyncs.isEmpty)
#expect(controller.providerSwitcherShortcutEventMonitor == nil)
#expect(controller.statusItem.menu == nil)
#expect(controller.statusItems.isEmpty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ struct StatusItemControllerSplitLifecycleTests {
_ = controller.openMenuRebuildRequests.replaceRequest(for: key)
controller.openMenuRebuildsClosingHostedSubviewMenus.insert(key)
controller.highlightedMenuItems[key] = NSMenuItem(title: "Highlighted", action: nil, keyEquivalent: "")
controller.nativeHighlightDeferredMenuRebuilds[key] = .init(provider: .codex)
controller.pendingMenuBaselineResyncs.insert(key)
}

settings.mergeIcons = true
Expand All @@ -168,6 +170,8 @@ struct StatusItemControllerSplitLifecycleTests {
#expect(controller.openMenuRebuildRequests.tokens[key] == nil)
#expect(!controller.openMenuRebuildsClosingHostedSubviewMenus.contains(key))
#expect(controller.highlightedMenuItems[key] == nil)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
#expect(!controller.pendingMenuBaselineResyncs.contains(key))
}
}

Expand Down
Loading