Files
Peter Steinberger fb66e63e87 refactor: move usage presentation policy into provider descriptors (#2688)
* test: pin provider presentation policy

* refactor: move usage window policy into descriptors

* refactor: move usage menu policy into descriptors

---------

Co-authored-by: Peter Steinberger <steipete@clawstudio.local>
2026-08-05 16:04:53 -07:00

181 lines
7.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import CodexBarCore
import Foundation
enum UsagePaceText {
struct WeeklyDetail {
let leftLabel: String
let rightLabel: String?
let expectedUsedPercent: Double
let stage: UsagePace.Stage
}
struct SessionEquivalentDetail: Equatable {
let leftText: String
let rightText: String
let accessibilityLabel: String
}
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)
}
static func sessionEquivalentDetail(forecast: SessionEquivalentForecast) -> SessionEquivalentDetail {
let leftText = Self.sessionQuotaEstimateText(forecast.estimatedWindowsToExhaustWeekly)
let rightText = Self.windowsUntilResetText(forecast.windowsUntilReset)
return SessionEquivalentDetail(
leftText: leftText,
rightText: rightText,
accessibilityLabel: L("%@ · %@", leftText, rightText))
}
private static func sessionQuotaEstimateText(_ value: Double) -> String {
let displayedEstimate: String
let unit: String
if value.isFinite, value > 0 {
let boundedValue = min(value, 1_000_000)
let roundedValue = (boundedValue * 10).rounded() / 10
displayedEstimate = roundedValue.formatted(
.number
.precision(.fractionLength(0...1))
.locale(codexBarLocalizedLocale()))
unit = roundedValue > 0 && roundedValue <= 1 ? L("session quota") : L("session quotas")
} else {
displayedEstimate = codexBarLocalizedInteger(0)
unit = L("session quotas")
}
let estimateValue = L("session_quota_estimate_value_format", displayedEstimate, unit)
return L("Estimated: %@", L("%@ left", estimateValue))
}
private static func windowsUntilResetText(_ count: Int) -> String {
let combinedText = String(
format: L("≈%d full 5h windows of weekly left · %d windows until reset"),
locale: codexBarLocalizedResourceLocale(),
arguments: [0, count])
guard let separatorRange = combinedText.range(of: " · ") else { return combinedText }
return String(combinedText[separatorRange.upperBound...])
}
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%% risk)", roundedRisk)
if let etaLabel {
return L("%@ %@", etaLabel, riskLabel)
}
return riskLabel
}
private static func combinedLastsLabel(for pace: UsagePace, provider: UsageProvider) -> String {
let paceCapability = ProviderDescriptorRegistry.descriptor(for: provider).pace
guard paceCapability.showsHeadroomHint 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? {
let paceCapability = ProviderDescriptorRegistry.descriptor(for: provider).pace
guard paceCapability.supportsSessionPace(window: window, now: now) else { 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)
}
}