138 lines
5.5 KiB
Swift
138 lines
5.5 KiB
Swift
import CodexBarCore
|
||
import Foundation
|
||
|
||
enum UsagePaceText {
|
||
struct WeeklyDetail {
|
||
let leftLabel: String
|
||
let rightLabel: String?
|
||
let expectedUsedPercent: Double
|
||
let stage: UsagePace.Stage
|
||
}
|
||
|
||
private enum DetailContext {
|
||
case session
|
||
case weekly
|
||
}
|
||
|
||
static func weeklySummary(provider: UsageProvider, pace: UsagePace, now: Date = .init()) -> String {
|
||
let detail = self.weeklyDetail(provider: provider, pace: pace, now: now)
|
||
if let rightLabel = detail.rightLabel {
|
||
return L("Pace: %@ · %@", detail.leftLabel, rightLabel)
|
||
}
|
||
return L("Pace: %@", detail.leftLabel)
|
||
}
|
||
|
||
static func weeklyDetail(provider: UsageProvider, pace: UsagePace, now: Date = .init()) -> WeeklyDetail {
|
||
WeeklyDetail(
|
||
leftLabel: self.detailLeftLabel(for: pace),
|
||
rightLabel: self.detailRightLabel(for: pace, provider: provider, context: .weekly, now: now),
|
||
expectedUsedPercent: pace.expectedUsedPercent,
|
||
stage: pace.stage)
|
||
}
|
||
|
||
private static func detailLeftLabel(for pace: UsagePace) -> String {
|
||
let deltaValue = Int(abs(pace.deltaPercent).rounded())
|
||
if deltaValue == 0 {
|
||
return L("On pace")
|
||
}
|
||
switch pace.stage {
|
||
case .onTrack:
|
||
return L("On pace")
|
||
case .slightlyAhead, .ahead, .farAhead:
|
||
return L("%d%% in deficit", deltaValue)
|
||
case .slightlyBehind, .behind, .farBehind:
|
||
return L("%d%% in reserve", deltaValue)
|
||
}
|
||
}
|
||
|
||
private static func detailRightLabel(
|
||
for pace: UsagePace,
|
||
provider: UsageProvider,
|
||
context: DetailContext,
|
||
now: Date) -> String?
|
||
{
|
||
let etaLabel: String?
|
||
if pace.willLastToReset {
|
||
etaLabel = self.combinedLastsLabel(for: pace, provider: provider)
|
||
} else if let etaSeconds = pace.etaSeconds {
|
||
let etaText = Self.durationText(seconds: etaSeconds, now: now)
|
||
if context == .session {
|
||
etaLabel = etaText == "now" ? L("Projected empty now") : L("Projected empty in %@", etaText)
|
||
} else {
|
||
etaLabel = etaText == "now" ? L("Runs out now") : L("Runs out in %@", etaText)
|
||
}
|
||
} else {
|
||
etaLabel = nil
|
||
}
|
||
|
||
guard let runOutProbability = pace.runOutProbability else { return etaLabel }
|
||
let roundedRisk = self.roundedRiskPercent(runOutProbability)
|
||
let riskLabel = L("≈ %d%% run-out risk", roundedRisk)
|
||
if pace.willLastToReset, roundedRisk > 0 {
|
||
return riskLabel
|
||
}
|
||
if let etaLabel {
|
||
return L("%@ · %@", etaLabel, riskLabel)
|
||
}
|
||
return riskLabel
|
||
}
|
||
|
||
private static func combinedLastsLabel(for pace: UsagePace, provider: UsageProvider) -> String {
|
||
guard provider == .codex else { return L("Lasts until reset") }
|
||
guard let speedLabel = self.speedHintLabel(for: pace) else {
|
||
return L("Lasts until reset")
|
||
}
|
||
return L("%@ · %@", L("Lasts until reset"), speedLabel)
|
||
}
|
||
|
||
private static func speedHintLabel(for pace: UsagePace) -> String? {
|
||
guard pace.deltaPercent < -15,
|
||
let multiplier = pace.speedMultiplierToReset,
|
||
multiplier >= 1.5
|
||
else { return nil }
|
||
return L("1.5× headroom")
|
||
}
|
||
|
||
private static func durationText(seconds: TimeInterval, now: Date) -> String {
|
||
let date = now.addingTimeInterval(seconds)
|
||
let countdown = UsageFormatter.resetCountdownDescription(from: date, now: now)
|
||
if countdown == "now" { return "now" }
|
||
if countdown.hasPrefix("in ") { return String(countdown.dropFirst(3)) }
|
||
return countdown
|
||
}
|
||
|
||
private static func roundedRiskPercent(_ probability: Double) -> Int {
|
||
let percent = probability.clamped(to: 0...1) * 100
|
||
let rounded = (percent / 5).rounded() * 5
|
||
return Int(rounded)
|
||
}
|
||
|
||
static func sessionPace(provider: UsageProvider, window: RateWindow, now: Date) -> UsagePace? {
|
||
guard provider == .codex || provider == .claude || provider == .ollama || provider == .antigravity
|
||
else { return nil }
|
||
if provider == .ollama, window.windowMinutes == nil { return nil }
|
||
if provider == .antigravity, let windowMinutes = window.windowMinutes, windowMinutes != 300 { return nil }
|
||
guard window.remainingPercent > 0 else { return nil }
|
||
guard let pace = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 300) else { return nil }
|
||
guard pace.expectedUsedPercent >= 3 else { return nil }
|
||
return pace
|
||
}
|
||
|
||
static func sessionDetail(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> WeeklyDetail? {
|
||
guard let pace = sessionPace(provider: provider, window: window, now: now) else { return nil }
|
||
return WeeklyDetail(
|
||
leftLabel: Self.detailLeftLabel(for: pace),
|
||
rightLabel: Self.detailRightLabel(for: pace, provider: provider, context: .session, now: now),
|
||
expectedUsedPercent: pace.expectedUsedPercent,
|
||
stage: pace.stage)
|
||
}
|
||
|
||
static func sessionSummary(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> String? {
|
||
guard let detail = sessionDetail(provider: provider, window: window, now: now) else { return nil }
|
||
if let rightLabel = detail.rightLabel {
|
||
return L("Pace: %@ · %@", detail.leftLabel, rightLabel)
|
||
}
|
||
return L("Pace: %@", detail.leftLabel)
|
||
}
|
||
}
|