import AppKitimport CodexBarCoreimport Observationimport QuartzCoreimport SwiftUI // MARK: - NSMenu construction extension StatusItemController { private static let menuCardBaseWidth: CGFloat = 310 private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit private static let overviewRowIdentifierPrefix = "overviewRow-" private static let menuOpenRefreshDelay: Duration = .seconds(1.2) static let usageBreakdownChartID = "usageBreakdownChart" static let creditsHistoryChartID = "creditsHistoryChart" static let costHistoryChartID = "costHistoryChart" static let usageHistoryChartID = "usageHistoryChart" private func menuCardWidth(for providers: [UsageProvider], menu: NSMenu? = nil) -> CGFloat { _ = menu return Self.menuCardBaseWidth } func makeMenu() -> NSMenu { guard self.shouldMergeIcons else { return self.makeMenu(for: nil) } let menu = NSMenu() menu.autoenablesItems = false menu.delegate = self return menu } func menuWillOpen(_ menu: NSMenu) { if self.isHostedSubviewMenu(menu) { self.hydrateHostedSubviewMenuIfNeeded(menu) self.refreshHostedSubviewHeights(in: menu) if Self.menuRefreshEnabled, self.isOpenAIWebSubviewMenu(menu) { self.store.requestOpenAIDashboardRefreshIfStale(reason: "submenu open") } self.openMenus[ObjectIdentifier(menu)] = menu // Removed redundant async refresh - single pass is sufficient after initial layout return } var provider: UsageProvider? if self.shouldMergeIcons { let resolvedProvider = self.resolvedMenuProvider() self.lastMenuProvider = resolvedProvider ?? .codex provider = resolvedProvider } else { if let menuProvider = self.menuProviders[ObjectIdentifier(menu)] { self.lastMenuProvider = menuProvider provider = menuProvider } else if menu === self.fallbackMenu { self.lastMenuProvider = self.store.enabledProvidersForDisplay().first ?? .codex provider = nil } else { let resolved = self.store.enabledProvidersForDisplay().first ?? .codex self.lastMenuProvider = resolved provider = resolved } } let didRefresh = self.menuNeedsRefresh(menu) if didRefresh { self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) // Heights are already set during populateMenu, no need to remeasure } self.openMenus[ObjectIdentifier(menu)] = menu // Only schedule refresh after menu is registered as open - refreshNow is called async if Self.menuRefreshEnabled { self.scheduleOpenMenuRefresh(for: menu) } } func menuDidClose(_ menu: NSMenu) { let key = ObjectIdentifier(menu) self.openMenus.removeValue(forKey: key) self.menuRefreshTasks.removeValue(forKey: key)?.cancel() let isPersistentMenu = menu === self.mergedMenu || menu === self.fallbackMenu || self.providerMenus.values.contains { $0 === menu } if !isPersistentMenu { self.menuProviders.removeValue(forKey: key) self.menuVersions.removeValue(forKey: key) } for menuItem in menu.items { (menuItem.view as? MenuCardHighlighting)?.setHighlighted(false) } } func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) { for menuItem in menu.items { let highlighted = menuItem == item && menuItem.isEnabled (menuItem.view as? MenuCardHighlighting)?.setHighlighted(highlighted) } } private func populateMenu(_ menu: NSMenu, provider: UsageProvider?) { let enabledProviders = self.store.enabledProvidersForDisplay() let includesOverview = self.includesOverviewTab(enabledProviders: enabledProviders) let switcherSelection = self.shouldMergeIcons && enabledProviders.count > 1 ? self.resolvedSwitcherSelection( enabledProviders: enabledProviders, includesOverview: includesOverview) : nil let isOverviewSelected = switcherSelection == .overview let selectedProvider = if isOverviewSelected { self.resolvedMenuProvider(enabledProviders: enabledProviders) } else { switcherSelection?.provider ?? provider } let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu) let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex let codexAccountDisplay = isOverviewSelected ? nil : self.codexAccountMenuDisplay(for: currentProvider) let tokenAccountDisplay = isOverviewSelected ? nil : self.tokenAccountMenuDisplay(for: currentProvider) let showAllTokenAccounts = tokenAccountDisplay?.showAll ?? false let openAIContext = self.openAIWebContext( currentProvider: currentProvider, showAllTokenAccounts: showAllTokenAccounts) let hasTokenSwitcher = menu.items.contains { $0.view is TokenAccountSwitcherView } let hasCodexSwitcher = menu.items.contains { $0.view is CodexAccountSwitcherView } let switcherProvidersMatch = enabledProviders == self.lastSwitcherProviders let switcherUsageBarsShowUsedMatch = self.settings.usageBarsShowUsed == self.lastSwitcherUsageBarsShowUsed let switcherSelectionMatches = switcherSelection == self.lastMergedSwitcherSelection let switcherOverviewAvailabilityMatches = includesOverview == self.lastSwitcherIncludesOverview let tokenSwitcherCompatible = tokenAccountDisplay == nil && !hasTokenSwitcher let codexSwitcherCompatible = codexAccountDisplay == self.lastCodexAccountMenuDisplay && ((codexAccountDisplay == nil && !hasCodexSwitcher) || (codexAccountDisplay != nil && hasCodexSwitcher)) let canSmartUpdate = self.shouldMergeIcons && enabledProviders.count > 1 && !isOverviewSelected && switcherProvidersMatch && switcherUsageBarsShowUsedMatch && switcherSelectionMatches && switcherOverviewAvailabilityMatches && tokenSwitcherCompatible && codexSwitcherCompatible && !menu.items.isEmpty && menu.items.first?.view is ProviderSwitcherView if canSmartUpdate { self.updateMenuContent( menu, provider: selectedProvider, currentProvider: currentProvider, menuWidth: menuWidth, openAIContext: openAIContext) return } menu.removeAllItems() let descriptor = MenuDescriptor.build( provider: selectedProvider, store: self.store, settings: self.settings, account: self.account, managedCodexAccountCoordinator: self.managedCodexAccountCoordinator, codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator, updateReady: self.updater.updateStatus.isUpdateReady, includeContextualActions: !isOverviewSelected) self.addProviderSwitcherIfNeeded( to: menu, enabledProviders: enabledProviders, includesOverview: includesOverview, selection: switcherSelection ?? .provider(currentProvider)) // Track which providers the switcher was built with for smart update detection if self.shouldMergeIcons, enabledProviders.count > 1 { self.lastSwitcherProviders = enabledProviders self.lastSwitcherUsageBarsShowUsed = self.settings.usageBarsShowUsed self.lastMergedSwitcherSelection = switcherSelection self.lastSwitcherIncludesOverview = includesOverview } self.addCodexAccountSwitcherIfNeeded(to: menu, display: codexAccountDisplay) self.lastCodexAccountMenuDisplay = codexAccountDisplay self.addTokenAccountSwitcherIfNeeded(to: menu, display: tokenAccountDisplay) let menuContext = MenuCardContext( currentProvider: currentProvider, selectedProvider: selectedProvider, menuWidth: menuWidth, tokenAccountDisplay: tokenAccountDisplay, openAIContext: openAIContext) if isOverviewSelected { if self.addOverviewRows( to: menu, enabledProviders: enabledProviders, menuWidth: menuWidth) { menu.addItem(.separator()) } else { self.addOverviewEmptyState(to: menu, enabledProviders: enabledProviders) menu.addItem(.separator()) } } else { let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext) self.addOpenAIWebItemsIfNeeded( to: menu, currentProvider: currentProvider, context: openAIContext, addedOpenAIWebItems: addedOpenAIWebItems) if self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider) { menu.addItem(.separator()) } } self.addActionableSections(descriptor.sections, to: menu, width: menuWidth) } /// Smart update: only rebuild content sections when switching providers (keep the switcher intact). private func updateMenuContent( _ menu: NSMenu, provider: UsageProvider?, currentProvider: UsageProvider, menuWidth: CGFloat, openAIContext: OpenAIWebContext) { // Batch menu updates to prevent visual flickering during provider switch. CATransaction.begin() CATransaction.setDisableActions(true) defer { CATransaction.commit() } var contentStartIndex = 0 if menu.items.first?.view is ProviderSwitcherView { contentStartIndex = 2 } if menu.items.count > contentStartIndex, menu.items[contentStartIndex].view is CodexAccountSwitcherView { contentStartIndex += 2 } if menu.items.count > contentStartIndex, menu.items[contentStartIndex].view is TokenAccountSwitcherView { contentStartIndex += 2 } while menu.items.count > contentStartIndex { menu.removeItem(at: contentStartIndex) } let descriptor = MenuDescriptor.build( provider: provider, store: self.store, settings: self.settings, account: self.account, managedCodexAccountCoordinator: self.managedCodexAccountCoordinator, codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator, updateReady: self.updater.updateStatus.isUpdateReady) let menuContext = MenuCardContext( currentProvider: currentProvider, selectedProvider: provider, menuWidth: menuWidth, tokenAccountDisplay: nil, openAIContext: openAIContext) let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext) self.addOpenAIWebItemsIfNeeded( to: menu, currentProvider: currentProvider, context: openAIContext, addedOpenAIWebItems: addedOpenAIWebItems) if self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider) { menu.addItem(.separator()) } self.addActionableSections(descriptor.sections, to: menu, width: menuWidth) } private struct OpenAIWebContext { let hasUsageBreakdown: Bool let hasCreditsHistory: Bool let hasCostHistory: Bool let canShowBuyCredits: Bool let hasOpenAIWebMenuItems: Bool } private struct MenuCardContext { let currentProvider: UsageProvider let selectedProvider: UsageProvider? let menuWidth: CGFloat let tokenAccountDisplay: TokenAccountMenuDisplay? let openAIContext: OpenAIWebContext } private func openAIWebContext( currentProvider: UsageProvider, showAllTokenAccounts: Bool) -> OpenAIWebContext { let codexProjection = self.store.codexConsumerProjectionIfNeeded( for: currentProvider, surface: .liveCard) let hasCreditsHistory = codexProjection?.hasCreditsHistory == true let hasUsageBreakdown = codexProjection?.hasUsageBreakdown == true let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) && (self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false) let canShowBuyCredits = self.settings.showOptionalCreditsAndExtraUsage && codexProjection?.canShowBuyCredits == true let hasOpenAIWebMenuItems = !showAllTokenAccounts && (hasCreditsHistory || hasUsageBreakdown || hasCostHistory) return OpenAIWebContext( hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, hasCostHistory: hasCostHistory, canShowBuyCredits: canShowBuyCredits, hasOpenAIWebMenuItems: hasOpenAIWebMenuItems) } private func addProviderSwitcherIfNeeded( to menu: NSMenu, enabledProviders: [UsageProvider], includesOverview: Bool, selection: ProviderSwitcherSelection) { guard self.shouldMergeIcons, enabledProviders.count > 1 else { return } let switcherItem = self.makeProviderSwitcherItem( providers: enabledProviders, includesOverview: includesOverview, selected: selection, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } private func addTokenAccountSwitcherIfNeeded(to menu: NSMenu, display: TokenAccountMenuDisplay?) { guard let display, display.showSwitcher else { return } let switcherItem = self.makeTokenAccountSwitcherItem(display: display, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } private func addCodexAccountSwitcherIfNeeded(to menu: NSMenu, display: CodexAccountMenuDisplay?) { guard let display else { return } let switcherItem = self.makeCodexAccountSwitcherItem(display: display, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } @discardableResult private func addOverviewRows( to menu: NSMenu, enabledProviders: [UsageProvider], menuWidth: CGFloat) -> Bool { let overviewProviders = self.settings.reconcileMergedOverviewSelectedProviders( activeProviders: enabledProviders) let rows: [(provider: UsageProvider, model: UsageMenuCardView.Model)] = overviewProviders .compactMap { provider in guard let model = self.menuCardModel(for: provider) else { return nil } return (provider: provider, model: model) } guard !rows.isEmpty else { return false } for (index, row) in rows.enumerated() { let identifier = "\(Self.overviewRowIdentifierPrefix)\(row.provider.rawValue)" let item = self.makeMenuCardItem( OverviewMenuCardRowView(model: row.model, width: menuWidth), id: identifier, width: menuWidth, onClick: { [weak self, weak menu] in guard let self, let menu else { return } self.selectOverviewProvider(row.provider, menu: menu) }) // Keep menu item action wired for keyboard activation and accessibility action paths. item.target = self item.action = #selector(self.selectOverviewProvider(_:)) menu.addItem(item) if index < rows.count - 1 { menu.addItem(.separator()) } } return true } private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { let resolvedProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, maxVisibleProviders: Self.maxOverviewProviders) let message = if resolvedProviders.isEmpty { "No providers selected for Overview." } else { "No overview data available." } let item = NSMenuItem(title: message, action: nil, keyEquivalent: "") item.isEnabled = false item.representedObject = "overviewEmptyState" menu.addItem(item) } private func addMenuCards(to menu: NSMenu, context: MenuCardContext) -> Bool { if let tokenAccountDisplay = context.tokenAccountDisplay, tokenAccountDisplay.showAll { let accountSnapshots = tokenAccountDisplay.snapshots let cards = accountSnapshots.isEmpty ? [] : accountSnapshots.compactMap { accountSnapshot in self.menuCardModel( for: context.currentProvider, snapshotOverride: accountSnapshot.snapshot, errorOverride: accountSnapshot.error) } if cards.isEmpty, let model = self.menuCardModel(for: context.selectedProvider) { menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: context.menuWidth), id: "menuCard", width: context.menuWidth)) menu.addItem(.separator()) } else { for (index, model) in cards.enumerated() { menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: context.menuWidth), id: "menuCard-\(index)", width: context.menuWidth)) if index < cards.count - 1 { menu.addItem(.separator()) } } if !cards.isEmpty { menu.addItem(.separator()) } } return false } guard let model = self.menuCardModel(for: context.selectedProvider) else { return false } if context.openAIContext.hasOpenAIWebMenuItems { let webItems = OpenAIWebMenuItems( hasUsageBreakdown: context.openAIContext.hasUsageBreakdown, hasCreditsHistory: context.openAIContext.hasCreditsHistory, hasCostHistory: context.openAIContext.hasCostHistory, canShowBuyCredits: context.openAIContext.canShowBuyCredits) self.addMenuCardSections( to: menu, model: model, provider: context.currentProvider, width: context.menuWidth, webItems: webItems) return true } menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: context.menuWidth), id: "menuCard", width: context.menuWidth)) if context.openAIContext.canShowBuyCredits { menu.addItem(self.makeBuyCreditsItem()) } menu.addItem(.separator()) return false } private func addOpenAIWebItemsIfNeeded( to menu: NSMenu, currentProvider: UsageProvider, context: OpenAIWebContext, addedOpenAIWebItems: Bool) { guard context.hasOpenAIWebMenuItems else { return } if !addedOpenAIWebItems { // Only show these when we actually have additional data. if context.hasUsageBreakdown { _ = self.addUsageBreakdownSubmenu(to: menu) } if context.hasCreditsHistory { _ = self.addCreditsHistorySubmenu(to: menu) } if context.hasCostHistory { _ = self.addCostHistorySubmenu(to: menu, provider: currentProvider) } } menu.addItem(.separator()) } private func addActionableSections(_ sections: [MenuDescriptor.Section], to menu: NSMenu, width: CGFloat) { let actionableSections = sections.filter { section in section.entries.contains { entry in if case .action = entry { return true } if case .submenu = entry { return true } return false } } for (index, section) in actionableSections.enumerated() { for entry in section.entries { switch entry { case let .text(text, style): if style == .secondary { menu.addItem(self.makeWrappedSecondaryTextItem(text: text, width: width)) continue } let item = NSMenuItem(title: text, action: nil, keyEquivalent: "") item.isEnabled = false if style == .headline { let font = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold) item.attributedTitle = NSAttributedString(string: text, attributes: [.font: font]) } else if style == .secondary { let font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) item.attributedTitle = NSAttributedString( string: text,Lines 1–500 of 1409. Load more
import AppKit import CodexBarCore import Observation import QuartzCore import SwiftUI // MARK: - NSMenu construction extension StatusItemController { private static let menuCardBaseWidth: CGFloat = 310 private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit private static let overviewRowIdentifierPrefix = "overviewRow-" private static let menuOpenRefreshDelay: Duration = .seconds(1.2) static let usageBreakdownChartID = "usageBreakdownChart" static let creditsHistoryChartID = "creditsHistoryChart" static let costHistoryChartID = "costHistoryChart" static let usageHistoryChartID = "usageHistoryChart" private func menuCardWidth(for providers: [UsageProvider], menu: NSMenu? = nil) -> CGFloat { _ = menu return Self.menuCardBaseWidth } func makeMenu() -> NSMenu { guard self.shouldMergeIcons else { return self.makeMenu(for: nil) } let menu = NSMenu() menu.autoenablesItems = false menu.delegate = self return menu } func menuWillOpen(_ menu: NSMenu) { if self.isHostedSubviewMenu(menu) { self.hydrateHostedSubviewMenuIfNeeded(menu) self.refreshHostedSubviewHeights(in: menu) if Self.menuRefreshEnabled, self.isOpenAIWebSubviewMenu(menu) { self.store.requestOpenAIDashboardRefreshIfStale(reason: "submenu open") } self.openMenus[ObjectIdentifier(menu)] = menu // Removed redundant async refresh - single pass is sufficient after initial layout return } var provider: UsageProvider? if self.shouldMergeIcons { let resolvedProvider = self.resolvedMenuProvider() self.lastMenuProvider = resolvedProvider ?? .codex provider = resolvedProvider } else { if let menuProvider = self.menuProviders[ObjectIdentifier(menu)] { self.lastMenuProvider = menuProvider provider = menuProvider } else if menu === self.fallbackMenu { self.lastMenuProvider = self.store.enabledProvidersForDisplay().first ?? .codex provider = nil } else { let resolved = self.store.enabledProvidersForDisplay().first ?? .codex self.lastMenuProvider = resolved provider = resolved } } let didRefresh = self.menuNeedsRefresh(menu) if didRefresh { self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) // Heights are already set during populateMenu, no need to remeasure } self.openMenus[ObjectIdentifier(menu)] = menu // Only schedule refresh after menu is registered as open - refreshNow is called async if Self.menuRefreshEnabled { self.scheduleOpenMenuRefresh(for: menu) } } func menuDidClose(_ menu: NSMenu) { let key = ObjectIdentifier(menu) self.openMenus.removeValue(forKey: key) self.menuRefreshTasks.removeValue(forKey: key)?.cancel() let isPersistentMenu = menu === self.mergedMenu || menu === self.fallbackMenu || self.providerMenus.values.contains { $0 === menu } if !isPersistentMenu { self.menuProviders.removeValue(forKey: key) self.menuVersions.removeValue(forKey: key) } for menuItem in menu.items { (menuItem.view as? MenuCardHighlighting)?.setHighlighted(false) } } func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) { for menuItem in menu.items { let highlighted = menuItem == item && menuItem.isEnabled (menuItem.view as? MenuCardHighlighting)?.setHighlighted(highlighted) } } private func populateMenu(_ menu: NSMenu, provider: UsageProvider?) { let enabledProviders = self.store.enabledProvidersForDisplay() let includesOverview = self.includesOverviewTab(enabledProviders: enabledProviders) let switcherSelection = self.shouldMergeIcons && enabledProviders.count > 1 ? self.resolvedSwitcherSelection( enabledProviders: enabledProviders, includesOverview: includesOverview) : nil let isOverviewSelected = switcherSelection == .overview let selectedProvider = if isOverviewSelected { self.resolvedMenuProvider(enabledProviders: enabledProviders) } else { switcherSelection?.provider ?? provider } let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu) let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex let codexAccountDisplay = isOverviewSelected ? nil : self.codexAccountMenuDisplay(for: currentProvider) let tokenAccountDisplay = isOverviewSelected ? nil : self.tokenAccountMenuDisplay(for: currentProvider) let showAllTokenAccounts = tokenAccountDisplay?.showAll ?? false let openAIContext = self.openAIWebContext( currentProvider: currentProvider, showAllTokenAccounts: showAllTokenAccounts) let hasTokenSwitcher = menu.items.contains { $0.view is TokenAccountSwitcherView } let hasCodexSwitcher = menu.items.contains { $0.view is CodexAccountSwitcherView } let switcherProvidersMatch = enabledProviders == self.lastSwitcherProviders let switcherUsageBarsShowUsedMatch = self.settings.usageBarsShowUsed == self.lastSwitcherUsageBarsShowUsed let switcherSelectionMatches = switcherSelection == self.lastMergedSwitcherSelection let switcherOverviewAvailabilityMatches = includesOverview == self.lastSwitcherIncludesOverview let tokenSwitcherCompatible = tokenAccountDisplay == nil && !hasTokenSwitcher let codexSwitcherCompatible = codexAccountDisplay == self.lastCodexAccountMenuDisplay && ((codexAccountDisplay == nil && !hasCodexSwitcher) || (codexAccountDisplay != nil && hasCodexSwitcher)) let canSmartUpdate = self.shouldMergeIcons && enabledProviders.count > 1 && !isOverviewSelected && switcherProvidersMatch && switcherUsageBarsShowUsedMatch && switcherSelectionMatches && switcherOverviewAvailabilityMatches && tokenSwitcherCompatible && codexSwitcherCompatible && !menu.items.isEmpty && menu.items.first?.view is ProviderSwitcherView if canSmartUpdate { self.updateMenuContent( menu, provider: selectedProvider, currentProvider: currentProvider, menuWidth: menuWidth, openAIContext: openAIContext) return } menu.removeAllItems() let descriptor = MenuDescriptor.build( provider: selectedProvider, store: self.store, settings: self.settings, account: self.account, managedCodexAccountCoordinator: self.managedCodexAccountCoordinator, codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator, updateReady: self.updater.updateStatus.isUpdateReady, includeContextualActions: !isOverviewSelected) self.addProviderSwitcherIfNeeded( to: menu, enabledProviders: enabledProviders, includesOverview: includesOverview, selection: switcherSelection ?? .provider(currentProvider)) // Track which providers the switcher was built with for smart update detection if self.shouldMergeIcons, enabledProviders.count > 1 { self.lastSwitcherProviders = enabledProviders self.lastSwitcherUsageBarsShowUsed = self.settings.usageBarsShowUsed self.lastMergedSwitcherSelection = switcherSelection self.lastSwitcherIncludesOverview = includesOverview } self.addCodexAccountSwitcherIfNeeded(to: menu, display: codexAccountDisplay) self.lastCodexAccountMenuDisplay = codexAccountDisplay self.addTokenAccountSwitcherIfNeeded(to: menu, display: tokenAccountDisplay) let menuContext = MenuCardContext( currentProvider: currentProvider, selectedProvider: selectedProvider, menuWidth: menuWidth, tokenAccountDisplay: tokenAccountDisplay, openAIContext: openAIContext) if isOverviewSelected { if self.addOverviewRows( to: menu, enabledProviders: enabledProviders, menuWidth: menuWidth) { menu.addItem(.separator()) } else { self.addOverviewEmptyState(to: menu, enabledProviders: enabledProviders) menu.addItem(.separator()) } } else { let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext) self.addOpenAIWebItemsIfNeeded( to: menu, currentProvider: currentProvider, context: openAIContext, addedOpenAIWebItems: addedOpenAIWebItems) if self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider) { menu.addItem(.separator()) } } self.addActionableSections(descriptor.sections, to: menu, width: menuWidth) } /// Smart update: only rebuild content sections when switching providers (keep the switcher intact). private func updateMenuContent( _ menu: NSMenu, provider: UsageProvider?, currentProvider: UsageProvider, menuWidth: CGFloat, openAIContext: OpenAIWebContext) { // Batch menu updates to prevent visual flickering during provider switch. CATransaction.begin() CATransaction.setDisableActions(true) defer { CATransaction.commit() } var contentStartIndex = 0 if menu.items.first?.view is ProviderSwitcherView { contentStartIndex = 2 } if menu.items.count > contentStartIndex, menu.items[contentStartIndex].view is CodexAccountSwitcherView { contentStartIndex += 2 } if menu.items.count > contentStartIndex, menu.items[contentStartIndex].view is TokenAccountSwitcherView { contentStartIndex += 2 } while menu.items.count > contentStartIndex { menu.removeItem(at: contentStartIndex) } let descriptor = MenuDescriptor.build( provider: provider, store: self.store, settings: self.settings, account: self.account, managedCodexAccountCoordinator: self.managedCodexAccountCoordinator, codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator, updateReady: self.updater.updateStatus.isUpdateReady) let menuContext = MenuCardContext( currentProvider: currentProvider, selectedProvider: provider, menuWidth: menuWidth, tokenAccountDisplay: nil, openAIContext: openAIContext) let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext) self.addOpenAIWebItemsIfNeeded( to: menu, currentProvider: currentProvider, context: openAIContext, addedOpenAIWebItems: addedOpenAIWebItems) if self.addUsageHistoryMenuItemIfNeeded(to: menu, provider: currentProvider) { menu.addItem(.separator()) } self.addActionableSections(descriptor.sections, to: menu, width: menuWidth) } private struct OpenAIWebContext { let hasUsageBreakdown: Bool let hasCreditsHistory: Bool let hasCostHistory: Bool let canShowBuyCredits: Bool let hasOpenAIWebMenuItems: Bool } private struct MenuCardContext { let currentProvider: UsageProvider let selectedProvider: UsageProvider? let menuWidth: CGFloat let tokenAccountDisplay: TokenAccountMenuDisplay? let openAIContext: OpenAIWebContext } private func openAIWebContext( currentProvider: UsageProvider, showAllTokenAccounts: Bool) -> OpenAIWebContext { let codexProjection = self.store.codexConsumerProjectionIfNeeded( for: currentProvider, surface: .liveCard) let hasCreditsHistory = codexProjection?.hasCreditsHistory == true let hasUsageBreakdown = codexProjection?.hasUsageBreakdown == true let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) && (self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false) let canShowBuyCredits = self.settings.showOptionalCreditsAndExtraUsage && codexProjection?.canShowBuyCredits == true let hasOpenAIWebMenuItems = !showAllTokenAccounts && (hasCreditsHistory || hasUsageBreakdown || hasCostHistory) return OpenAIWebContext( hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, hasCostHistory: hasCostHistory, canShowBuyCredits: canShowBuyCredits, hasOpenAIWebMenuItems: hasOpenAIWebMenuItems) } private func addProviderSwitcherIfNeeded( to menu: NSMenu, enabledProviders: [UsageProvider], includesOverview: Bool, selection: ProviderSwitcherSelection) { guard self.shouldMergeIcons, enabledProviders.count > 1 else { return } let switcherItem = self.makeProviderSwitcherItem( providers: enabledProviders, includesOverview: includesOverview, selected: selection, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } private func addTokenAccountSwitcherIfNeeded(to menu: NSMenu, display: TokenAccountMenuDisplay?) { guard let display, display.showSwitcher else { return } let switcherItem = self.makeTokenAccountSwitcherItem(display: display, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } private func addCodexAccountSwitcherIfNeeded(to menu: NSMenu, display: CodexAccountMenuDisplay?) { guard let display else { return } let switcherItem = self.makeCodexAccountSwitcherItem(display: display, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } @discardableResult private func addOverviewRows( to menu: NSMenu, enabledProviders: [UsageProvider], menuWidth: CGFloat) -> Bool { let overviewProviders = self.settings.reconcileMergedOverviewSelectedProviders( activeProviders: enabledProviders) let rows: [(provider: UsageProvider, model: UsageMenuCardView.Model)] = overviewProviders .compactMap { provider in guard let model = self.menuCardModel(for: provider) else { return nil } return (provider: provider, model: model) } guard !rows.isEmpty else { return false } for (index, row) in rows.enumerated() { let identifier = "\(Self.overviewRowIdentifierPrefix)\(row.provider.rawValue)" let item = self.makeMenuCardItem( OverviewMenuCardRowView(model: row.model, width: menuWidth), id: identifier, width: menuWidth, onClick: { [weak self, weak menu] in guard let self, let menu else { return } self.selectOverviewProvider(row.provider, menu: menu) }) // Keep menu item action wired for keyboard activation and accessibility action paths. item.target = self item.action = #selector(self.selectOverviewProvider(_:)) menu.addItem(item) if index < rows.count - 1 { menu.addItem(.separator()) } } return true } private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { let resolvedProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, maxVisibleProviders: Self.maxOverviewProviders) let message = if resolvedProviders.isEmpty { "No providers selected for Overview." } else { "No overview data available." } let item = NSMenuItem(title: message, action: nil, keyEquivalent: "") item.isEnabled = false item.representedObject = "overviewEmptyState" menu.addItem(item) } private func addMenuCards(to menu: NSMenu, context: MenuCardContext) -> Bool { if let tokenAccountDisplay = context.tokenAccountDisplay, tokenAccountDisplay.showAll { let accountSnapshots = tokenAccountDisplay.snapshots let cards = accountSnapshots.isEmpty ? [] : accountSnapshots.compactMap { accountSnapshot in self.menuCardModel( for: context.currentProvider, snapshotOverride: accountSnapshot.snapshot, errorOverride: accountSnapshot.error) } if cards.isEmpty, let model = self.menuCardModel(for: context.selectedProvider) { menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: context.menuWidth), id: "menuCard", width: context.menuWidth)) menu.addItem(.separator()) } else { for (index, model) in cards.enumerated() { menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: context.menuWidth), id: "menuCard-\(index)", width: context.menuWidth)) if index < cards.count - 1 { menu.addItem(.separator()) } } if !cards.isEmpty { menu.addItem(.separator()) } } return false } guard let model = self.menuCardModel(for: context.selectedProvider) else { return false } if context.openAIContext.hasOpenAIWebMenuItems { let webItems = OpenAIWebMenuItems( hasUsageBreakdown: context.openAIContext.hasUsageBreakdown, hasCreditsHistory: context.openAIContext.hasCreditsHistory, hasCostHistory: context.openAIContext.hasCostHistory, canShowBuyCredits: context.openAIContext.canShowBuyCredits) self.addMenuCardSections( to: menu, model: model, provider: context.currentProvider, width: context.menuWidth, webItems: webItems) return true } menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: context.menuWidth), id: "menuCard", width: context.menuWidth)) if context.openAIContext.canShowBuyCredits { menu.addItem(self.makeBuyCreditsItem()) } menu.addItem(.separator()) return false } private func addOpenAIWebItemsIfNeeded( to menu: NSMenu, currentProvider: UsageProvider, context: OpenAIWebContext, addedOpenAIWebItems: Bool) { guard context.hasOpenAIWebMenuItems else { return } if !addedOpenAIWebItems { // Only show these when we actually have additional data. if context.hasUsageBreakdown { _ = self.addUsageBreakdownSubmenu(to: menu) } if context.hasCreditsHistory { _ = self.addCreditsHistorySubmenu(to: menu) } if context.hasCostHistory { _ = self.addCostHistorySubmenu(to: menu, provider: currentProvider) } } menu.addItem(.separator()) } private func addActionableSections(_ sections: [MenuDescriptor.Section], to menu: NSMenu, width: CGFloat) { let actionableSections = sections.filter { section in section.entries.contains { entry in if case .action = entry { return true } if case .submenu = entry { return true } return false } } for (index, section) in actionableSections.enumerated() { for entry in section.entries { switch entry { case let .text(text, style): if style == .secondary { menu.addItem(self.makeWrappedSecondaryTextItem(text: text, width: width)) continue } let item = NSMenuItem(title: text, action: nil, keyEquivalent: "") item.isEnabled = false if style == .headline { let font = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold) item.attributedTitle = NSAttributedString(string: text, attributes: [.font: font]) } else if style == .secondary { let font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) item.attributedTitle = NSAttributedString( string: text,