import AppKitimport CodexBarCoreimport Observationimport QuartzCoreimport SwiftUI // MARK: - NSMenu construction extension StatusItemController { private static let menuCardBaseWidth: CGFloat = 310 private static let menuOpenRefreshDelay: Duration = .seconds(1.2) private struct OpenAIWebMenuItems { let hasUsageBreakdown: Bool let hasCreditsHistory: Bool let hasCostHistory: Bool } private struct TokenAccountMenuDisplay { let provider: UsageProvider let accounts: [ProviderTokenAccount] let snapshots: [TokenAccountUsageSnapshot] let activeIndex: Int let showAll: Bool let showSwitcher: Bool } 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.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.enabledProviders().first ?? .codex provider = nil } else { let resolved = self.store.enabledProviders().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 selectedProvider = provider let enabledProviders = self.store.enabledProviders() let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu) let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex let tokenAccountDisplay = self.tokenAccountMenuDisplay(for: currentProvider) let showAllTokenAccounts = tokenAccountDisplay?.showAll ?? false let openAIContext = self.openAIWebContext( currentProvider: currentProvider, showAllTokenAccounts: showAllTokenAccounts) let hasTokenAccountSwitcher = menu.items.contains { $0.view is TokenAccountSwitcherView } let switcherProvidersMatch = enabledProviders == self.lastSwitcherProviders let switcherUsageBarsShowUsedMatch = self.settings.usageBarsShowUsed == self.lastSwitcherUsageBarsShowUsed let canSmartUpdate = self.shouldMergeIcons && enabledProviders.count > 1 && switcherProvidersMatch && switcherUsageBarsShowUsedMatch && tokenAccountDisplay == nil && !hasTokenAccountSwitcher && !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, updateReady: self.updater.updateStatus.isUpdateReady) self.addProviderSwitcherIfNeeded( to: menu, enabledProviders: enabledProviders, selectedProvider: selectedProvider) // 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.addTokenAccountSwitcherIfNeeded(to: menu, display: tokenAccountDisplay) let menuContext = MenuCardContext( currentProvider: currentProvider, selectedProvider: selectedProvider, menuWidth: menuWidth, tokenAccountDisplay: tokenAccountDisplay, openAIContext: openAIContext) let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext) self.addOpenAIWebItemsIfNeeded( to: menu, currentProvider: currentProvider, context: openAIContext, addedOpenAIWebItems: addedOpenAIWebItems) self.addActionableSections(descriptor.sections, to: menu) } /// 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 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, 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) self.addActionableSections(descriptor.sections, to: menu) } private struct OpenAIWebContext { let hasUsageBreakdown: Bool let hasCreditsHistory: Bool let hasCostHistory: 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 dashboard = self.store.openAIDashboard let openAIWebEligible = currentProvider == .codex && self.store.openAIDashboardRequiresLogin == false && dashboard != nil let hasCreditsHistory = openAIWebEligible && !(dashboard?.dailyBreakdown ?? []).isEmpty let hasUsageBreakdown = openAIWebEligible && !(dashboard?.usageBreakdown ?? []).isEmpty let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) && (self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false) let hasOpenAIWebMenuItems = !showAllTokenAccounts && (hasCreditsHistory || hasUsageBreakdown || hasCostHistory) return OpenAIWebContext( hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, hasCostHistory: hasCostHistory, hasOpenAIWebMenuItems: hasOpenAIWebMenuItems) } private func addProviderSwitcherIfNeeded( to menu: NSMenu, enabledProviders: [UsageProvider], selectedProvider: UsageProvider?) { guard self.shouldMergeIcons, enabledProviders.count > 1 else { return } let switcherItem = self.makeProviderSwitcherItem( providers: enabledProviders, selected: selectedProvider, 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 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) 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.currentProvider == .codex, model.creditsText != nil { 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) { let actionableSections = sections.filter { section in section.entries.contains { entry in if case .action = entry { return true } return false } } for (index, section) in actionableSections.enumerated() { for entry in section.entries { switch entry { case let .text(text, style): 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, attributes: [.font: font, .foregroundColor: NSColor.secondaryLabelColor]) } menu.addItem(item) case let .action(title, action): let (selector, represented) = self.selector(for: action) let item = NSMenuItem(title: title, action: selector, keyEquivalent: "") item.target = self item.representedObject = represented if let iconName = action.systemImageName, let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) { image.isTemplate = true image.size = NSSize(width: 16, height: 16) item.image = image } if case let .switchAccount(targetProvider) = action, let subtitle = self.switchAccountSubtitle(for: targetProvider) { item.isEnabled = false self.applySubtitle(subtitle, to: item, title: title) } menu.addItem(item) case .divider: menu.addItem(.separator()) } } if index < actionableSections.count - 1 { menu.addItem(.separator()) } } } func makeMenu(for provider: UsageProvider?) -> NSMenu { let menu = NSMenu() menu.autoenablesItems = false menu.delegate = self if let provider { self.menuProviders[ObjectIdentifier(menu)] = provider } return menu } private func makeProviderSwitcherItem( providers: [UsageProvider], selected: UsageProvider?, menu: NSMenu) -> NSMenuItem { let view = ProviderSwitcherView( providers: providers, selected: selected, width: self.menuCardWidth(for: providers, menu: menu), showsIcons: self.settings.switcherShowsIcons, iconProvider: { [weak self] provider in self?.switcherIcon(for: provider) ?? NSImage() }, weeklyRemainingProvider: { [weak self] provider in self?.switcherWeeklyRemaining(for: provider) }, onSelect: { [weak self, weak menu] provider in guard let self, let menu else { return } self.selectedMenuProvider = provider self.lastMenuProvider = provider self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) self.applyIcon(phase: nil) }) let item = NSMenuItem() item.view = view item.isEnabled = false return item } private func makeTokenAccountSwitcherItem( display: TokenAccountMenuDisplay, menu: NSMenu) -> NSMenuItem { let view = TokenAccountSwitcherView( accounts: display.accounts, selectedIndex: display.activeIndex, width: self.menuCardWidth(for: self.store.enabledProviders(), menu: menu), onSelect: { [weak self, weak menu] index in guard let self, let menu else { return } self.settings.setActiveTokenAccountIndex(index, for: display.provider) Task { @MainActor in await ProviderInteractionContext.$current.withValue(.userInitiated) { await self.store.refresh() } } self.populateMenu(menu, provider: display.provider) self.markMenuFresh(menu) self.applyIcon(phase: nil) }) let item = NSMenuItem() item.view = view item.isEnabled = false return item } private func resolvedMenuProvider() -> UsageProvider? { let enabled = self.store.enabledProviders() if enabled.isEmpty { return .codex } if let selected = self.selectedMenuProvider, enabled.contains(selected) { return selected } return enabled.first } private func tokenAccountMenuDisplay(for provider: UsageProvider) -> TokenAccountMenuDisplay? { guard TokenAccountSupportCatalog.support(for: provider) != nil else { return nil } let accounts = self.settings.tokenAccounts(for: provider) guard accounts.count > 1 else { return nil } let activeIndex = self.settings.tokenAccountsData(for: provider)?.clampedActiveIndex() ?? 0 let showAll = self.settings.showAllTokenAccountsInMenu let snapshots = showAll ? (self.store.accountSnapshots[provider] ?? []) : [] return TokenAccountMenuDisplay( provider: provider, accounts: accounts,Lines 1–500 of 1266. 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 menuOpenRefreshDelay: Duration = .seconds(1.2) private struct OpenAIWebMenuItems { let hasUsageBreakdown: Bool let hasCreditsHistory: Bool let hasCostHistory: Bool } private struct TokenAccountMenuDisplay { let provider: UsageProvider let accounts: [ProviderTokenAccount] let snapshots: [TokenAccountUsageSnapshot] let activeIndex: Int let showAll: Bool let showSwitcher: Bool } 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.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.enabledProviders().first ?? .codex provider = nil } else { let resolved = self.store.enabledProviders().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 selectedProvider = provider let enabledProviders = self.store.enabledProviders() let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu) let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex let tokenAccountDisplay = self.tokenAccountMenuDisplay(for: currentProvider) let showAllTokenAccounts = tokenAccountDisplay?.showAll ?? false let openAIContext = self.openAIWebContext( currentProvider: currentProvider, showAllTokenAccounts: showAllTokenAccounts) let hasTokenAccountSwitcher = menu.items.contains { $0.view is TokenAccountSwitcherView } let switcherProvidersMatch = enabledProviders == self.lastSwitcherProviders let switcherUsageBarsShowUsedMatch = self.settings.usageBarsShowUsed == self.lastSwitcherUsageBarsShowUsed let canSmartUpdate = self.shouldMergeIcons && enabledProviders.count > 1 && switcherProvidersMatch && switcherUsageBarsShowUsedMatch && tokenAccountDisplay == nil && !hasTokenAccountSwitcher && !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, updateReady: self.updater.updateStatus.isUpdateReady) self.addProviderSwitcherIfNeeded( to: menu, enabledProviders: enabledProviders, selectedProvider: selectedProvider) // 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.addTokenAccountSwitcherIfNeeded(to: menu, display: tokenAccountDisplay) let menuContext = MenuCardContext( currentProvider: currentProvider, selectedProvider: selectedProvider, menuWidth: menuWidth, tokenAccountDisplay: tokenAccountDisplay, openAIContext: openAIContext) let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext) self.addOpenAIWebItemsIfNeeded( to: menu, currentProvider: currentProvider, context: openAIContext, addedOpenAIWebItems: addedOpenAIWebItems) self.addActionableSections(descriptor.sections, to: menu) } /// 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 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, 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) self.addActionableSections(descriptor.sections, to: menu) } private struct OpenAIWebContext { let hasUsageBreakdown: Bool let hasCreditsHistory: Bool let hasCostHistory: 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 dashboard = self.store.openAIDashboard let openAIWebEligible = currentProvider == .codex && self.store.openAIDashboardRequiresLogin == false && dashboard != nil let hasCreditsHistory = openAIWebEligible && !(dashboard?.dailyBreakdown ?? []).isEmpty let hasUsageBreakdown = openAIWebEligible && !(dashboard?.usageBreakdown ?? []).isEmpty let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) && (self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false) let hasOpenAIWebMenuItems = !showAllTokenAccounts && (hasCreditsHistory || hasUsageBreakdown || hasCostHistory) return OpenAIWebContext( hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, hasCostHistory: hasCostHistory, hasOpenAIWebMenuItems: hasOpenAIWebMenuItems) } private func addProviderSwitcherIfNeeded( to menu: NSMenu, enabledProviders: [UsageProvider], selectedProvider: UsageProvider?) { guard self.shouldMergeIcons, enabledProviders.count > 1 else { return } let switcherItem = self.makeProviderSwitcherItem( providers: enabledProviders, selected: selectedProvider, 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 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) 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.currentProvider == .codex, model.creditsText != nil { 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) { let actionableSections = sections.filter { section in section.entries.contains { entry in if case .action = entry { return true } return false } } for (index, section) in actionableSections.enumerated() { for entry in section.entries { switch entry { case let .text(text, style): 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, attributes: [.font: font, .foregroundColor: NSColor.secondaryLabelColor]) } menu.addItem(item) case let .action(title, action): let (selector, represented) = self.selector(for: action) let item = NSMenuItem(title: title, action: selector, keyEquivalent: "") item.target = self item.representedObject = represented if let iconName = action.systemImageName, let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) { image.isTemplate = true image.size = NSSize(width: 16, height: 16) item.image = image } if case let .switchAccount(targetProvider) = action, let subtitle = self.switchAccountSubtitle(for: targetProvider) { item.isEnabled = false self.applySubtitle(subtitle, to: item, title: title) } menu.addItem(item) case .divider: menu.addItem(.separator()) } } if index < actionableSections.count - 1 { menu.addItem(.separator()) } } } func makeMenu(for provider: UsageProvider?) -> NSMenu { let menu = NSMenu() menu.autoenablesItems = false menu.delegate = self if let provider { self.menuProviders[ObjectIdentifier(menu)] = provider } return menu } private func makeProviderSwitcherItem( providers: [UsageProvider], selected: UsageProvider?, menu: NSMenu) -> NSMenuItem { let view = ProviderSwitcherView( providers: providers, selected: selected, width: self.menuCardWidth(for: providers, menu: menu), showsIcons: self.settings.switcherShowsIcons, iconProvider: { [weak self] provider in self?.switcherIcon(for: provider) ?? NSImage() }, weeklyRemainingProvider: { [weak self] provider in self?.switcherWeeklyRemaining(for: provider) }, onSelect: { [weak self, weak menu] provider in guard let self, let menu else { return } self.selectedMenuProvider = provider self.lastMenuProvider = provider self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) self.applyIcon(phase: nil) }) let item = NSMenuItem() item.view = view item.isEnabled = false return item } private func makeTokenAccountSwitcherItem( display: TokenAccountMenuDisplay, menu: NSMenu) -> NSMenuItem { let view = TokenAccountSwitcherView( accounts: display.accounts, selectedIndex: display.activeIndex, width: self.menuCardWidth(for: self.store.enabledProviders(), menu: menu), onSelect: { [weak self, weak menu] index in guard let self, let menu else { return } self.settings.setActiveTokenAccountIndex(index, for: display.provider) Task { @MainActor in await ProviderInteractionContext.$current.withValue(.userInitiated) { await self.store.refresh() } } self.populateMenu(menu, provider: display.provider) self.markMenuFresh(menu) self.applyIcon(phase: nil) }) let item = NSMenuItem() item.view = view item.isEnabled = false return item } private func resolvedMenuProvider() -> UsageProvider? { let enabled = self.store.enabledProviders() if enabled.isEmpty { return .codex } if let selected = self.selectedMenuProvider, enabled.contains(selected) { return selected } return enabled.first } private func tokenAccountMenuDisplay(for provider: UsageProvider) -> TokenAccountMenuDisplay? { guard TokenAccountSupportCatalog.support(for: provider) != nil else { return nil } let accounts = self.settings.tokenAccounts(for: provider) guard accounts.count > 1 else { return nil } let activeIndex = self.settings.tokenAccountsData(for: provider)?.clampedActiveIndex() ?? 0 let showAll = self.settings.showAllTokenAccountsInMenu let snapshots = showAll ? (self.store.accountSnapshots[provider] ?? []) : [] return TokenAccountMenuDisplay( provider: provider, accounts: accounts,