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, snapshots: snapshots, activeIndex: activeIndex, showAll: showAll, showSwitcher: !showAll) } private func menuNeedsRefresh(_ menu: NSMenu) -> Bool { let key = ObjectIdentifier(menu) return self.menuVersions[key] != self.menuContentVersion } private func markMenuFresh(_ menu: NSMenu) { let key = ObjectIdentifier(menu) self.menuVersions[key] = self.menuContentVersion } func refreshOpenMenusIfNeeded() { guard !self.openMenus.isEmpty else { return } for (key, menu) in self.openMenus { guard key == ObjectIdentifier(menu) else { // Clean up orphaned menu entries from all tracking dictionaries self.openMenus.removeValue(forKey: key) self.menuRefreshTasks.removeValue(forKey: key)?.cancel() self.menuProviders.removeValue(forKey: key) self.menuVersions.removeValue(forKey: key) continue } if self.isHostedSubviewMenu(menu) { self.refreshHostedSubviewHeights(in: menu) continue } if self.menuNeedsRefresh(menu) { let provider = self.menuProvider(for: menu) self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) // Heights are already set during populateMenu, no need to remeasure } } } private func menuProvider(for menu: NSMenu) -> UsageProvider? { if self.shouldMergeIcons { return self.resolvedMenuProvider() } if let provider = self.menuProviders[ObjectIdentifier(menu)] { return provider } if menu === self.fallbackMenu { return nil } return self.store.enabledProviders().first ?? .codex } private func scheduleOpenMenuRefresh(for menu: NSMenu) { // Kick off a user-initiated refresh on open (non-forced) and re-check after a delay. // NEVER block menu opening with network requests. if !self.store.isRefreshing { self.refreshStore(forceTokenUsage: false) } let key = ObjectIdentifier(menu) self.menuRefreshTasks[key]?.cancel() self.menuRefreshTasks[key] = Task { @MainActor [weak self, weak menu] in guard let self, let menu else { return } try? await Task.sleep(for: Self.menuOpenRefreshDelay) guard !Task.isCancelled else { return } guard self.openMenus[ObjectIdentifier(menu)] != nil else { return } guard !self.store.isRefreshing else { return } let provider = self.menuProvider(for: menu) ?? self.resolvedMenuProvider() let isStale = provider.map { self.store.isStale(provider: $0) } ?? self.store.isStale let hasSnapshot = provider.map { self.store.snapshot(for: $0) != nil } ?? true guard isStale || !hasSnapshot else { return } self.refreshStore(forceTokenUsage: false) } } private func refreshMenuCardHeights(in menu: NSMenu) { // Re-measure the menu card height right before display to avoid stale/incorrect sizing when content // changes (e.g. dashboard error lines causing wrapping). let cardItems = menu.items.filter { item in (item.representedObject as? String)?.hasPrefix("menuCard") == true } for item in cardItems { guard let view = item.view else { continue } let width = self.menuCardWidth(for: self.store.enabledProviders(), menu: menu) let height = self.menuCardHeight(for: view, width: width) view.frame = NSRect( origin: .zero, size: NSSize(width: width, height: height)) } } private func makeMenuCardItem( _ view: some View, id: String, width: CGFloat, submenu: NSMenu? = nil) -> NSMenuItem { if !Self.menuCardRenderingEnabled { let item = NSMenuItem() item.isEnabled = true item.representedObject = id item.submenu = submenu if submenu != nil { item.target = self item.action = #selector(self.menuCardNoOp(_:)) } return item } let highlightState = MenuCardHighlightState() let wrapped = MenuCardSectionContainerView( highlightState: highlightState, showsSubmenuIndicator: submenu != nil) { view } let hosting = MenuCardItemHostingView(rootView: wrapped, highlightState: highlightState) // Set frame with target width immediately let height = self.menuCardHeight(for: hosting, width: width) hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: height)) let item = NSMenuItem() item.view = hosting item.isEnabled = true item.representedObject = id item.submenu = submenu if submenu != nil { item.target = self item.action = #selector(self.menuCardNoOp(_:)) } return item } private func menuCardHeight(for view: NSView, width: CGFloat) -> CGFloat { let basePadding: CGFloat = 6 let descenderSafety: CGFloat = 1 // Fast path: use protocol-based measurement when available (avoids layout passes) if let measured = view as? MenuCardMeasuring { return max(1, ceil(measured.measuredHeight(width: width) + basePadding + descenderSafety)) } // Set frame with target width before measuring. view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1)) // Use fittingSize directly - SwiftUI hosting views respect the frame width for wrapping let fitted = view.fittingSize return max(1, ceil(fitted.height + basePadding + descenderSafety)) } private func addMenuCardSections( to menu: NSMenu, model: UsageMenuCardView.Model, provider: UsageProvider, width: CGFloat, webItems: OpenAIWebMenuItems) { let hasUsageBlock = !model.metrics.isEmpty || model.placeholder != nil let hasCredits = model.creditsText != nil let hasExtraUsage = model.providerCost != nil let hasCost = model.tokenUsage != nil let bottomPadding = CGFloat(hasCredits ? 4 : 6) let sectionSpacing = CGFloat(6) let usageBottomPadding = bottomPadding let creditsBottomPadding = bottomPadding let headerView = UsageMenuCardHeaderSectionView( model: model, showDivider: hasUsageBlock, width: width) menu.addItem(self.makeMenuCardItem(headerView, id: "menuCardHeader", width: width)) if hasUsageBlock { let usageView = UsageMenuCardUsageSectionView( model: model, showBottomDivider: false, bottomPadding: usageBottomPadding, width: width) let usageSubmenu = self.makeUsageSubmenu( provider: provider, snapshot: self.store.snapshot(for: provider), webItems: webItems) menu.addItem(self.makeMenuCardItem( usageView, id: "menuCardUsage", width: width, submenu: usageSubmenu)) } if hasCredits || hasExtraUsage || hasCost { menu.addItem(.separator()) } if hasCredits { if hasExtraUsage || hasCost { menu.addItem(.separator()) } let creditsView = UsageMenuCardCreditsSectionView( model: model, showBottomDivider: false, topPadding: sectionSpacing, bottomPadding: creditsBottomPadding, width: width) let creditsSubmenu = webItems.hasCreditsHistory ? self.makeCreditsHistorySubmenu() : nil menu.addItem(self.makeMenuCardItem( creditsView, id: "menuCardCredits", width: width, submenu: creditsSubmenu)) if provider == .codex { menu.addItem(self.makeBuyCreditsItem()) } } if hasExtraUsage { if hasCredits { menu.addItem(.separator()) } let extraUsageView = UsageMenuCardExtraUsageSectionView( model: model, topPadding: sectionSpacing, bottomPadding: bottomPadding, width: width) menu.addItem(self.makeMenuCardItem( extraUsageView, id: "menuCardExtraUsage", width: width)) } if hasCost { if hasCredits || hasExtraUsage { menu.addItem(.separator()) } let costView = UsageMenuCardCostSectionView( model: model, topPadding: sectionSpacing, bottomPadding: bottomPadding, width: width) let costSubmenu = webItems.hasCostHistory ? self.makeCostHistorySubmenu(provider: provider) : nil menu.addItem(self.makeMenuCardItem( costView, id: "menuCardCost", width: width, submenu: costSubmenu)) } } private func switcherIcon(for provider: UsageProvider) -> NSImage { if let brand = ProviderBrandIcon.image(for: provider) { return brand } // Fallback to the dynamic icon renderer if resources are missing (e.g. dev bundle mismatch). let snapshot = self.store.snapshot(for: provider) let showUsed = self.settings.usageBarsShowUsed let primary = showUsed ? snapshot?.primary?.usedPercent : snapshot?.primary?.remainingPercent var weekly = showUsed ? snapshot?.secondary?.usedPercent : snapshot?.secondary?.remainingPercent if showUsed, provider == .warp, let remaining = snapshot?.secondary?.remainingPercent, remaining <= 0 { // Preserve Warp "no bonus/exhausted bonus" layout even in show-used mode. weekly = 0 } if showUsed, provider == .warp, let remaining = snapshot?.secondary?.remainingPercent, remaining > 0, weekly == 0 { // In show-used mode, `0` means "unused", not "missing". Keep the weekly lane present. weekly = 0.0001 } let credits = provider == .codex ? self.store.credits?.remaining : nil let stale = self.store.isStale(provider: provider) let style = self.store.style(for: provider) let indicator = self.store.statusIndicator(for: provider) let image = IconRenderer.makeIcon( primaryRemaining: primary, weeklyRemaining: weekly, creditsRemaining: credits, stale: stale, style: style, blink: 0, wiggle: 0, tilt: 0, statusIndicator: indicator) image.isTemplate = true return image } nonisolated static func switcherWeeklyMetricPercent( for provider: UsageProvider, snapshot: UsageSnapshot?, showUsed: Bool) -> Double? { let window = snapshot?.switcherWeeklyWindow(for: provider, showUsed: showUsed) guard let window else { return nil } return showUsed ? window.usedPercent : window.remainingPercent } private func switcherWeeklyRemaining(for provider: UsageProvider) -> Double? { Self.switcherWeeklyMetricPercent( for: provider, snapshot: self.store.snapshot(for: provider), showUsed: self.settings.usageBarsShowUsed) } private func selector(for action: MenuDescriptor.MenuAction) -> (Selector, Any?) { switch action { case .installUpdate: (#selector(self.installUpdate), nil) case .refresh: (#selector(self.refreshNow), nil) case .refreshAugmentSession: (#selector(self.refreshAugmentSession), nil) case .dashboard: (#selector(self.openDashboard), nil) case .statusPage: (#selector(self.openStatusPage), nil) case let .switchAccount(provider): (#selector(self.runSwitchAccount(_:)), provider.rawValue) case let .openTerminal(command): (#selector(self.openTerminalCommand(_:)), command) case let .loginToProvider(url): (#selector(self.openLoginToProvider(_:)), url) case .settings: (#selector(self.showSettingsGeneral), nil) case .about: (#selector(self.showSettingsAbout), nil) case .quit: (#selector(self.quit), nil) case let .copyError(message): (#selector(self.copyError(_:)), message) } } @MainActor private protocol MenuCardHighlighting: AnyObject { func setHighlighted(_ highlighted: Bool) } @MainActor private protocol MenuCardMeasuring: AnyObject { func measuredHeight(width: CGFloat) -> CGFloat } @MainActor @Observable fileprivate final class MenuCardHighlightState { var isHighlighted = false } private final class MenuHostingView: NSHostingView { override var allowsVibrancy: Bool { true } } @MainActor private final class MenuCardItemHostingView: NSHostingView, MenuCardHighlighting, MenuCardMeasuring { private let highlightState: MenuCardHighlightState override var allowsVibrancy: Bool { true } override var intrinsicContentSize: NSSize { let size = super.intrinsicContentSize guard self.frame.width > 0 else { return size } return NSSize(width: self.frame.width, height: size.height) } init(rootView: Content, highlightState: MenuCardHighlightState) { self.highlightState = highlightState super.init(rootView: rootView) } required init(rootView: Content) { self.highlightState = MenuCardHighlightState() super.init(rootView: rootView) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func measuredHeight(width: CGFloat) -> CGFloat { let controller = NSHostingController(rootView: self.rootView) let measured = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) return measured.height } func setHighlighted(_ highlighted: Bool) { guard self.highlightState.isHighlighted != highlighted else { return } self.highlightState.isHighlighted = highlighted } } private struct MenuCardSectionContainerView: View { @Bindable var highlightState: MenuCardHighlightState let showsSubmenuIndicator: Bool let content: Content init( highlightState: MenuCardHighlightState, showsSubmenuIndicator: Bool, @ViewBuilder content: () -> Content) { self.highlightState = highlightState self.showsSubmenuIndicator = showsSubmenuIndicator self.content = content() } var body: some View { self.content .environment(\.menuItemHighlighted, self.highlightState.isHighlighted) .foregroundStyle(MenuHighlightStyle.primary(self.highlightState.isHighlighted)) .background(alignment: .topLeading) { if self.highlightState.isHighlighted { RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(MenuHighlightStyle.selectionBackground(true)) .padding(.horizontal, 6) .padding(.vertical, 2) } } .overlay(alignment: .topTrailing) { if self.showsSubmenuIndicator { Image(systemName: "chevron.right") .font(.caption2.weight(.semibold)) .foregroundStyle(MenuHighlightStyle.secondary(self.highlightState.isHighlighted)) .padding(.top, 8) .padding(.trailing, 10) } } } } private func makeBuyCreditsItem() -> NSMenuItem { let item = NSMenuItem(title: "Buy Credits...", action: #selector(self.openCreditsPurchase), keyEquivalent: "") item.target = self if let image = NSImage(systemSymbolName: "plus.circle", accessibilityDescription: nil) { image.isTemplate = true image.size = NSSize(width: 16, height: 16) item.image = image } return item } @discardableResult private func addCreditsHistorySubmenu(to menu: NSMenu) -> Bool { guard let submenu = self.makeCreditsHistorySubmenu() else { return false } let item = NSMenuItem(title: "Credits history", action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu menu.addItem(item) return true } @discardableResult private func addUsageBreakdownSubmenu(to menu: NSMenu) -> Bool { guard let submenu = self.makeUsageBreakdownSubmenu() else { return false } let item = NSMenuItem(title: "Usage breakdown", action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu menu.addItem(item) return true } @discardableResult private func addCostHistorySubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool { guard let submenu = self.makeCostHistorySubmenu(provider: provider) else { return false } let item = NSMenuItem(title: "Usage history (30 days)", action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu menu.addItem(item) return true } private func makeUsageSubmenu( provider: UsageProvider, snapshot: UsageSnapshot?, webItems: OpenAIWebMenuItems) -> NSMenu? { if provider == .codex, webItems.hasUsageBreakdown { return self.makeUsageBreakdownSubmenu() } if provider == .zai { return self.makeZaiUsageDetailsSubmenu(snapshot: snapshot) } return nil } private func makeZaiUsageDetailsSubmenu(snapshot: UsageSnapshot?) -> NSMenu? { guard let timeLimit = snapshot?.zaiUsage?.timeLimit else { return nil } guard !timeLimit.usageDetails.isEmpty else { return nil } let submenu = NSMenu() submenu.delegate = self let titleItem = NSMenuItem(title: "MCP details", action: nil, keyEquivalent: "") titleItem.isEnabled = false submenu.addItem(titleItem) if let window = timeLimit.windowLabel { let item = NSMenuItem(title: "Window: \(window)", action: nil, keyEquivalent: "") item.isEnabled = false submenu.addItem(item) } if let resetTime = timeLimit.nextResetTime { let reset = self.settings.resetTimeDisplayStyle == .absolute ? UsageFormatter.resetDescription(from: resetTime) : UsageFormatter.resetCountdownDescription(from: resetTime) let item = NSMenuItem(title: "Resets: \(reset)", action: nil, keyEquivalent: "") item.isEnabled = false submenu.addItem(item) } submenu.addItem(.separator()) let sortedDetails = timeLimit.usageDetails.sorted { $0.modelCode.localizedCaseInsensitiveCompare($1.modelCode) == .orderedAscending } for detail in sortedDetails { let usage = UsageFormatter.tokenCountString(detail.usage) let item = NSMenuItem(title: "\(detail.modelCode): \(usage)", action: nil, keyEquivalent: "") submenu.addItem(item) } return submenu } private func makeUsageBreakdownSubmenu() -> NSMenu? { let breakdown = self.store.openAIDashboard?.usageBreakdown ?? [] let width = Self.menuCardBaseWidth guard !breakdown.isEmpty else { return nil } if !Self.menuCardRenderingEnabled { let submenu = NSMenu() submenu.delegate = self let chartItem = NSMenuItem() chartItem.isEnabled = false chartItem.representedObject = "usageBreakdownChart" submenu.addItem(chartItem) return submenu } let submenu = NSMenu() submenu.delegate = self let chartView = UsageBreakdownChartMenuView(breakdown: breakdown, width: width) let hosting = MenuHostingView(rootView: chartView) // Use NSHostingController for efficient size calculation without multiple layout passes let controller = NSHostingController(rootView: chartView) let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height)) let chartItem = NSMenuItem() chartItem.view = hosting chartItem.isEnabled = false chartItem.representedObject = "usageBreakdownChart" submenu.addItem(chartItem) return submenu } private func makeCreditsHistorySubmenu() -> NSMenu? { let breakdown = self.store.openAIDashboard?.dailyBreakdown ?? [] let width = Self.menuCardBaseWidth guard !breakdown.isEmpty else { return nil } if !Self.menuCardRenderingEnabled { let submenu = NSMenu() submenu.delegate = self let chartItem = NSMenuItem() chartItem.isEnabled = false chartItem.representedObject = "creditsHistoryChart" submenu.addItem(chartItem) return submenu } let submenu = NSMenu() submenu.delegate = self let chartView = CreditsHistoryChartMenuView(breakdown: breakdown, width: width) let hosting = MenuHostingView(rootView: chartView) // Use NSHostingController for efficient size calculation without multiple layout passes let controller = NSHostingController(rootView: chartView) let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height)) let chartItem = NSMenuItem() chartItem.view = hosting chartItem.isEnabled = false chartItem.representedObject = "creditsHistoryChart" submenu.addItem(chartItem) return submenu } private func makeCostHistorySubmenu(provider: UsageProvider) -> NSMenu? { guard provider == .codex || provider == .claude || provider == .vertexai else { return nil } let width = Self.menuCardBaseWidth guard let tokenSnapshot = self.store.tokenSnapshot(for: provider) else { return nil } guard !tokenSnapshot.daily.isEmpty else { return nil } if !Self.menuCardRenderingEnabled { let submenu = NSMenu() submenu.delegate = self let chartItem = NSMenuItem() chartItem.isEnabled = false chartItem.representedObject = "costHistoryChart" submenu.addItem(chartItem) return submenu } let submenu = NSMenu() submenu.delegate = self let chartView = CostHistoryChartMenuView( provider: provider, daily: tokenSnapshot.daily, totalCostUSD: tokenSnapshot.last30DaysCostUSD, width: width) let hosting = MenuHostingView(rootView: chartView) // Use NSHostingController for efficient size calculation without multiple layout passes let controller = NSHostingController(rootView: chartView) let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height)) let chartItem = NSMenuItem() chartItem.view = hosting chartItem.isEnabled = false chartItem.representedObject = "costHistoryChart" submenu.addItem(chartItem) return submenu } private func isHostedSubviewMenu(_ menu: NSMenu) -> Bool { let ids: Set = [ "usageBreakdownChart", "creditsHistoryChart", "costHistoryChart", ] return menu.items.contains { item in guard let id = item.representedObject as? String else { return false } return ids.contains(id) } } private func isOpenAIWebSubviewMenu(_ menu: NSMenu) -> Bool { let ids: Set = [ "usageBreakdownChart", "creditsHistoryChart", ] return menu.items.contains { item in guard let id = item.representedObject as? String else { return false } return ids.contains(id) } } private func refreshHostedSubviewHeights(in menu: NSMenu) { let enabledProviders = self.store.enabledProviders() let width = self.menuCardWidth(for: enabledProviders, menu: menu) for item in menu.items { guard let view = item.view else { continue } view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1)) view.layoutSubtreeIfNeeded() let height = view.fittingSize.height view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: height)) } } private func menuCardModel( for provider: UsageProvider?, snapshotOverride: UsageSnapshot? = nil, errorOverride: String? = nil) -> UsageMenuCardView.Model? { let target = provider ?? self.store.enabledProviders().first ?? .codex let metadata = self.store.metadata(for: target) let snapshot = snapshotOverride ?? self.store.snapshot(for: target) let credits: CreditsSnapshot? let creditsError: String? let dashboard: OpenAIDashboardSnapshot? let dashboardError: String? let tokenSnapshot: CostUsageTokenSnapshot? let tokenError: String? if target == .codex, snapshotOverride == nil { credits = self.store.credits creditsError = self.store.lastCreditsError dashboard = self.store.openAIDashboardRequiresLogin ? nil : self.store.openAIDashboard dashboardError = self.store.lastOpenAIDashboardError tokenSnapshot = self.store.tokenSnapshot(for: target) tokenError = self.store.tokenError(for: target) } else if target == .claude || target == .vertexai, snapshotOverride == nil { credits = nil creditsError = nil dashboard = nil dashboardError = nil tokenSnapshot = self.store.tokenSnapshot(for: target) tokenError = self.store.tokenError(for: target) } else { credits = nil creditsError = nil dashboard = nil dashboardError = nil tokenSnapshot = nil tokenError = nil } let input = UsageMenuCardView.Model.Input( provider: target, metadata: metadata, snapshot: snapshot, credits: credits, creditsError: creditsError, dashboard: dashboard, dashboardError: dashboardError, tokenSnapshot: tokenSnapshot, tokenError: tokenError, account: self.account, isRefreshing: self.store.isRefreshing, lastError: errorOverride ?? self.store.error(for: target), usageBarsShowUsed: self.settings.usageBarsShowUsed, resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle, tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: target), showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage, hidePersonalInfo: self.settings.hidePersonalInfo, now: Date()) return UsageMenuCardView.Model.make(input) } @objc private func menuCardNoOp(_ sender: NSMenuItem) { _ = sender } private func applySubtitle(_ subtitle: String, to item: NSMenuItem, title: String) { if #available(macOS 14.4, *) { // NSMenuItem.subtitle is only available on macOS 14.4+. item.subtitle = subtitle } else { item.view = self.makeMenuSubtitleView(title: title, subtitle: subtitle, isEnabled: item.isEnabled) item.toolTip = "\(title) — \(subtitle)" } } private func makeMenuSubtitleView(title: String, subtitle: String, isEnabled: Bool) -> NSView { let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false container.alphaValue = isEnabled ? 1.0 : 0.7 let titleField = NSTextField(labelWithString: title) titleField.font = NSFont.menuFont(ofSize: NSFont.systemFontSize) titleField.textColor = NSColor.labelColor titleField.lineBreakMode = .byTruncatingTail titleField.maximumNumberOfLines = 1 titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) let subtitleField = NSTextField(labelWithString: subtitle) subtitleField.font = NSFont.menuFont(ofSize: NSFont.smallSystemFontSize) subtitleField.textColor = NSColor.secondaryLabelColor subtitleField.lineBreakMode = .byTruncatingTail subtitleField.maximumNumberOfLines = 1 subtitleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) let stack = NSStackView(views: [titleField, subtitleField]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 1 stack.translatesAutoresizingMaskIntoConstraints = false container.addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 18), stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -10), stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 2), stack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -2), ]) return container } }