Files
steipete--codexbar/Sources/CodexBar/MenuCardView+ModelHelpers.swift
T
2026-07-13 12:22:33 +08:00

934 lines
37 KiB
Swift

import CodexBarCore
import SwiftUI
extension UsageMenuCardView.Model {
struct PaceDetail {
let leftLabel: String
let rightLabel: String?
let pacePercent: Double?
let paceOnTop: Bool
}
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)
}
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,
paceOnTop: metric.paceOnTop,
warningMarkerPercents: metric.warningMarkerPercents,
workdayMarkerPercents: metric.workdayMarkerPercents,
cardStyle: metric.cardStyle)
}
}
static func usageNotes(input: Input) -> [String] {
let subscriptionNotes = self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider)
if input.provider == .sub2api {
return self.sub2APIUsageNotes(input.snapshot?.sub2APIUsage) + subscriptionNotes
}
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 let notes = self.apiProviderUsageNotes(input: input) {
return notes + subscriptionNotes
}
if input.provider == .crossmodel, let crossModel = input.snapshot?.crossModelUsage {
return Self.crossModelSpendNotes(crossModel) + subscriptionNotes
}
guard input.provider == .openrouter,
let openRouter = input.snapshot?.openRouterUsage
else {
return subscriptionNotes
}
var notes = Self.openRouterSpendNotes(openRouter)
switch openRouter.keyQuotaStatus {
case .available:
break
case .noLimitConfigured:
notes.append(L("No limit set for the API key"))
case .unavailable:
notes.append(L("API key limit unavailable right now"))
}
return notes + subscriptionNotes
}
var isOverviewErrorOnly: Bool {
self.subtitleStyle == .error &&
self.metrics.isEmpty &&
self.usageNotes.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.openAIAPIUsage != nil ||
self.inlineUsageDashboard != 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.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.comparisonLines.count == candidate.comparisonLines.count
default:
false
}
}
static func progressColor(for provider: UsageProvider) -> Color {
if provider == .elevenlabs {
return Color(nsColor: .labelColor)
}
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
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 ("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.cursorRequests != nil {
"Requests"
} else if input.provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .sub2api {
Sub2APIProviderDescriptor.primaryLabel(details: snapshot.sub2APIUsage) ?? input.metadata.sessionLabel
} else {
input.metadata.sessionLabel
}
return (
L(primaryLabel),
L(input.metadata.weeklyLabel),
input.metadata.opusLabel.map(L) ?? L("Sonnet"),
input.metadata.supportsOpus)
}
static func sub2APIUsageNotes(_ usage: Sub2APIUsageDetails?) -> [String] {
guard let usage else { return [] }
var notes: [String] = []
if let balance = usage.balance {
notes.append("\(L("Balance")): \(UsageFormatter.currencyString(balance, currencyCode: usage.unit))")
}
if let today = usage.today {
notes.append("\(L("Today")): \(self.sub2APITotalsText(today, unit: usage.unit))")
}
if let total = usage.total {
notes.append("\(L("Total")): \(self.sub2APITotalsText(total, unit: usage.unit))")
}
return notes
}
private static func sub2APITotalsText(_ totals: Sub2APIUsageDetails.Totals, unit: String) -> String {
"\(UsageFormatter.tokenCountString(totals.requests)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(totals.totalTokens)) \(L("tokens")) · " +
UsageFormatter.currencyString(totals.actualCostUSD, currencyCode: unit)
}
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
}
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input, lastError: 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 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 input.limitsAvailability?.isUnavailable == true {
return true
}
return self.rateLimitsUnavailable(input: input, lastError: currentError)
}
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)
}
static func weeklyPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
pace: UsagePace?,
showUsed: Bool) -> PaceDetail?
{
guard let pace 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)
}
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?
{
guard self.supportsResetWindowPace(provider: input.provider, window: window),
window.remainingPercent > 0
else { return nil }
let paceWindow = Self.resetWindowForPace(provider: input.provider, window: window)
let resolved = pace ?? 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 let monthlyWindowSentinelMinutes = 30 * 24 * 60
private static func supportsResetWindowPace(provider: UsageProvider, window: RateWindow) -> Bool {
switch provider {
case .cursor:
window.windowMinutes != nil
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
window.windowMinutes == self.monthlyWindowSentinelMinutes
default:
false
}
}
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.
guard self.usesInferredMonthlyDuration(provider: provider, window: window),
let resetsAt = window.resetsAt,
let minutes = self.inferredMonthlyWindowMinutes(endingAt: resetsAt)
else { return window }
return RateWindow(
usedPercent: window.usedPercent,
windowMinutes: minutes,
resetsAt: window.resetsAt,
resetDescription: window.resetDescription,
nextRegenPercent: window.nextRegenPercent,
isSyntheticPlaceholder: window.isSyntheticPlaceholder)
}
private static func usesInferredMonthlyDuration(provider: UsageProvider, window: RateWindow) -> Bool {
guard window.windowMinutes == self.monthlyWindowSentinelMinutes else { return false }
switch provider {
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
return true
default:
return false
}
}
private static func inferredMonthlyWindowMinutes(endingAt resetsAt: Date) -> Int? {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? calendar.timeZone
guard let startsAt = calendar.date(byAdding: .month, value: -1, to: resetsAt) else { return nil }
let minutes = resetsAt.timeIntervalSince(startsAt) / 60
guard minutes.isFinite, minutes > 0 else { return nil }
return Int(minutes.rounded())
}
static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {
let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
if Self.hasAntigravityQuotaSummaryWindows(snapshot) {
return Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle)
}
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 []
}
let visibleRateWindows = if input.provider == .codex, !input.codexSparkUsageVisible {
extraRateWindows.filter { !Self.isCodexSparkRateWindow($0) }
} else {
extraRateWindows
}
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 = input.provider == .sub2api
? namedWindow.window.resetDescription
: nil
let statusText: String? = if usageKnown {
nil
} else if let resetText {
"\(L("Unavailable")) - \(resetText)"
} else {
L("Unavailable")
}
return Metric(
id: namedWindow.id,
title: namedWindow.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: usageKnown ? paceDetail?.leftLabel : nil,
detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
}
private static func isCodexSparkRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWindowID ||
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWeeklyWindowID
}
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?
{
guard provider == .codex || 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,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
static func zaiLimitDetailText(limit: ZaiLimitEntry?) -> String? {
guard let limit else { return nil }
if let currentValue = limit.currentValue,
let usage = limit.usage,
let remaining = limit.remaining
{
let currentStr = UsageFormatter.tokenCountString(currentValue)
let usageStr = UsageFormatter.tokenCountString(usage)
let remainingStr = UsageFormatter.tokenCountString(remaining)
return String(format: L("%@ / %@ (%@ remaining)"), currentStr, usageStr, remainingStr)
}
return nil
}
static func crossModelSpendNotes(_ usage: CrossModelUsageSnapshot) -> [String] {
let candidates: [(String, Double?)] = [
(L("Today"), usage.daily?.cost),
(L("This week"), usage.weekly?.cost),
(L("This month"), usage.monthly?.cost),
]
let rendered = candidates.compactMap { candidate -> String? in
guard let value = candidate.1 else { return nil }
return "\(candidate.0): \(usage.currencyString(value))"
}
guard !rendered.isEmpty else { return [] }
return [rendered.joined(separator: " · ")]
}
static func openRouterQuotaDetail(provider: UsageProvider, snapshot: UsageSnapshot) -> String? {
guard provider == .openrouter,
let usage = snapshot.openRouterUsage,
usage.hasValidKeyQuota,
let keyRemaining = usage.keyRemaining,
let keyLimit = usage.keyLimit
else {
return nil
}
let remaining = UsageFormatter.usdString(keyRemaining)
let limit = UsageFormatter.usdString(keyLimit)
return String(format: L("%@/%@ left"), remaining, limit)
}
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))
}
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))
}
}