Files
steipete--codexbar/Sources/CodexBar/MenuCardView+ModelHelpers.swift
sf-jin-ku d2ef26296a fix(codex): show Business monthly credit on stacked cards (#3112)
* fix(codex): show Business monthly credit on stacked cards

Stacked account fetches skipped the spend-controls credits path, so
Enterprise/Business cards showed Limits not available instead of used vs cap.

* docs: credit #3112 changelog entry

* fix(codex): keep cached monthly limits after spend-controls failures

Transient enrichment errors were persisted as missing credits, so stacked Business cards fell back to “Limits not available.” Restore the unreleased changelog heading as well.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): clear published credits when the monthly cap is gone

Successful spend-controls absence skipped publication and left the old cap on the selected account. Retarget gatekeeper fingerprints after that cluster split.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): keep monthly-only usage out of reset-credit rescue

A transient spend-controls failure still produced a success with no windows, and OAuth reset-credit rescue converted that into noRateLimitsFound before the cached monthly cap could be preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): treat unmappable monthly limits as enrichment failures

A nested spend-controls limit that is present but not numeric is no longer treated as confirmed absence, so a cached Business cap is preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): honor disabled caps and unmappable monthly usage

Inactive enforcement still confirms absence even if limit JSON is malformed, and a non-numeric current_month_usage is treated as enrichment failure instead of zero used.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): let an absent monthly cap win over malformed usage

Malformed current_month_usage only counts as enrichment failure when an active numeric limit actually needs that used amount.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): hide monthly credit when optional credits are off

Override-derived monthly lanes now follow Show credits + extra usage, so stacked Business cards no longer keep a credit meter the user asked to hide.

* fix(codex): include monthly credits in compact account planning

Inactive compact rows now see per-account monthly caps so an exhausted Business limit cannot hide behind a window-less usage snapshot.

* fix(codex): preserve only the monthly cap after enrichment failure

When spend-controls fails and usage confirmed there is no generic credit balance, keep the cached monthly limit without republishing stale remaining/events.

* fix(codex): fail monthly enrichment when account id or enforcement is unknown

A gated spend-controls fetch without an account id, or a non-string enforcement_mode, is now inconclusive so a cached Business cap is not cleared or published as active.

* fix(codex): hide menu-bar monthly credit when optional credits are off

Menu-bar projections now omit the optional monthly cap when the credits toggle is off, and a mappable nonpositive limit confirms cap removal even if enforcement is unreadable.

* fix(codex): reject non-finite monthly credit values

NaN and Infinity usage or limit strings are treated as mapping failures so they cannot zero out cached usage or persist invalid credit snapshots.

* fix(codex): keep monthly cap through standalone credits refresh

When spend-controls enrichment fails, the follow-up credits refresh now merges the prior monthly limit instead of publishing a generic-only snapshot.

* fix(codex): clear generic credits when monthly enrichment fails

A standalone credits refresh that confirms the generic balance is gone now publishes nil instead of throwing, so stale credits are not restored while a monthly cap is still preserved when one exists.

* fix(codex): retarget snapshot-store architecture suppressions

The monthly-cap helper shifted the Codex identity anchors, so the provider-architecture allowlist now points at the new lines.

* fix(codex): fetch monthly credits when plan type is unknown

Spend-control accounts with a missing or unmapped plan type now query the monthly endpoint, so a cached Business cap is not treated as confirmed removal.

* fix(codex): hydrate cached monthly credits and clear stale generic balance

Offline startup left the menu-bar projection without the disk-cached cap, and enrichment failure kept a generic balance when the monthly cap was already absent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): persist standalone credits onto account snapshots

Usage refresh stored the selected account snapshot before the credits pass, so a later cap add or removal never reached disk and offline hydration replayed the stale value.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): persist credits using workspace identity

Same-email workspace accounts made the snapshot write a no-op; match the published cap with the scoped refresh identity so only the selected workspace row is stored.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger CI

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-21 16:18:15 -07:00

1244 lines
50 KiB
Swift

import CodexBarCore
import SwiftUI
extension UsageMenuCardView.Model {
/// Resolves the displayed primary percentage/reset through the provider's binding quotas.
/// Primary-owned detail text remains sourced from the raw primary window.
static func bindingQuotaProjection(
input: Input,
primary: RateWindow,
snapshot: UsageSnapshot) -> RateWindowBindingQuotaProjection?
{
let lanes = ProviderDescriptorRegistry.descriptor(for: input.provider)
.presentation.primaryBindingQuotaLanes
let bindingWindows = lanes.compactMap { lane -> RateWindow? in
switch lane {
case .primary: nil
case .secondary: snapshot.secondary
case .tertiary: snapshot.tertiary
}
}
guard !bindingWindows.isEmpty else { return nil }
return RateWindow.bindingQuotaProjection(
primary: primary,
bindingLanes: bindingWindows,
now: input.now)
}
struct PaceDetail {
let leftLabel: String
let rightLabel: String?
let pacePercent: Double?
let paceOnTop: Bool
/// True only for text produced by a pace calculation. Defaults to false
/// so provider-owned text sharing the detail slot is preserved.
let isPaceDerived: Bool
init(
leftLabel: String,
rightLabel: String?,
pacePercent: Double?,
paceOnTop: Bool,
isPaceDerived: Bool = false)
{
self.leftLabel = leftLabel
self.rightLabel = rightLabel
self.pacePercent = pacePercent
self.paceOnTop = paceOnTop
self.isPaceDerived = isPaceDerived
}
}
struct PrimaryMetricPresentation {
var statusText: String?
var resetText: String?
var detailText: String?
var detailLeft: String?
var detailRight: String?
var pacePercent: Double?
var paceOnTop = true
var detailIsPaceDerived = false
}
static func applyPrimaryQuotaPresentation(
_ presentation: inout PrimaryMetricPresentation,
input: Input,
primary: RateWindow)
{
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
guard let detail = self.trimmedResetDescription(primary) else { return }
switch policy.primaryDescriptionPlacement {
case .reset:
presentation.resetText = detail
case .detailLeft:
presentation.detailLeft = detail
case .detail:
presentation.detailText = detail
case .detailBySecondaryPresence:
if input.snapshot?.secondary != nil {
presentation.detailRight = detail
} else {
presentation.detailText = detail
}
case .standard:
break
}
}
static func applyPrimaryBalancePresentation(
_ presentation: inout PrimaryMetricPresentation,
input: Input,
primary: RateWindow)
{
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
if policy.showsPrimaryBalanceDescription,
let detail = nonEmptyResetDescription(primary)
{
presentation.detailText = detail
}
switch policy.primaryDetailKind {
case .poeBalance:
if let balance = Self.poeBalanceDetailText(input: input) {
presentation.detailText = balance
}
case .kiroCredits:
if let remaining = input.snapshot?.detailRow(label: "Credits left")?.value,
let total = input.snapshot?.detailRow(label: "Credits total")?.value,
total != "0"
{
presentation.detailLeft = String(format: L("%@ of %@ credits left"), remaining, total)
}
case .none, .requestQuota:
break
}
if policy.clearsPrimaryReset {
presentation.resetText = nil
}
}
static func applyPrimaryResetPresentation(
_ presentation: inout PrimaryMetricPresentation,
input: Input,
primary: RateWindow)
{
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
if policy.usesRawPrimaryResetDescription {
presentation.resetText = primary.resetDescription
}
if policy.hidesPrimaryResetWithoutDate, primary.resetsAt == nil {
presentation.resetText = nil
}
if policy.hidesPrimaryResetWithoutSecondary, input.snapshot?.secondary == nil {
presentation.resetText = nil
}
}
static func applyPrimaryPacePresentation(
_ presentation: inout PrimaryMetricPresentation,
input: Input,
primary: RateWindow)
{
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
if let paceDetail = sessionPaceDetail(
provider: input.provider,
window: primary,
now: input.now,
showUsed: input.usageBarsShowUsed)
{
self.apply(paceDetail, to: &presentation)
}
if policy.usesAbacusPace {
if let detail = Self.nonEmptyResetDescription(primary) {
presentation.detailText = detail
}
if primary.resetsAt == nil {
presentation.resetText = nil
}
if let pace = input.weeklyPace,
let paceDetail = Self.weeklyPaceDetail(
provider: input.provider,
window: primary,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
{
Self.apply(paceDetail, to: &presentation)
}
} else if let paceDetail = Self.resetWindowPaceDetail(
window: primary,
input: input,
pace: policy.resetWindowUsesWeeklyPace ? input.weeklyPace : nil)
{
Self.apply(paceDetail, to: &presentation)
}
}
static func applyPrimaryFinalOverrides(
_ presentation: inout PrimaryMetricPresentation,
input: Input,
primary: RateWindow)
{
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
// Legacy request-based Cursor plans surface the raw used/limit quota on its own line.
if case .requestQuota = policy.primaryDetailKind,
let quota = input.snapshot?.detailRow(label: "Request quota")?.value
{
presentation.detailText = "\(L("Request quota")): \(quota)"
}
if policy.usesSyntheticRollingRegen,
let regen = Self.syntheticRollingRegenDetail(
window: primary,
now: input.now,
showUsed: input.usageBarsShowUsed)
{
presentation.resetText = regen.resetText
Self.apply(regen.pace, to: &presentation)
}
// Provider-specific by design: DeepSeek's balance description is provider-owned copy localized here.
if input.provider == .deepseek, let detail = presentation.detailText {
presentation.detailText = Self.localizedDeepSeekBalanceDescription(detail)
}
if policy.movesPrimaryDetailToStatus(snapshot: input.snapshot) {
presentation.statusText = presentation.detailText
presentation.detailText = nil
}
}
private static func apply(_ paceDetail: PaceDetail, to presentation: inout PrimaryMetricPresentation) {
presentation.detailLeft = paceDetail.leftLabel
presentation.detailRight = paceDetail.rightLabel
presentation.pacePercent = paceDetail.pacePercent
presentation.paceOnTop = paceDetail.paceOnTop
presentation.detailIsPaceDerived = paceDetail.isPaceDerived
}
private static func nonEmptyResetDescription(_ window: RateWindow) -> String? {
guard let detail = window.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
else { return nil }
return detail
}
private static func trimmedResetDescription(_ window: RateWindow) -> String? {
guard let detail = window.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty
else { return nil }
return detail
}
static func redactedMetricDetail(_ detail: String?, provider: UsageProvider, metricID: String) -> String? {
guard let detail else { return nil }
guard provider == .litellm,
metricID == "secondary",
detail.hasPrefix("Team "),
let separator = detail.range(of: ": ", options: .backwards)
else {
return PersonalInfoRedactor.redactEmails(in: detail, isEnabled: true)
}
return PersonalInfoRedactor.redactEmails(in: "Team\(detail[separator.lowerBound...])", isEnabled: true)
}
/// Clears the pace stripe and the forecast text when the user hides pace.
/// Copies every `Metric` field so unrelated decorations (quota and workday
/// ticks) survive; dropping one here would silently disable them.
static func paceGatedMetrics(_ metrics: [Metric], paceVisible: Bool) -> [Metric] {
guard !paceVisible else { return metrics }
return metrics.map { metric in
// The detail slots are shared: providers such as Kiro, Copilot, and
// ZenMux put their own credit and reset text there. Clear them only
// when they carry a pace forecast.
Metric(
id: metric.id,
title: metric.title,
percent: metric.percent,
percentStyle: metric.percentStyle,
statusText: metric.statusText,
resetText: metric.resetText,
detailText: metric.detailText,
detailLeftText: metric.detailIsPaceDerived ? nil : metric.detailLeftText,
detailRightText: metric.detailIsPaceDerived ? nil : metric.detailRightText,
pacePercent: nil,
detailIsPaceDerived: metric.detailIsPaceDerived,
paceOnTop: metric.paceOnTop,
warningMarkerPercents: metric.warningMarkerPercents,
workdayMarkerPercents: metric.workdayMarkerPercents,
workdayTickAppearance: metric.workdayTickAppearance,
cardStyle: metric.cardStyle,
sessionEquivalentDetail: nil)
}
}
static func redactedMetrics(
_ metrics: [Metric],
provider: UsageProvider,
hidePersonalInfo: Bool) -> [Metric]
{
guard hidePersonalInfo else { return metrics }
return metrics.map { metric in
Metric(
id: metric.id,
title: PersonalInfoRedactor.redactEmails(in: metric.title, isEnabled: true) ?? metric.title,
percent: metric.percent,
percentStyle: metric.percentStyle,
statusText: PersonalInfoRedactor.redactEmails(in: metric.statusText, isEnabled: true),
resetText: PersonalInfoRedactor.redactEmails(in: metric.resetText, isEnabled: true),
detailText: Self.redactedMetricDetail(
metric.detailText,
provider: provider,
metricID: metric.id),
detailLeftText: PersonalInfoRedactor.redactEmails(in: metric.detailLeftText, isEnabled: true),
detailRightText: PersonalInfoRedactor.redactEmails(in: metric.detailRightText, isEnabled: true),
pacePercent: metric.pacePercent,
detailIsPaceDerived: metric.detailIsPaceDerived,
paceOnTop: metric.paceOnTop,
warningMarkerPercents: metric.warningMarkerPercents,
workdayMarkerPercents: metric.workdayMarkerPercents,
workdayTickAppearance: metric.workdayTickAppearance,
cardStyle: metric.cardStyle,
sessionEquivalentDetail: metric.sessionEquivalentDetail)
}
}
static func usageNotes(input: Input) -> [String] {
let subscriptionNotes = self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider)
if input.provider == .kiro {
return self.kiroUsageNotes(input: input) + subscriptionNotes
}
if input.provider == .kilo {
var notes = Self.kiloLoginDetails(snapshot: input.snapshot)
let resolvedSource = input.sourceLabel?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if input.kiloAutoMode,
resolvedSource == "cli",
!notes.contains(where: { $0.caseInsensitiveCompare("Using CLI fallback") == .orderedSame })
{
notes.append(L("Using CLI fallback"))
}
return notes + subscriptionNotes
}
if input.provider == .mimo, input.snapshot != nil {
return Self.mimoUsageNotes(input: input, subscriptionNotes: subscriptionNotes)
}
if input.provider == .claude, input.snapshot?.dataConfidence == .percentOnly {
// CLI-scraped usage carries rendered percentages only; label the reduced fidelity honestly.
return [L("Usage via Claude CLI (limited detail)")] + subscriptionNotes
}
// Provider-specific by design: OpenCode Go local quota windows need an explicit authority warning.
if input.provider == .opencodego, input.snapshot?.dataConfidence == .estimated {
return [L("Quota estimated from local usage history")] + subscriptionNotes
}
if let notes = self.apiProviderUsageNotes(input: input) {
return notes + subscriptionNotes
}
return subscriptionNotes
}
var isOverviewErrorOnly: Bool {
self.subtitleStyle == .error &&
self.metrics.isEmpty &&
self.usageNotes.isEmpty &&
self.providerDetails.isEmpty &&
self.openAIAPIUsage == nil &&
self.inlineUsageDashboard == nil &&
self.creditsRemaining == nil &&
self.providerCost == nil &&
self.tokenUsage == nil &&
self.placeholder == nil
}
var hasUsageContent: Bool {
!self.metrics.isEmpty ||
!self.usageNotes.isEmpty ||
!self.providerDetails.isEmpty ||
self.openAIAPIUsage != nil ||
self.inlineUsageDashboard != nil ||
self.codexResetCredits != nil ||
self.placeholder != nil
}
var creditsOnlyInlineUsageDashboard: Bool {
self.creditsText != nil &&
self.inlineUsageDashboard != nil &&
self.metrics.isEmpty &&
self.usageNotes.isEmpty &&
self.providerDetails.isEmpty &&
self.openAIAPIUsage == nil &&
self.codexResetCredits == nil &&
self.placeholder == nil
}
var usesStackedDetailLayout: Bool {
!self.metrics.isEmpty ||
self.creditsText != nil ||
self.codexResetCredits != nil ||
self.providerCost != nil ||
self.tokenUsage != nil
}
func hasCompatibleTrackedLayout(with candidate: Self) -> Bool {
self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: true)
}
func hasCompatibleTrackedLayoutIgnoringMetrics(with candidate: Self) -> Bool {
self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: false)
}
func hasCompatibleTrackedMetricSubset(of candidate: Self) -> Bool {
guard self.metrics.count < candidate.metrics.count,
self.hasCompatibleTrackedLayoutIgnoringMetrics(with: candidate)
else {
return false
}
return self.metrics.allSatisfy { metric in
candidate.metrics.contains { Self.hasCompatibleMetricLayout(metric, $0) }
}
}
private func hasCompatibleTrackedLayout(with candidate: Self, includeMetrics: Bool) -> Bool {
guard self.provider == candidate.provider,
!includeMetrics || self.metrics.count == candidate.metrics.count,
self.usageNotes == candidate.usageNotes,
self.providerDetails == candidate.providerDetails,
(self.openAIAPIUsage == nil) == (candidate.openAIAPIUsage == nil),
Self.hasCompatibleCreditsLayout(
currentText: self.creditsText,
currentRemaining: self.creditsRemaining,
candidateText: candidate.creditsText,
candidateRemaining: candidate.creditsRemaining),
self.creditsHintText == candidate.creditsHintText,
self.codexResetCredits == candidate.codexResetCredits,
self.placeholder == candidate.placeholder,
Self.hasCompatibleDashboardLayout(self.inlineUsageDashboard, candidate.inlineUsageDashboard),
Self.hasCompatibleProviderCostLayout(self.providerCost, candidate.providerCost),
Self.hasCompatibleTokenUsageLayout(self.tokenUsage, candidate.tokenUsage)
else {
return false
}
guard includeMetrics else { return true }
return zip(self.metrics, candidate.metrics).allSatisfy(Self.hasCompatibleMetricLayout)
}
private static func hasCompatibleMetricLayout(_ current: Metric, _ candidate: Metric) -> Bool {
current.id == candidate.id &&
current.title == candidate.title &&
current.percentStyle == candidate.percentStyle &&
(current.statusText == nil) == (candidate.statusText == nil) &&
(current.resetText == nil) == (candidate.resetText == nil) &&
(current.detailText == nil) == (candidate.detailText == nil) &&
(current.detailLeftText == nil) == (candidate.detailLeftText == nil) &&
(current.detailRightText == nil) == (candidate.detailRightText == nil) &&
current.cardStyle == candidate.cardStyle
}
private static func hasCompatibleCreditsLayout(
currentText: String?,
currentRemaining: Double?,
candidateText: String?,
candidateRemaining: Double?) -> Bool
{
switch (currentText, candidateText) {
case (nil, nil):
return true
case let (currentText?, candidateText?):
guard (currentRemaining == nil) == (candidateRemaining == nil) else { return false }
// Numeric balances render as a fixed single line beside the full-scale label.
// Multiline workspace balances retain their measured text until the menu reopens.
return currentRemaining != nil || currentText == candidateText
default:
return false
}
}
private static func hasCompatibleDashboardLayout(
_ current: InlineUsageDashboardModel?,
_ candidate: InlineUsageDashboardModel?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.valueStyle == candidate.valueStyle &&
current.kpis.count == candidate.kpis.count &&
current.points.count == candidate.points.count &&
current.detailLines.count == candidate.detailLines.count &&
zip(current.kpis, candidate.kpis).allSatisfy {
$0.title == $1.title && $0.emphasis == $1.emphasis
} &&
zip(current.points, candidate.points).allSatisfy {
$0.id == $1.id && $0.label == $1.label
}
default:
false
}
}
private static func hasCompatibleProviderCostLayout(
_ current: ProviderCostSection?,
_ candidate: ProviderCostSection?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.title == candidate.title &&
(current.percentUsed == nil) == (candidate.percentUsed == nil) &&
(current.percentLine == nil) == (candidate.percentLine == nil) &&
(current.personalSpendLine == nil) == (candidate.personalSpendLine == nil)
default:
false
}
}
private static func hasCompatibleTokenUsageLayout(
_ current: TokenUsageSection?,
_ candidate: TokenUsageSection?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.hintLine == candidate.hintLine &&
current.errorLine == candidate.errorLine &&
(current.meteredLine == nil) == (candidate.meteredLine == nil) &&
current.comparisonLines.count == candidate.comparisonLines.count
default:
false
}
}
static func progressColor(for provider: UsageProvider) -> Color {
let branding = ProviderDescriptorRegistry.descriptor(for: provider).branding
if branding.progressColorStyle == .label {
return Color(nsColor: .labelColor)
}
let color = ProviderAccentPalette.color(for: provider)
return Color(red: color.red, green: color.green, blue: color.blue)
}
static func rateWindowLabels(
input: Input,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if input.provider == .factory, snapshot.tertiary != nil {
return (L("5-hour"), L("Weekly"), L("Monthly"), true)
}
// Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
let primaryLabel = if input.provider == .cursor, snapshot.detailRow(label: "Request quota") != nil {
"Requests"
} else if input.provider == .crof {
CrofProviderDescriptor.primaryLabel(snapshot: snapshot)
} else if input.provider == .grok {
GrokProviderDescriptor.displayLabel(window: snapshot.primary, now: input.now) ?? input.metadata.sessionLabel
} else if input.provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .sub2api {
Sub2APIProviderDescriptor.primaryLabel(snapshot: snapshot) ?? input.metadata.sessionLabel
} else if input.provider == .amp {
AmpProviderDescriptor.primaryLabel(snapshot: snapshot) ?? input.metadata.sessionLabel
} else if input.provider == .alibabatokenplan {
AlibabaTokenPlanProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else {
input.metadata.sessionLabel
}
let secondaryLabel = if input.provider == .amp {
AmpProviderDescriptor.secondaryLabel(snapshot: snapshot) ?? input.metadata.weeklyLabel
} else if input.provider == .alibabatokenplan {
AlibabaTokenPlanProviderDescriptor.secondaryLabel(window: snapshot.secondary) ?? input.metadata.weeklyLabel
} else if input.provider == .sub2api {
"Weekly"
} else {
input.metadata.weeklyLabel
}
let tertiaryLabel = input.provider == .sub2api
? L("Monthly")
: input.metadata.opusLabel.map(L) ?? L("Sonnet")
return (
localizedSessionQuotaLabel(primaryLabel, windowMinutes: snapshot.primary?.windowMinutes),
L(secondaryLabel),
tertiaryLabel,
input.metadata.supportsOpus)
}
static func sub2APILocalizedDetails(_ details: [ProviderDetailSection]) -> [ProviderDetailSection] {
details.map { section in
guard section.title == "Usage summary" else { return section }
do {
var rows: [ProviderDetailSection.Row] = []
var consumedLabels: Set<String> = []
if let balance = section.rows.first(where: { $0.label == "Balance" }) {
try rows.append(ProviderDetailSection.Row(
label: L("Balance"),
value: balance.value,
secondaryValue: balance.secondaryValue))
consumedLabels.insert(balance.label)
}
for period in [
(label: "Today", requests: "Today requests", tokens: "Today tokens"),
(label: "Total", requests: "All time requests", tokens: "All time tokens"),
] {
guard let requests = section.rows.first(where: { $0.label == period.requests }),
let tokens = section.rows.first(where: { $0.label == period.tokens })
else { continue }
var secondaryParts = ["\(tokens.value) \(L("tokens"))"]
if let cost = tokens.secondaryValue {
secondaryParts.append("\(L("Cost")): \(cost)")
}
try rows.append(ProviderDetailSection.Row(
label: L(period.label),
value: "\(requests.value) \(L("requests"))",
secondaryValue: secondaryParts.joined(separator: " · ")))
consumedLabels.formUnion([period.requests, period.tokens])
}
rows.append(contentsOf: section.rows.filter { !consumedLabels.contains($0.label) })
return try ProviderDetailSection(title: L("Usage"), rows: rows, chart: section.chart)
} catch {
return section
}
}
}
static func resetText(
for window: RateWindow,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
UsageFormatter.resetLine(for: window, style: style, now: now)
}
static func placeholder(input: Input) -> String? {
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input) {
return L("Limits not available")
}
if input.snapshot == nil, !input.isRefreshing, input.lastError == nil {
return self.hasLocalCodexTokenUsage(input) ? nil : L("No usage yet")
}
return nil
}
static func lastError(input: Input) -> String? {
guard let lastError = input.lastError?.trimmingCharacters(in: .whitespacesAndNewlines),
!lastError.isEmpty
else {
return nil
}
// Local Codex session costs are independent from OAuth, CLI quota, and OpenAI web
// dashboard access. Do not present a failed account-level quota fetch as a failure of
// a valid local API-key ledger.
if input.codexLocalSessionCostLedgerEnabled,
self.hasLocalCodexTokenUsage(input),
self.isRemoteCodexQuotaFetchError(lastError)
{
return nil
}
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input, lastError: lastError) {
return nil
}
if self.hasCodexCreditOrRateMeters(input) {
if UsageError.isNoRateLimitsFoundDescription(lastError)
|| ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(lastError)
{
return nil
}
}
return lastError
}
static func dashboardHint(error: String?) -> String? {
guard let error, !error.isEmpty else { return nil }
return error
}
static func mimoUsageNotes(input: Input, subscriptionNotes: [String]) -> [String] {
let source = input.sourceLabel?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
guard source != "local" else { return [] }
return [
L("Balance updates in near-real time (up to 5 min lag)"),
L("Daily billing data finalizes at 07:00 UTC"),
] + subscriptionNotes
}
static func subscriptionMetadataNotes(snapshot: UsageSnapshot?, provider: UsageProvider) -> [String] {
guard let snapshot else { return [] }
if let renewsAt = snapshot.subscriptionRenewsAt {
return [String(format: L("Renews: %@"), self.subscriptionDateString(renewsAt, provider: provider))]
}
if let expiresAt = snapshot.subscriptionExpiresAt {
return [String(format: L("Plan expires: %@"), self.subscriptionDateString(expiresAt, provider: provider))]
}
return []
}
private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
return formatter.string(from: date)
}
private static func subscriptionDateTimeZone(provider: UsageProvider) -> TimeZone {
switch provider {
case .minimax:
TimeZone(identifier: "Asia/Shanghai") ?? .current
default:
.current
}
}
static func poeBalanceDetailText(input: Input) -> String? {
guard input.provider == .poe else { return nil }
return StatusItemController.poeBalanceDisplayText(snapshot: input.snapshot)
}
private static func hasLocalCodexTokenUsage(_ input: Input) -> Bool {
input.provider == .codex &&
input.tokenCostUsageEnabled &&
self.tokenUsageSnapshot(input: input) != nil
}
private static func isRemoteCodexQuotaFetchError(_ error: String) -> Bool {
error.localizedCaseInsensitiveContains("Codex usage is temporarily unavailable")
}
private static func shouldShowRateLimitsUnavailablePlaceholder(input: Input, lastError: String? = nil) -> Bool {
let currentError = lastError ?? input.lastError
if let currentError = currentError?.trimmingCharacters(in: .whitespacesAndNewlines),
!currentError.isEmpty,
!UsageError.isNoRateLimitsFoundDescription(currentError),
!ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(currentError)
{
return false
}
if self.hasCodexCreditOrRateMeters(input) {
return false
}
if input.limitsAvailability?.isUnavailable == true {
return true
}
return self.rateLimitsUnavailable(input: input, lastError: currentError)
}
private static func hasCodexCreditOrRateMeters(_ input: Input) -> Bool {
if let lanes = input.codexProjection?.displayedRateLanes(
showOptionalCreditsAndExtraUsage: input.showOptionalCreditsAndExtraUsage),
!lanes.isEmpty
{
return true
}
guard input.showOptionalCreditsAndExtraUsage else { return false }
return input.credits?.codexCreditLimit != nil
}
private static func rateLimitsUnavailable(input: Input, lastError: String? = nil) -> Bool {
UsageLimitsAvailability.resolve(
provider: input.provider,
snapshot: input.snapshot,
account: input.account,
lastErrorDescription: lastError ?? input.lastError)
.isUnavailable
}
static func sessionPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
showUsed: Bool) -> PaceDetail?
{
guard let detail = UsagePaceText.sessionDetail(provider: provider, window: window, now: now) else { return nil }
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
if expectedPercent.isFinite == false || actualPercent.isFinite == false {
return nil
}
let paceOnTop = actualUsed <= expectedUsed
let pacePercent: Double? = if detail.stage == .onTrack {
nil
} else {
expectedPercent
}
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop,
isPaceDerived: true)
}
static func weeklyPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
pace: UsagePace?,
showUsed: Bool) -> PaceDetail?
{
guard let pace, window.remainingPercent > 0 else { return nil }
let detail = UsagePaceText.weeklyDetail(provider: provider, pace: pace, now: now)
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
if expectedPercent.isFinite == false || actualPercent.isFinite == false {
return nil
}
let paceOnTop = actualUsed <= expectedUsed
let pacePercent: Double? = if detail.stage == .onTrack {
nil
} else {
expectedPercent
}
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop,
isPaceDerived: true)
}
static func standardWeeklyPace(input: Input, window: RateWindow) -> UsagePace? {
if let weeklyPace = input.weeklyPace {
return weeklyPace
}
return Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
}
private static func displayableWeeklyPace(_ pace: UsagePace?) -> UsagePace? {
guard let pace else { return nil }
return pace.expectedUsedPercent >= 3 || pace.etaSeconds == 0 ? pace : nil
}
static func resetWindowPaceDetail(
window: RateWindow,
input: Input,
pace: UsagePace? = nil) -> PaceDetail?
{
let capability = ProviderDescriptorRegistry.descriptor(for: input.provider).pace
guard capability.supportsResetWindowPace(window: window, now: input.now),
window.remainingPercent > 0
else { return nil }
let paceWindow = Self.resetWindowForPace(provider: input.provider, window: window)
// A caller-supplied pace was measured against the raw window, so reuse it only when resolution
// left the duration alone. Trusting it for a monthly sentinel would score the billing period as
// a flat 30 days and silently undo the calendar-cycle resolution one line above.
let reusablePace = paceWindow.windowMinutes == window.windowMinutes ? pace : nil
let resolved = reusablePace ?? UsagePace.weekly(
window: paceWindow,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek)
guard let resolved = Self.displayableWeeklyPace(resolved) else { return nil }
return Self.weeklyPaceDetail(
provider: input.provider,
window: paceWindow,
now: input.now,
pace: resolved,
showUsed: input.usageBarsShowUsed)
}
private static func resetWindowForPace(provider: UsageProvider, window: RateWindow) -> RateWindow {
// Provider snapshots use 30 days as a monthly sentinel; use the reset date for the real calendar-cycle length.
ProviderDescriptorRegistry.descriptor(for: provider).pace.resolvedResetWindowForPace(window)
}
static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {
let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
if Self.hasAntigravityQuotaSummaryWindows(snapshot) {
let metrics = Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle)
guard !input.showsAllUsageLanes else { return metrics }
let idleIDs = AntigravityQuotaFamilyVisibility.idleWindowIDs(in: snapshot)
return idleIDs.isEmpty ? metrics : metrics.filter { !idleIDs.contains($0.id) }
}
var metrics: [Metric] = []
if let primary = snapshot.primary {
metrics.append(Self.antigravityMetric(
id: "primary",
title: L(input.metadata.sessionLabel),
window: primary,
input: input,
percentStyle: percentStyle))
}
if let secondary = snapshot.secondary {
metrics.append(Self.antigravityMetric(
id: "secondary",
title: L(input.metadata.weeklyLabel),
window: secondary,
input: input,
percentStyle: percentStyle))
}
if input.metadata.supportsOpus, let tertiary = snapshot.tertiary {
metrics.append(Self.antigravityMetric(
id: "tertiary",
title: input.metadata.opusLabel.map(L) ?? L("Gemini Flash"),
window: tertiary,
input: input,
percentStyle: percentStyle))
}
metrics.append(contentsOf: Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle))
return metrics
}
static func extraRateWindowMetrics(
snapshot: UsageSnapshot,
input: Input,
percentStyle: PercentStyle) -> [Metric]
{
guard let extraRateWindows = snapshot.extraRateWindows else { return [] }
// Codex additional limits (e.g. Codex Spark) are optional extra usage and follow the
// "optional credits and extra usage" setting. Other providers' extra windows (Antigravity
// per-model quotas, Factory core windows, etc.) are core data and must always render.
if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {
return []
}
if input.provider == .copilot, !input.copilotBudgetExtrasEnabled {
return []
}
var visibleRateWindows = if input.provider == .codex, !input.codexSparkUsageVisible {
extraRateWindows.filter { !Self.isCodexSparkRateWindow($0) }
} else {
extraRateWindows
}
if input.provider == .claude,
!input.showOptionalCreditsAndExtraUsage || !input.claudeDailyRoutinesUsageVisible
{
visibleRateWindows.removeAll(where: Self.isClaudeDailyRoutinesRateWindow)
}
return visibleRateWindows.map { namedWindow in
let paceDetail = Self.extraRateWindowPaceDetail(
provider: input.provider,
window: namedWindow.window,
input: input)
let usageKnown = namedWindow.usageKnown
let resolvedResetText = Self.extraRateWindowResetText(
namedWindow: namedWindow,
input: input)
let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil
? nil
: resolvedResetText
let detailText: String? = if input.provider == .sub2api {
namedWindow.window.resetDescription
} else {
nil
}
let statusText: String? = if usageKnown {
nil
} else if let resetText {
"\(L("Unavailable")) - \(resetText)"
} else {
L("Unavailable")
}
let title = input.provider == .doubao && namedWindow.id.contains("-team-")
? "\(L(namedWindow.title)) (\(L("Team")))"
: L(namedWindow.title)
// Provider-specific by design: Kiro overage remaining copy is unique to that extra window.
let detailLeftText: String? = if usageKnown {
Self.kiroOverageRemainingDetail(
snapshot: snapshot,
namedWindow: namedWindow,
provider: input.provider)
?? paceDetail?.leftLabel
} else {
nil
}
return Metric(
id: namedWindow.id,
title: title,
percent: Self.clamped(
input.usageBarsShowUsed
? namedWindow.window.usedPercent
: namedWindow.window.remainingPercent),
percentStyle: percentStyle,
statusText: statusText,
resetText: usageKnown ? resetText : nil,
detailText: usageKnown ? detailText : nil,
detailLeftText: detailLeftText,
detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
detailIsPaceDerived: paceDetail?.isPaceDerived ?? false,
paceOnTop: paceDetail?.paceOnTop ?? true,
sessionEquivalentDetail: usageKnown
? Self.sessionEquivalentDetail(
input: input,
weeklyWindow: namedWindow.window,
weeklyWindowID: namedWindow.id)
: nil)
}
}
private static func isCodexSparkRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWindowID ||
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWeeklyWindowID
}
private static func kiroOverageRemainingDetail(
snapshot: UsageSnapshot,
namedWindow: NamedRateWindow,
provider: UsageProvider) -> String?
{
guard provider == .kiro, namedWindow.id == "kiro-overage",
let remaining = snapshot.detailRow(label: "Overage credits left")?.value,
let capPhrase = snapshot.detailRow(label: "Overage usage")?.secondaryValue,
capPhrase.hasPrefix("of ")
else { return nil }
let total = String(capPhrase.dropFirst(3))
guard !total.isEmpty else { return nil }
return String(format: L("%@ of %@ credits left"), remaining, total)
}
private static func isClaudeDailyRoutinesRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id == "claude-routines"
}
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static func hasAntigravityQuotaSummaryWindows(_ snapshot: UsageSnapshot) -> Bool {
snapshot.extraRateWindows?.contains(where: self.isAntigravityQuotaSummaryWindow) == true
}
private static func isAntigravityQuotaSummaryWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id.hasPrefix(self.antigravityQuotaSummaryWindowIDPrefix)
}
private static func extraRateWindowResetText(
namedWindow: NamedRateWindow,
input: Input) -> String?
{
if namedWindow.window.resetsAt != nil {
return self.resetText(
for: namedWindow.window,
style: input.resetTimeDisplayStyle,
now: input.now)
}
if input.provider == .antigravity,
self.isAntigravityQuotaSummaryWindow(namedWindow)
{
return self.antigravityQuotaSummaryResetText(namedWindow.window.resetDescription)
}
return self.resetText(
for: namedWindow.window,
style: input.resetTimeDisplayStyle,
now: input.now)
}
private static func antigravityQuotaSummaryResetText(_ description: String?) -> String? {
guard let description = description?.trimmingCharacters(in: .whitespacesAndNewlines),
!description.isEmpty
else { return nil }
if let range = description.range(of: "fully refresh in ", options: .caseInsensitive) {
var suffix = String(description[range.upperBound...])
.trimmingCharacters(in: .whitespacesAndNewlines)
while suffix.last == "." {
suffix.removeLast()
}
guard !suffix.isEmpty else { return description }
return String(format: L("Resets in %@"), suffix)
}
return description
}
private static func extraRateWindowPaceDetail(
provider: UsageProvider,
window: RateWindow,
input: Input) -> PaceDetail?
{
if provider == .claude, window.windowMinutes != 10080 {
return nil
}
guard provider == .codex || provider == .claude || provider == .antigravity else { return nil }
switch window.windowMinutes {
case 300:
return self.sessionPaceDetail(
provider: provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
return Self.weeklyPaceDetail(
provider: provider,
window: window,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
default:
return nil
}
}
private static func antigravityMetricPaceDetail(
window: RateWindow,
input: Input) -> PaceDetail?
{
guard input.provider == .antigravity else { return nil }
switch window.windowMinutes {
case nil, 300:
return self.sessionPaceDetail(
provider: input.provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
return Self.weeklyPaceDetail(
provider: input.provider,
window: window,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
default:
return nil
}
}
static func antigravityMetric(
id: String,
title: String,
window: RateWindow?,
input: Input,
percentStyle: PercentStyle) -> Metric
{
guard let window else {
let placeholderPercent = input.usageBarsShowUsed ? 100.0 : 0.0
return Metric(
id: id,
title: title,
percent: placeholderPercent,
percentStyle: percentStyle,
statusText: nil,
resetText: nil,
detailText: nil,
detailLeftText: nil,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true)
}
let percent = input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent
let paceDetail = Self.antigravityMetricPaceDetail(window: window, input: input)
return Metric(
id: id,
title: title,
percent: Self.clamped(percent),
percentStyle: percentStyle,
resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now),
detailText: nil,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
pacePercent: paceDetail?.pacePercent,
detailIsPaceDerived: paceDetail?.isPaceDerived ?? false,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
static func syntheticRegenDetail(
weekly: RateWindow,
cost: ProviderCostSnapshot?,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let cost,
cost.limit > 0,
let nextRegenAmount = cost.nextRegenAmount,
nextRegenAmount > 0,
let resetsAt = weekly.resetsAt
else { return nil }
let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = String(format: L("Regenerates %@"), countdown)
let nextRegenPercent = (nextRegenAmount / cost.limit) * 100
let afterNextRegenRemaining = min(100, weekly.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? L("used after next regen") : L("after next regen")
let ticksToFull = max(0, cost.used) / nextRegenAmount
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let right = if ticksToFull <= 0.1 {
L("Near full")
} else if ticksToFull < 1.5 {
L("Full in ~1 regen")
} else {
String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
}
return (resetText, PaceDetail(
leftLabel: left,
rightLabel: right,
pacePercent: nil,
paceOnTop: true,
isPaceDerived: true))
}
static func syntheticRollingRegenDetail(
window: RateWindow,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let resetsAt = window.resetsAt,
let nextRegenPercent = window.nextRegenPercent,
nextRegenPercent > 0
else { return nil }
let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = String(format: L("Regenerates %@"), countdown)
let afterNextRegenRemaining = min(100, window.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? L("used after next regen") : L("after next regen")
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let missingPercent = max(0, window.usedPercent)
let ticksToFull = missingPercent / nextRegenPercent
let right = if ticksToFull <= 0.1 {
L("Near full")
} else if ticksToFull < 1.5 {
L("Full in ~1 regen")
} else {
String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
}
return (resetText, PaceDetail(
leftLabel: left,
rightLabel: right,
pacePercent: nil,
paceOnTop: true,
isPaceDerived: true))
}
}