Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
46 changes: 40 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 Down Expand Up @@ -485,7 +519,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