import AppKit import CodexBarCore import SwiftUI /// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels. struct UsageMenuCardView: View { struct Model { enum PercentStyle: String { case left case used var labelSuffix: String { switch self { case .left: L("usage_percent_suffix_left") case .used: L("usage_percent_suffix_used") } } var accessibilityLabel: String { switch self { case .left: L("Usage remaining") case .used: L("Usage used") } } } struct Metric: Identifiable { let id: String let title: String let percent: Double let percentStyle: PercentStyle let statusText: String? let resetText: String? let detailText: String? let detailLeftText: String? let detailRightText: String? let pacePercent: Double? let paceOnTop: Bool let warningMarkerPercents: [Double] let workdayMarkerPercents: [Double] let cardStyle: Bool init( id: String, title: String, percent: Double, percentStyle: PercentStyle, statusText: String? = nil, resetText: String?, detailText: String?, detailLeftText: String?, detailRightText: String?, pacePercent: Double?, paceOnTop: Bool, warningMarkerPercents: [Double] = [], workdayMarkerPercents: [Double] = [], cardStyle: Bool = false) { self.id = id self.title = title self.percent = percent self.percentStyle = percentStyle self.statusText = statusText self.resetText = resetText self.detailText = detailText self.detailLeftText = detailLeftText self.detailRightText = detailRightText self.pacePercent = pacePercent self.paceOnTop = paceOnTop self.warningMarkerPercents = warningMarkerPercents self.workdayMarkerPercents = workdayMarkerPercents self.cardStyle = cardStyle } var percentLabel: String { UsageFormatter.percentText(self.percent, suffix: self.percentStyle.labelSuffix) } } enum SubtitleStyle { case info case loading case error } struct TokenUsageSection { let sessionLine: String let monthLine: String let comparisonLines: [String] let hintLine: String? let errorLine: String? let errorCopyText: String? init( sessionLine: String, monthLine: String, comparisonLines: [String] = [], hintLine: String?, errorLine: String?, errorCopyText: String?) { self.sessionLine = sessionLine self.monthLine = monthLine self.comparisonLines = comparisonLines self.hintLine = hintLine self.errorLine = errorLine self.errorCopyText = errorCopyText } } struct ProviderCostSection { let title: String let percentUsed: Double? let spendLine: String let percentLine: String? var personalSpendLine: String? } let provider: UsageProvider let providerName: String let email: String let subtitleText: String let subtitleStyle: SubtitleStyle var usesLiveSubtitle: Bool = false let planText: String? let metrics: [Metric] let usageNotes: [String] let openAIAPIUsage: OpenAIAPIUsageSnapshot? let inlineUsageDashboard: InlineUsageDashboardModel? let creditsText: String? let creditsRemaining: Double? var creditsProgressPercent: Double?, creditsScaleText: String? let creditsHintText: String? let creditsHintCopyText: String? var codexResetCredits: CodexResetCreditsPresentation? let providerCost: ProviderCostSection? let tokenUsage: TokenUsageSection? let placeholder: String? let progressColor: Color } let model: Model var layoutModel: Model? let width: CGFloat var planAction: (() -> Void)? @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor static func popupMetricTitle(provider: UsageProvider, metric: Model.Metric) -> String { if provider == .openrouter, metric.id == "primary" { return L("API key limit") } return metric.title } var body: some View { let liveModel = self.liveModel VStack(alignment: .leading, spacing: 0) { UsageMenuCardHeaderView( model: self.layoutModel ?? self.model, planAction: self.planAction) if Self.hasDetails(for: liveModel) { Divider() .padding(.top, UsageMenuCardLayout.headerContentSpacing) .padding(.bottom, Self.dividerBottomPadding(for: liveModel)) } if !liveModel.usesStackedDetailLayout { if let dashboard = liveModel.inlineUsageDashboard { InlineUsageDashboardContent(model: dashboard) } else if !liveModel.usageNotes.isEmpty { UsageNotesContent(notes: liveModel.usageNotes) } else if let placeholder = liveModel.placeholder { // Non-stacked placeholders are standalone detail rows; stacked usage placeholders are gated below. Text(placeholder) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .font(.subheadline) } } else { let hasUsage = liveModel.hasUsageContent let hasCredits = liveModel.creditsText != nil let hasProviderCost = liveModel.providerCost != nil let hasCost = liveModel.tokenUsage != nil || hasProviderCost VStack(alignment: .leading, spacing: 12) { if hasUsage { UsageMenuCardUsageContentView(model: liveModel, showBottomDivider: false) } if hasUsage, hasCredits || hasCost { Divider() } if let credits = liveModel.creditsText { CreditsBarContent( creditsText: credits, creditsRemaining: liveModel.creditsRemaining, progressPercent: liveModel.creditsProgressPercent, scaleText: liveModel.creditsScaleText, hintText: liveModel.creditsHintText, hintCopyText: liveModel.creditsHintCopyText, progressColor: liveModel.progressColor) } if hasCredits, hasCost { Divider() } if let providerCost = liveModel.providerCost { ProviderCostContent( section: providerCost, progressColor: liveModel.progressColor) } if hasProviderCost, liveModel.tokenUsage != nil { Divider() } if let tokenUsage = liveModel.tokenUsage { VStack(alignment: .leading, spacing: 6) { Text(L("cost_header_estimated")) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) .font(.footnote) .lineLimit(1) Text(tokenUsage.monthLine) .font(.footnote) .lineLimit(1) ForEach(tokenUsage.comparisonLines, id: \.self) { line in Text(line) .font(.footnote) .lineLimit(1) } if let hint = tokenUsage.hintLine, !hint.isEmpty { Text(hint) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } if let error = tokenUsage.errorLine, !error.isEmpty { Text(error) .font(.footnote) .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) .overlay { ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error) } } } } } } } .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding( .top, Self.hasDetails(for: liveModel) ? UsageMenuCardLayout.sectionTopPadding : UsageMenuCardLayout.headerOnlyVerticalPadding) // AppKit's following separator row adds visual bottom space, so detail cards keep this inset tight. .padding( .bottom, Self.hasDetails(for: liveModel) ? UsageMenuCardLayout.sectionBottomPadding : UsageMenuCardLayout.headerOnlyVerticalPadding) .frame(width: self.width, alignment: .leading) } private var liveModel: Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } private static func hasDetails(for model: Model) -> Bool { model.hasUsageContent || model.usesStackedDetailLayout } static func dividerBottomPadding(for model: Model) -> CGFloat { if model.usesStackedDetailLayout, model.hasUsageContent { return UsageMenuCardLayout.postHeaderDividerContentSpacing } return UsageMenuCardLayout.sectionBottomPadding } } private struct UsageMenuCardHeaderView: View { let model: UsageMenuCardView.Model var planAction: (() -> Void)? @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { VStack(alignment: .leading, spacing: UsageMenuCardLayout.headerLineSpacing) { HStack(alignment: .firstTextBaseline, spacing: UsageMenuCardLayout.headerColumnSpacing) { Text(self.model.providerName).font(.headline) .fontWeight(.semibold) .lineLimit(1).truncationMode(.tail).layoutPriority(1) Spacer() Text(self.model.email).font(.subheadline) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1).truncationMode(.middle) } let liveSubtitle = self.liveSubtitle // Keep the geometry AppKit measured for this hosted row. A new error stays one line // until the next rebuild; a recovered error keeps its reserved height until then. let usesErrorLayout = self.model.subtitleStyle == .error let subtitleAlignment: VerticalAlignment = usesErrorLayout ? .top : .firstTextBaseline HStack(alignment: subtitleAlignment, spacing: UsageMenuCardLayout.headerColumnSpacing) { if usesErrorLayout { Text(self.model.subtitleText) .font(.footnote) .lineLimit(4) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 4) .hidden() .overlay(alignment: .topLeading) { Text(liveSubtitle.text) .font(.footnote) .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) .lineLimit(4) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) } .clipped() .layoutPriority(1) } else { Text(liveSubtitle.text) .font(.footnote) .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) .lineLimit(1) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .layoutPriority(1) } Spacer() if usesErrorLayout { let showsCopyButton = liveSubtitle.style == .error && !liveSubtitle.text.isEmpty CopyIconButton( copyText: liveSubtitle.text, isHighlighted: self.isHighlighted, isInteractive: showsCopyButton) .opacity(showsCopyButton ? 1 : 0) .allowsHitTesting(showsCopyButton) .accessibilityHidden(!showsCopyButton) } if let plan = self.model.planText { Group { if let planAction { Button(action: planAction) { Text(plan) } .buttonStyle(.plain) .menuCardInteractiveControl() .accessibilityLabel(plan) } else { Text(plan) } } .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } } } private var liveSubtitle: MenuCardLiveSubtitle { let fallback = MenuCardLiveSubtitle(text: self.model.subtitleText, style: self.model.subtitleStyle) guard self.model.usesLiveSubtitle else { return fallback } return self.refreshMonitor?.subtitle(for: self.model.provider, fallback: fallback) ?? fallback } private func subtitleColor(for style: UsageMenuCardView.Model.SubtitleStyle) -> Color { switch style { case .info: MenuHighlightStyle.secondary(self.isHighlighted) case .loading: MenuHighlightStyle.secondary(self.isHighlighted) case .error: MenuHighlightStyle.error(self.isHighlighted) } } } private struct CopyIconButtonStyle: ButtonStyle { let isHighlighted: Bool func makeBody(configuration: Configuration) -> some View { configuration.label .padding(4) .background { RoundedRectangle(cornerRadius: 4, style: .continuous) .fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(configuration.isPressed ? 0.18 : 0)) } .scaleEffect(configuration.isPressed ? 0.94 : 1) .animation(.easeOut(duration: 0.12), value: configuration.isPressed) } } private struct CopyIconButton: View { let copyText: String let isHighlighted: Bool let isInteractive: Bool @State private var didCopy = false @State private var resetTask: Task? var body: some View { Button { self.handleCopy() } label: { Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc") .font(.caption2.weight(.semibold)) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .frame(width: 18, height: 18) } .buttonStyle(CopyIconButtonStyle(isHighlighted: self.isHighlighted)) .menuCardInteractiveControl(isEnabled: self.isInteractive) .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy error")) } private func handleCopy() { let text = self.copyText self.resetTask?.cancel() MenuPasteboardCopy.perform(text, completion: { self.didCopy = true self.resetTask = Task { @MainActor in try? await Task.sleep(for: .seconds(0.9)) self.didCopy = false } }) } } private struct ProviderCostContent: View { let section: UsageMenuCardView.Model.ProviderCostSection let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 6) { Text(self.section.title) .font(.body) .fontWeight(.medium) if let percentUsed = self.section.percentUsed { UsageProgressBar( percent: percentUsed, tint: self.progressColor, accessibilityLabel: L("Extra usage spent")) } HStack(alignment: .firstTextBaseline) { Text(self.section.spendLine).font(.footnote).lineLimit(1) Spacer() if let percentLine = self.section.percentLine { Text(percentLine) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } if let personalSpendLine = self.section.personalSpendLine { Text(personalSpendLine) .font(.footnote).foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)).lineLimit(1) } } } } private struct MetricRow: View { let metric: UsageMenuCardView.Model.Metric let title: String let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 6) { Text(self.title) .font(.body) .fontWeight(.medium) if let statusText = self.metric.statusText { Text(statusText) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } else { UsageProgressBar( percent: self.metric.percent, tint: self.progressColor, accessibilityLabel: self.metric.percentStyle.accessibilityLabel, pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents, workdayMarkerPercents: self.metric.workdayMarkerPercents) VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline) { Text(self.metric.percentLabel) .font(.footnote) .lineLimit(1) Spacer() if let rightLabel = self.metric.resetText { Text(rightLabel) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } if self.metric.detailLeftText != nil || self.metric.detailRightText != nil { HStack(alignment: .firstTextBaseline) { if let detailLeft = self.metric.detailLeftText { Text(detailLeft) .font(.footnote) .foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted)) .lineLimit(1) } Spacer() if let detailRight = self.metric.detailRightText { Text(detailRight) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } } } .frame(maxWidth: .infinity, alignment: .leading) if let detail = self.metric.detailText { Text(detail) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(self.metric.cardStyle ? 10 : 0) .background(self.metric.cardStyle ? Color.secondary.opacity(self.isHighlighted ? 0.2 : 0.08) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: self.metric.cardStyle ? 10 : 0)) } } private struct UsageNotesContent: View { let notes: [String] @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 4) { ForEach(Array(self.notes.enumerated()), id: \.offset) { _, note in Text(note) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .leading) } } struct UsageMenuCardHeaderSectionView: View { let model: UsageMenuCardView.Model let showDivider: Bool let width: CGFloat var body: some View { VStack(alignment: .leading, spacing: UsageMenuCardLayout.headerContentSpacing) { UsageMenuCardHeaderView(model: self.model, planAction: nil) if self.showDivider { Divider() } } .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding(.top, UsageMenuCardLayout.headerOnlyVerticalPadding) .padding(.bottom, self.headerBottomPadding) .frame(width: self.width, alignment: .leading) } private var headerBottomPadding: CGFloat { if self.model.subtitleStyle == .error { return UsageMenuCardLayout.sectionBottomPadding } return self.showDivider ? UsageMenuCardLayout.sectionBottomPadding : UsageMenuCardLayout.headerOnlyVerticalPadding } } private struct UsageMenuCardUsageContentView: View { let model: UsageMenuCardView.Model let showBottomDivider: Bool @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 12) { ForEach(self.model.metrics, id: \.id) { metric in MetricRow( metric: metric, title: UsageMenuCardView.popupMetricTitle(provider: self.model.provider, metric: metric), progressColor: self.model.progressColor) } if let resetCredits = self.model.codexResetCredits { if !self.model.metrics.isEmpty { Divider() } CodexResetCreditsContent(presentation: resetCredits) } if let dashboard = self.model.inlineUsageDashboard { InlineUsageDashboardContent(model: dashboard) } else if !self.model.usageNotes.isEmpty { UsageNotesContent(notes: self.model.usageNotes) } else if let placeholder = self.model.placeholder, self.model.metrics.isEmpty, self.model.codexResetCredits == nil { Text(placeholder) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .font(.subheadline) } if self.showBottomDivider { Divider() } } } } struct UsageMenuCardUsageSectionView: View { let model: UsageMenuCardView.Model let showBottomDivider: Bool let bottomPadding: CGFloat let width: CGFloat @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { let liveModel = self.liveModel UsageMenuCardUsageContentView(model: liveModel, showBottomDivider: self.showBottomDivider) .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding(.top, UsageMenuCardLayout.usageSectionTopPadding) .padding(.bottom, self.bottomPadding) .frame(width: self.width, alignment: .leading) } private var liveModel: UsageMenuCardView.Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } } struct UsageMenuCardCreditsSectionView: View { let model: UsageMenuCardView.Model let showBottomDivider: Bool let topPadding: CGFloat let bottomPadding: CGFloat let width: CGFloat @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { let liveModel = self.liveModel if let credits = liveModel.creditsText { VStack(alignment: .leading, spacing: 6) { CreditsBarContent( creditsText: credits, creditsRemaining: liveModel.creditsRemaining, progressPercent: liveModel.creditsProgressPercent, scaleText: liveModel.creditsScaleText, hintText: liveModel.creditsHintText, hintCopyText: liveModel.creditsHintCopyText, progressColor: liveModel.progressColor) if self.showBottomDivider { Divider() } } .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding(.top, self.topPadding) .padding(.bottom, self.bottomPadding) .frame(width: self.width, alignment: .leading) } } private var liveModel: UsageMenuCardView.Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } } private struct CreditsBarContent: View { private static let fullScaleTokens: Double = 1000 let creditsText: String let creditsRemaining: Double? var progressPercent: Double?, scaleText: String? let hintText: String? let hintCopyText: String? let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted private var percentLeft: Double? { if let progressPercent { return min(100, max(0, progressPercent)) } guard let creditsRemaining else { return nil } let percent = (creditsRemaining / Self.fullScaleTokens) * 100 return min(100, max(0, percent)) } private var effectiveScaleText: String { if let scaleText { return scaleText } let scale = UsageFormatter.tokenCountString(Int(Self.fullScaleTokens)) return "\(scale) \(L("tokens"))" } var body: some View { VStack(alignment: .leading, spacing: 6) { Text(L("Credits")) .font(.body) .fontWeight(.medium) if let percentLeft { UsageProgressBar( percent: percentLeft, tint: self.progressColor, accessibilityLabel: L("Credits remaining")) HStack(alignment: .firstTextBaseline) { Text(self.creditsText) .font(.caption) .lineLimit(1) Spacer() Text(self.effectiveScaleText) .font(.caption) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) } } else { Text(self.creditsText) .font(.caption) } if let hintText, !hintText.isEmpty { Text(hintText) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) .overlay { ClickToCopyOverlay(copyText: self.hintCopyText ?? hintText) } } } } } struct UsageMenuCardCostSectionView: View { let model: UsageMenuCardView.Model let topPadding: CGFloat let bottomPadding: CGFloat let width: CGFloat @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { let liveModel = self.liveModel let hasTokenCost = liveModel.tokenUsage != nil return Group { if hasTokenCost { VStack(alignment: .leading, spacing: 10) { if let tokenUsage = liveModel.tokenUsage { VStack(alignment: .leading, spacing: 6) { Text(L("cost_header_estimated")) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) .font(.caption) .lineLimit(1) Text(tokenUsage.monthLine) .font(.caption) .lineLimit(1) ForEach(tokenUsage.comparisonLines, id: \.self) { line in Text(line) .font(.caption) .lineLimit(1) } if let hint = tokenUsage.hintLine, !hint.isEmpty { Text(hint) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } if let error = tokenUsage.errorLine, !error.isEmpty { Text(error) .font(.footnote) .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) .overlay { ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error) } } } } } .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding(.top, self.topPadding) .padding(.bottom, self.bottomPadding) .frame(width: self.width, alignment: .leading) } } } private var liveModel: UsageMenuCardView.Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } } struct UsageMenuCardExtraUsageSectionView: View { let model: UsageMenuCardView.Model let topPadding: CGFloat let bottomPadding: CGFloat let width: CGFloat @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { let liveModel = self.liveModel Group { if let providerCost = liveModel.providerCost { ProviderCostContent( section: providerCost, progressColor: liveModel.progressColor) .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding(.top, self.topPadding) .padding(.bottom, self.bottomPadding) .frame(width: self.width, alignment: .leading) } } } private var liveModel: UsageMenuCardView.Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } } // MARK: - Model factory extension UsageMenuCardView.Model { static func make(_ input: Input) -> UsageMenuCardView.Model { let planText = Self.plan( for: input.provider, snapshot: input.snapshot, account: input.account, override: input.planOverride, metadata: input.metadata) let metrics = Self.redactedMetrics( Self.metrics(input: input), provider: input.provider, hidePersonalInfo: input.hidePersonalInfo) let openAIAPIUsage = input.snapshot?.openAIAPIUsage let inlineUsageDashboard = Self.inlineUsageDashboard(input: input) let usageNotes = Self.usageNotes(input: input) let rawCreditsText: String? = if input.provider == .openrouter { nil } else if input.codexProjection != nil, !input.showOptionalCreditsAndExtraUsage { nil } else { Self.creditsLine( metadata: input.metadata, snapshot: input.snapshot, credits: input.credits, error: input.creditsError) } let creditsText = PersonalInfoRedactor.redactEmails(in: rawCreditsText, isEnabled: input.hidePersonalInfo) let creditsProgressPercent = Self.creditsProgressPercent(credits: input.credits) let creditsScaleText = Self.creditsScaleText(credits: input.credits) let codexCreditLimitDetail = Self.codexCreditLimitDetail(credits: input.credits, now: input.now) let isClaudeAdminAPI = input.provider == .claude && input.snapshot?.identity?.loginMethod == "Admin API" let isRequiredOpenCodeZenBalance = Self.isRequiredOpenCodeZenBalance(input.snapshot) let hidesOptionalProviderCost = ((input.provider == .claude && !isClaudeAdminAPI) || input.provider == .factory || input.provider == .devin || (input.provider == .opencodego && !isRequiredOpenCodeZenBalance)) && !input.showOptionalCreditsAndExtraUsage let providerCost: ProviderCostSection? = if input.provider == .sakana { input.showOptionalCreditsAndExtraUsage ? Self.sakanaPayAsYouGoSection(input.snapshot?.sakanaPayAsYouGo) : nil } else if hidesOptionalProviderCost || (input.provider == .openai && openAIAPIUsage != nil) { nil } else { Self.providerCostSection(provider: input.provider, cost: input.snapshot?.providerCost) } let tokenUsageSnapshot = Self.tokenUsageSnapshot(input: input) let tokenUsage = Self.tokenUsageSection( provider: input.provider, enabled: input.tokenCostMenuSectionEnabled, comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled, snapshot: tokenUsageSnapshot, error: input.tokenError) let subtitle = Self.subtitle( snapshot: input.snapshot, isRefreshing: input.isRefreshing, lastError: Self.lastError(input: input), now: input.now) let redacted = Self.redactedText(input: input, subtitle: subtitle) let placeholder = Self.placeholder(input: input) return UsageMenuCardView.Model( provider: input.provider, providerName: input.metadata.displayName, email: redacted.email, subtitleText: redacted.subtitleText, subtitleStyle: subtitle.style, usesLiveSubtitle: input.usesLiveSubtitle, planText: planText, metrics: metrics, usageNotes: usageNotes, openAIAPIUsage: openAIAPIUsage, inlineUsageDashboard: inlineUsageDashboard, creditsText: creditsText, creditsRemaining: input.credits?.codexCreditLimit?.remaining ?? input.credits?.remaining, creditsProgressPercent: creditsProgressPercent, creditsScaleText: creditsScaleText, creditsHintText: codexCreditLimitDetail ?? redacted.creditsHintText, creditsHintCopyText: codexCreditLimitDetail ?? redacted.creditsHintCopyText, codexResetCredits: Self.codexResetCredits(input: input), providerCost: providerCost, tokenUsage: tokenUsage, placeholder: placeholder, progressColor: Self.progressColor(for: input.provider)) } static func openRouterSpendNotes(_ usage: OpenRouterUsageSnapshot) -> [String] { var parts: [String] = [] if let daily = usage.keyUsageDaily { parts.append("\(L("Today")): \(Self.openRouterCurrencyString(daily))") } if let weekly = usage.keyUsageWeekly { parts.append("\(L("This week")): \(Self.openRouterCurrencyString(weekly))") } guard !parts.isEmpty else { return [] } return [parts.joined(separator: " · ")] } private static func openRouterCurrencyString(_ value: Double) -> String { String(format: "$%.2f", value) } private static func email( for provider: UsageProvider, snapshot: UsageSnapshot?, account: AccountInfo, metadata: ProviderMetadata, accountIsAuthoritative: Bool) -> String { if let email = snapshot?.accountEmail(for: provider), !email.isEmpty { return email } if metadata.usesAccountFallback || accountIsAuthoritative, let email = account.email, !email.isEmpty { return email } return "" } private static func plan( for provider: UsageProvider, snapshot: UsageSnapshot?, account: AccountInfo, override: String?, metadata: ProviderMetadata) -> String? { if let override, !override.isEmpty { return override } if provider == .kiro, let plan = kiroPlan(snapshot: snapshot) { return plan } if provider == .kilo { guard let pass = self.kiloLoginPass(snapshot: snapshot) else { return nil } return self.planDisplay(pass, for: provider) } if let plan = snapshot?.loginMethod(for: provider), !plan.isEmpty { return self.planDisplay(plan, for: provider) } if metadata.usesAccountFallback, let plan = account.plan, !plan.isEmpty { return Self.planDisplay(plan, for: provider) } return nil } private static func planDisplay(_ text: String, for provider: UsageProvider) -> String { if provider == .minimax { return self.miniMaxPlanDisplay(text) } let cleaned = if provider == .codex { CodexPlanFormatting.displayName(text) ?? UsageFormatter.cleanPlanName(text) } else { UsageFormatter.cleanPlanName(text) } return cleaned.isEmpty ? text : cleaned } private static func miniMaxPlanDisplay(_ text: String) -> String { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = trimmed.lowercased() if normalized.contains("tokenplanplus") || normalized.contains("token plan plus") { return "Plus" } if normalized.contains("tokenplanmax") || normalized.contains("token plan max") { return "Max" } if normalized.contains("tokenplanultra") || normalized.contains("token plan ultra") { return "Ultra" } return trimmed } private static func kiloLoginPass(snapshot: UsageSnapshot?) -> String? { self.kiloLoginParts(snapshot: snapshot).pass } static func kiloLoginDetails(snapshot: UsageSnapshot?) -> [String] { self.kiloLoginParts(snapshot: snapshot).details } private static func kiloLoginParts(snapshot: UsageSnapshot?) -> (pass: String?, details: [String]) { guard let loginMethod = snapshot?.loginMethod(for: .kilo) else { return (nil, []) } let parts = loginMethod .components(separatedBy: "·") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } guard !parts.isEmpty else { return (nil, []) } let first = parts[0] if self.isKiloActivitySegment(first) { return (nil, parts) } return (first, Array(parts.dropFirst())) } private static func isKiloActivitySegment(_ text: String) -> Bool { let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return normalized.hasPrefix("auto top-up:") } private static func subtitle( snapshot: UsageSnapshot?, isRefreshing: Bool, lastError: String?, now: Date) -> (text: String, style: SubtitleStyle) { if let lastError, !lastError.isEmpty { return (lastError.trimmingCharacters(in: .whitespacesAndNewlines), .error) } if isRefreshing { return ("\(L("Refreshing"))…", .loading) } if let updated = snapshot?.updatedAt { return (UsageFormatter.updatedString(from: updated, now: now), .info) } return (L("Not fetched yet"), .info) } private struct RedactedText { let email: String let subtitleText: String let creditsHintText: String? let creditsHintCopyText: String? } private static func redactedText( input: Input, subtitle: (text: String, style: SubtitleStyle)) -> RedactedText { let email = PersonalInfoRedactor.redactEmail( Self.email( for: input.provider, snapshot: input.snapshot, account: input.account, metadata: input.metadata, accountIsAuthoritative: input.accountIsAuthoritative), isEnabled: input.hidePersonalInfo) let subtitleText = PersonalInfoRedactor.redactEmails(in: subtitle.text, isEnabled: input.hidePersonalInfo) ?? subtitle.text let creditsHintText = PersonalInfoRedactor.redactEmails( in: Self.dashboardHint(error: input.dashboardError), isEnabled: input.hidePersonalInfo) let creditsHintCopyText = Self.creditsHintCopyText( dashboardError: input.dashboardError, hidePersonalInfo: input.hidePersonalInfo) return RedactedText( email: email, subtitleText: subtitleText, creditsHintText: creditsHintText, creditsHintCopyText: creditsHintCopyText) } private static func creditsHintCopyText(dashboardError: String?, hidePersonalInfo: Bool) -> String? { guard let dashboardError, !dashboardError.isEmpty else { return nil } return hidePersonalInfo ? "" : dashboardError } private static func metrics(input: Input) -> [Metric] { guard let snapshot = input.snapshot else { return [] } if input.provider == .antigravity { return Self.antigravityMetrics(input: input, snapshot: snapshot) } if input.provider == .minimax { if let minimaxUsage = snapshot.minimaxUsage { let services = minimaxUsage.orderedQuotaServices if !services.isEmpty { return Self.minimaxMetrics(services: services, input: input) } } } var metrics: [Metric] = [] let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left let zaiUsage = input.provider == .zai ? snapshot.zaiUsage : nil let zaiTokenDetail = Self.zaiLimitDetailText(limit: zaiUsage?.tokenLimit) let zaiTimeDetail = Self.zaiLimitDetailText(limit: zaiUsage?.timeLimit) let zaiSessionDetail = Self.zaiLimitDetailText(limit: zaiUsage?.sessionTokenLimit) let openRouterQuotaDetail = Self.openRouterQuotaDetail(provider: input.provider, snapshot: snapshot) let labels = Self.rateWindowLabels(input: input, snapshot: snapshot) if input.provider == .mistral, let credits = snapshot.mistralUsage?.credits { metrics.append(Metric( id: "mistral-balance", title: L("Balance"), percent: 0, percentStyle: percentStyle, statusText: credits.formattedAvailableAmount, resetText: nil, detailText: nil, detailLeftText: nil, detailRightText: nil, pacePercent: nil, paceOnTop: true)) } if input.provider == .codex, let codexProjection = input.codexProjection { metrics.append(contentsOf: Self.codexRateMetrics( input: input, projection: codexProjection, percentStyle: percentStyle)) } else if let primary = snapshot.primary { metrics.append(Self.primaryMetric( input: input, primary: primary, percentStyle: percentStyle, title: labels.primary, zaiTokenDetail: zaiTokenDetail, openRouterQuotaDetail: openRouterQuotaDetail)) } if input.provider != .codex, let weekly = snapshot.secondary { metrics.append(Self.secondaryMetric( input: input, weekly: weekly, percentStyle: percentStyle, title: labels.secondary, zaiTimeDetail: zaiTimeDetail)) } if input.provider == .mimo, let mimoUsage = snapshot.mimoUsage { metrics.append(Metric( id: "mimo-balance", title: L("Balance"), percent: 0, percentStyle: percentStyle, statusText: mimoUsage.balanceDetail, resetText: nil, detailText: nil, detailLeftText: nil, detailRightText: nil, pacePercent: nil, paceOnTop: true)) } if labels.showsTertiary, let opus = snapshot.tertiary { var tertiaryDetailText: String? if input.provider == .alibaba || input.provider == .alibabatokenplan, let detail = opus.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { tertiaryDetailText = detail } if input.provider == .zai, let detail = zaiSessionDetail { tertiaryDetailText = detail } // Perplexity purchased credits don't reset; show balance without "Resets" prefix. let opusResetText: String? = input.provider == .perplexity || input.provider == .sub2api ? opus.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines) : Self.resetText(for: opus, style: input.resetTimeDisplayStyle, now: input.now) let tertiaryPaceDetail = Self.resetWindowPaceDetail(window: opus, input: input) metrics.append(Metric( id: "tertiary", title: labels.tertiary, percent: Self.clamped(input.usageBarsShowUsed ? opus.usedPercent : opus.remainingPercent), percentStyle: percentStyle, resetText: opusResetText, detailText: tertiaryDetailText, detailLeftText: tertiaryPaceDetail?.leftLabel, detailRightText: tertiaryPaceDetail?.rightLabel, pacePercent: tertiaryPaceDetail?.pacePercent, paceOnTop: tertiaryPaceDetail?.paceOnTop ?? true, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.weekly], showUsed: input.usageBarsShowUsed))) } metrics.append(contentsOf: Self.extraRateWindowMetrics( snapshot: snapshot, input: input, percentStyle: percentStyle)) if input.provider == .kilo || input.provider == .kimi, metrics.contains(where: { $0.id == "primary" }), metrics.contains(where: { $0.id == "secondary" }) { metrics.sort { lhs, rhs in let primarySecondaryOrder: [String: Int] = [ "secondary": 0, "primary": 1, ] return (primarySecondaryOrder[lhs.id] ?? Int.max) < (primarySecondaryOrder[rhs.id] ?? Int.max) } } if let codexProjection = input.codexProjection, codexProjection.supplementalMetrics.contains(.codeReview), let remaining = codexProjection.remainingPercent(for: .codeReview) { let percent = input.usageBarsShowUsed ? (100 - remaining) : remaining let resetText = codexProjection.limitWindow(for: .codeReview).flatMap { Self.resetText(for: $0, style: input.resetTimeDisplayStyle, now: input.now) } metrics.append(Metric( id: "code-review", title: L("Code review"), percent: Self.clamped(percent), percentStyle: percentStyle, resetText: resetText, detailText: nil, detailLeftText: nil, detailRightText: nil, pacePercent: nil, paceOnTop: true)) } return metrics } private static func primaryMetric( input: Input, primary: RateWindow, percentStyle: PercentStyle, title: String? = nil, zaiTokenDetail: String?, openRouterQuotaDetail: String?) -> Metric { var primaryDetailText: String? = input.provider == .zai ? zaiTokenDetail : nil var primaryResetText = Self.resetText(for: primary, style: input.resetTimeDisplayStyle, now: input.now) var primaryDetailLeft: String? var primaryDetailRight: String? if input.provider == .crof, let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty { primaryDetailRight = detail } if input.provider == .openrouter, let openRouterQuotaDetail { primaryResetText = openRouterQuotaDetail } if input.provider == .copilot, let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty { primaryDetailLeft = detail } if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .litellm].contains(input.provider), let detail = primary.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { primaryDetailText = detail } if input.provider == .sub2api { primaryResetText = primary.resetDescription } if let balance = Self.poeBalanceDetailText(input: input) { primaryDetailText = balance } if input.provider == .kiro, let kiroUsage = input.snapshot?.kiroUsage, kiroUsage.creditsTotal > 0 { let remaining = UsageFormatter.kiroCreditNumber(kiroUsage.creditsRemaining) let total = UsageFormatter.kiroCreditNumber(kiroUsage.creditsTotal) primaryDetailLeft = String(format: L("%@ of %@ credits left"), remaining, total) } if input.provider == .alibaba || input.provider == .alibabatokenplan || input.provider == .manus, let detail = primary.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { primaryDetailText = detail if input.provider == .manus { primaryResetText = nil } } if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .litellm].contains(input.provider), primary.resetsAt == nil { primaryResetText = nil } // Abacus: show credits as detail, compute pace on the primary monthly window var primaryPacePercent: Double? var primaryPaceOnTop = true if let paceDetail = Self.sessionPaceDetail( provider: input.provider, window: primary, now: input.now, showUsed: input.usageBarsShowUsed) { primaryDetailLeft = paceDetail.leftLabel primaryDetailRight = paceDetail.rightLabel primaryPacePercent = paceDetail.pacePercent primaryPaceOnTop = paceDetail.paceOnTop } if input.provider == .abacus { if let detail = primary.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { primaryDetailText = detail } if primary.resetsAt == nil { primaryResetText = nil } if let pace = input.weeklyPace { let paceDetail = Self.weeklyPaceDetail( provider: input.provider, window: primary, now: input.now, pace: pace, showUsed: input.usageBarsShowUsed) if let paceDetail { primaryDetailLeft = paceDetail.leftLabel primaryDetailRight = paceDetail.rightLabel primaryPacePercent = paceDetail.pacePercent primaryPaceOnTop = paceDetail.paceOnTop } } } else if let paceDetail = Self.resetWindowPaceDetail(window: primary, input: input) { primaryDetailLeft = paceDetail.leftLabel primaryDetailRight = paceDetail.rightLabel primaryPacePercent = paceDetail.pacePercent primaryPaceOnTop = paceDetail.paceOnTop } // Legacy request-based Cursor plans: surface the raw used/limit quota on its own line, // since the percentage bar and pace detail alone never spell out the request cap. if input.provider == .cursor, let requests = input.snapshot?.cursorRequests { primaryDetailText = String( format: L("Request quota: %@ / %@"), "\(requests.used)", "\(requests.limit)") } if input.provider == .synthetic, let regen = Self.syntheticRollingRegenDetail( window: primary, now: input.now, showUsed: input.usageBarsShowUsed) { primaryResetText = regen.resetText primaryDetailLeft = regen.pace.leftLabel primaryDetailRight = regen.pace.rightLabel primaryPacePercent = regen.pace.pacePercent primaryPaceOnTop = regen.pace.paceOnTop } let usesBalanceStatusText = input.provider == .deepseek let primaryStatusText = usesBalanceStatusText ? primaryDetailText : nil if usesBalanceStatusText { primaryDetailText = nil } return Metric( id: "primary", title: title ?? L(input.metadata.sessionLabel), percent: Self.clamped( input.usageBarsShowUsed ? primary.usedPercent : primary.remainingPercent), percentStyle: percentStyle, statusText: primaryStatusText, resetText: primaryResetText, detailText: primaryDetailText, detailLeftText: primaryDetailLeft, detailRightText: primaryDetailRight, pacePercent: primaryPacePercent, paceOnTop: primaryPaceOnTop, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.session], showUsed: input.usageBarsShowUsed)) } private static func secondaryMetric( input: Input, weekly: RateWindow, percentStyle: PercentStyle, title: String? = nil, zaiTimeDetail: String?) -> Metric { var paceDetail = Self.weeklyPaceDetail( provider: input.provider, window: weekly, now: input.now, pace: input.weeklyPace, showUsed: input.usageBarsShowUsed) var weeklyResetText = Self.resetText(for: weekly, style: input.resetTimeDisplayStyle, now: input.now) var weeklyDetailText: String? = input.provider == .zai ? zaiTimeDetail : nil if input.provider == .warp, let detail = weekly.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { weeklyResetText = nil weeklyDetailText = detail } if input.provider == .kilo || input.provider == .litellm, let detail = weekly.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { weeklyDetailText = detail if weekly.resetsAt == nil { weeklyResetText = nil } } if input.provider == .sub2api { weeklyResetText = weekly.resetDescription } if input.provider == .kiro, let kiroUsage = input.snapshot?.kiroUsage, let remaining = kiroUsage.bonusCreditsRemaining, let total = kiroUsage.bonusCreditsTotal { let remainingText = UsageFormatter.kiroCreditNumber(remaining) let totalText = UsageFormatter.kiroCreditNumber(total) paceDetail = PaceDetail( leftLabel: String(format: L("%@ of %@ bonus credits left"), remainingText, totalText), rightLabel: nil, pacePercent: nil, paceOnTop: true) } if input.provider == .alibaba || input.provider == .alibabatokenplan, let detail = weekly.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { weeklyDetailText = detail } if input.provider == .manus, let detail = weekly.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { weeklyDetailText = detail } if input.provider == .crof, let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty { weeklyResetText = detail } if input.provider == .copilot, let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty { paceDetail = PaceDetail(leftLabel: detail, rightLabel: nil, pacePercent: nil, paceOnTop: true) } if let cursorPaceDetail = Self.resetWindowPaceDetail( window: weekly, input: input, pace: input.weeklyPace) { paceDetail = cursorPaceDetail } // Perplexity bonus credits don't reset; show balance without "Resets" prefix. if input.provider == .perplexity, let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty { weeklyResetText = detail } if input.provider == .synthetic, let regen = Self.syntheticRegenDetail( weekly: weekly, cost: input.snapshot?.providerCost, now: input.now, showUsed: input.usageBarsShowUsed) { weeklyResetText = regen.resetText paceDetail = regen.pace } return Metric( id: "secondary", title: title ?? L(input.metadata.weeklyLabel), percent: Self.clamped(input.usageBarsShowUsed ? weekly.usedPercent : weekly.remainingPercent), percentStyle: percentStyle, statusText: nil, resetText: weeklyResetText, detailText: weeklyDetailText, detailLeftText: paceDetail?.leftLabel, detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, paceOnTop: paceDetail?.paceOnTop ?? true, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.weekly], showUsed: input.usageBarsShowUsed), workdayMarkerPercents: workDayMarkerPercents( workDays: input.workDaysPerWeek, windowMinutes: weekly.windowMinutes)) } private static func codexRateMetrics( input: Input, projection: CodexConsumerProjection, percentStyle: PercentStyle) -> [Metric] { projection.visibleRateLanes.compactMap { lane in guard let window = projection.rateWindow(for: lane) else { return nil } let title: String let id: String let paceDetail: PaceDetail? switch lane { case .session: title = L(input.metadata.sessionLabel) id = "primary" paceDetail = Self.sessionPaceDetail( provider: input.provider, window: window, now: input.now, showUsed: input.usageBarsShowUsed) case .weekly: title = L(input.metadata.weeklyLabel) id = "secondary" paceDetail = Self.weeklyPaceDetail( provider: input.provider, window: window, now: input.now, pace: Self.standardWeeklyPace(input: input, window: window), showUsed: input.usageBarsShowUsed) } return Metric( id: id, title: title, percent: Self.clamped(input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent), percentStyle: percentStyle, resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now), detailText: nil, detailLeftText: paceDetail?.leftLabel, detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, paceOnTop: paceDetail?.paceOnTop ?? true, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow], showUsed: input.usageBarsShowUsed), workdayMarkerPercents: lane == .weekly ? workDayMarkerPercents( workDays: input.workDaysPerWeek, windowMinutes: window.windowMinutes) : []) } } }