import AppKitimport CodexBarCoreimport Observationimport 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 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) self.openMenus[ObjectIdentifier(menu)] = menu // Removed redundant async refresh - single pass is sufficient after initial layout return } var provider: UsageProvider? if self.shouldMergeIcons { self.selectedMenuProvider = self.resolvedMenuProvider() self.lastMenuProvider = self.selectedMenuProvider ?? .codex provider = self.selectedMenuProvider } 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 } } if self.menuNeedsRefresh(menu) { self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) } self.refreshMenuCardHeights(in: menu) self.openMenus[ObjectIdentifier(menu)] = menu self.scheduleOpenMenuRefresh(for: menu) // Removed redundant async refresh - single pass is sufficient after initial layout } func menuDidClose(_ menu: NSMenu) { self.openMenus.removeValue(forKey: ObjectIdentifier(menu)) self.menuRefreshTasks.removeValue(forKey: ObjectIdentifier(menu))?.cancel() 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?) { menu.removeAllItems() let selectedProvider = provider let enabledProviders = self.store.enabledProviders() let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu) let descriptor = MenuDescriptor.build( provider: selectedProvider, store: self.store, settings: self.settings, account: self.account, updateReady: self.updater.updateStatus.isUpdateReady) let dashboard = self.store.openAIDashboard let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex 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 = hasCreditsHistory || hasUsageBreakdown || hasCostHistory var addedOpenAIWebItems = false if self.shouldMergeIcons, enabledProviders.count > 1 { let switcherItem = self.makeProviderSwitcherItem( providers: enabledProviders, selected: selectedProvider, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } if let model = self.menuCardModel(for: selectedProvider) { if hasOpenAIWebMenuItems { let webItems = OpenAIWebMenuItems( hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, hasCostHistory: hasCostHistory) self.addMenuCardSections( to: menu, model: model, provider: currentProvider, width: menuWidth, webItems: webItems) addedOpenAIWebItems = true } else { menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: menuWidth), id: "menuCard", width: menuWidth)) if currentProvider == .codex, model.creditsText != nil { menu.addItem(self.makeBuyCreditsItem()) } menu.addItem(.separator()) } } if hasOpenAIWebMenuItems { if !addedOpenAIWebItems { // Only show these when we actually have additional data. if hasUsageBreakdown { _ = self.addUsageBreakdownSubmenu(to: menu) } if hasCreditsHistory { _ = self.addCreditsHistorySubmenu(to: menu) } if hasCostHistory { _ = self.addCostHistorySubmenu(to: menu, provider: currentProvider) } } menu.addItem(.separator()) } let actionableSections = descriptor.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.refreshMenuCardHeights(in: 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 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 { self.openMenus.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) self.refreshMenuCardHeights(in: menu) } } } private func menuProvider(for menu: NSMenu) -> UsageProvider? { if self.shouldMergeIcons { return self.selectedMenuProvider ?? 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) { self.refreshNow() 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.refreshNow() } } 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 { let highlightState = MenuCardHighlightState() let wrapped = MenuCardSectionContainerView( highlightState: highlightState, showsSubmenuIndicator: submenu != nil) { view } let hosting = MenuCardItemHostingView(rootView: wrapped, highlightState: highlightState) hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1)) hosting.needsLayout = true hosting.layoutSubtreeIfNeeded() // Important: constrain width before asking SwiftUI for the fitting height, otherwise text wrapping // changes the required height and the menu item becomes visually "squeezed". 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)) // Constrain width for accurate wrapping during layout. let widthConstraint = view.widthAnchor.constraint(equalToConstant: width) widthConstraint.priority = .required widthConstraint.isActive = true // Single layout pass instead of multiple invalidations. view.layoutSubtreeIfNeeded() widthConstraint.isActive = false // Use fittingSize directly - the frame width already constrains the view 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.usageBarsShowUsedLines 1–500 of 1651. Load more
import AppKit import CodexBarCore import Observation 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 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) self.openMenus[ObjectIdentifier(menu)] = menu // Removed redundant async refresh - single pass is sufficient after initial layout return } var provider: UsageProvider? if self.shouldMergeIcons { self.selectedMenuProvider = self.resolvedMenuProvider() self.lastMenuProvider = self.selectedMenuProvider ?? .codex provider = self.selectedMenuProvider } 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 } } if self.menuNeedsRefresh(menu) { self.populateMenu(menu, provider: provider) self.markMenuFresh(menu) } self.refreshMenuCardHeights(in: menu) self.openMenus[ObjectIdentifier(menu)] = menu self.scheduleOpenMenuRefresh(for: menu) // Removed redundant async refresh - single pass is sufficient after initial layout } func menuDidClose(_ menu: NSMenu) { self.openMenus.removeValue(forKey: ObjectIdentifier(menu)) self.menuRefreshTasks.removeValue(forKey: ObjectIdentifier(menu))?.cancel() 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?) { menu.removeAllItems() let selectedProvider = provider let enabledProviders = self.store.enabledProviders() let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu) let descriptor = MenuDescriptor.build( provider: selectedProvider, store: self.store, settings: self.settings, account: self.account, updateReady: self.updater.updateStatus.isUpdateReady) let dashboard = self.store.openAIDashboard let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex 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 = hasCreditsHistory || hasUsageBreakdown || hasCostHistory var addedOpenAIWebItems = false if self.shouldMergeIcons, enabledProviders.count > 1 { let switcherItem = self.makeProviderSwitcherItem( providers: enabledProviders, selected: selectedProvider, menu: menu) menu.addItem(switcherItem) menu.addItem(.separator()) } if let model = self.menuCardModel(for: selectedProvider) { if hasOpenAIWebMenuItems { let webItems = OpenAIWebMenuItems( hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, hasCostHistory: hasCostHistory) self.addMenuCardSections( to: menu, model: model, provider: currentProvider, width: menuWidth, webItems: webItems) addedOpenAIWebItems = true } else { menu.addItem(self.makeMenuCardItem( UsageMenuCardView(model: model, width: menuWidth), id: "menuCard", width: menuWidth)) if currentProvider == .codex, model.creditsText != nil { menu.addItem(self.makeBuyCreditsItem()) } menu.addItem(.separator()) } } if hasOpenAIWebMenuItems { if !addedOpenAIWebItems { // Only show these when we actually have additional data. if hasUsageBreakdown { _ = self.addUsageBreakdownSubmenu(to: menu) } if hasCreditsHistory { _ = self.addCreditsHistorySubmenu(to: menu) } if hasCostHistory { _ = self.addCostHistorySubmenu(to: menu, provider: currentProvider) } } menu.addItem(.separator()) } let actionableSections = descriptor.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.refreshMenuCardHeights(in: 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 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 { self.openMenus.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) self.refreshMenuCardHeights(in: menu) } } } private func menuProvider(for menu: NSMenu) -> UsageProvider? { if self.shouldMergeIcons { return self.selectedMenuProvider ?? 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) { self.refreshNow() 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.refreshNow() } } 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 { let highlightState = MenuCardHighlightState() let wrapped = MenuCardSectionContainerView( highlightState: highlightState, showsSubmenuIndicator: submenu != nil) { view } let hosting = MenuCardItemHostingView(rootView: wrapped, highlightState: highlightState) hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1)) hosting.needsLayout = true hosting.layoutSubtreeIfNeeded() // Important: constrain width before asking SwiftUI for the fitting height, otherwise text wrapping // changes the required height and the menu item becomes visually "squeezed". 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)) // Constrain width for accurate wrapping during layout. let widthConstraint = view.widthAnchor.constraint(equalToConstant: width) widthConstraint.priority = .required widthConstraint.isActive = true // Single layout pass instead of multiple invalidations. view.layoutSubtreeIfNeeded() widthConstraint.isActive = false // Use fittingSize directly - the frame width already constrains the view 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