237 lines
9.7 KiB
Swift
237 lines
9.7 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
#if canImport(WidgetKit)
|
|
import WidgetKit
|
|
#endif
|
|
|
|
extension UsageStore {
|
|
func persistWidgetSnapshot(reason: String) {
|
|
let snapshot = self.makeWidgetSnapshot()
|
|
let previousTask = self.widgetSnapshotPersistTask
|
|
self.widgetSnapshotPersistTask = Task { @MainActor in
|
|
_ = await previousTask?.result
|
|
|
|
if let override = self._test_widgetSnapshotSaveOverride {
|
|
await override(snapshot)
|
|
return
|
|
}
|
|
|
|
await Task.detached(priority: .utility) {
|
|
WidgetSnapshotStore.save(snapshot)
|
|
}.value
|
|
#if canImport(WidgetKit)
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private func makeWidgetSnapshot() -> WidgetSnapshot {
|
|
let now = Date()
|
|
let enabledProviders = self.enabledProviders()
|
|
let entries = UsageProvider.allCases.compactMap { provider in
|
|
self.makeWidgetEntry(for: provider, now: now)
|
|
}
|
|
return WidgetSnapshot(
|
|
entries: entries,
|
|
enabledProviders: enabledProviders,
|
|
usageBarsShowUsed: self.settings.usageBarsShowUsed,
|
|
generatedAt: now)
|
|
}
|
|
|
|
private func makeWidgetEntry(for provider: UsageProvider, now: Date) -> WidgetSnapshot.ProviderEntry? {
|
|
let snapshot = self.snapshots[provider]
|
|
let storedTokenSnapshot = self.tokenSnapshots[provider]
|
|
guard snapshot != nil || (provider == .claude && storedTokenSnapshot != nil) else { return nil }
|
|
|
|
let tokenSnapshot = self.tokenSnapshot(fromProviderSnapshot: snapshot, provider: provider) ?? self
|
|
.tokenSnapshots[provider]
|
|
let dailyUsage = tokenSnapshot?.daily.map { entry in
|
|
WidgetSnapshot.DailyUsagePoint(
|
|
dayKey: entry.date,
|
|
totalTokens: entry.totalTokens,
|
|
costUSD: entry.costUSD)
|
|
} ?? []
|
|
|
|
let tokenUsage = Self.widgetTokenUsageSummary(from: tokenSnapshot, provider: provider)
|
|
let usageRows = snapshot.map { self.widgetUsageRows(provider: provider, snapshot: $0, now: now) } ?? []
|
|
|
|
let creditsRemaining: Double?
|
|
let codeReviewRemaining: Double?
|
|
if provider == .codex, let snapshot {
|
|
let projection = self.codexConsumerProjection(
|
|
surface: .widget,
|
|
snapshotOverride: snapshot,
|
|
now: now)
|
|
let displayOnlyExtrasHidden = projection.dashboardVisibility == .displayOnly
|
|
creditsRemaining = displayOnlyExtrasHidden ? nil : projection.credits?.remaining
|
|
codeReviewRemaining = displayOnlyExtrasHidden ? nil : projection.remainingPercent(for: .codeReview)
|
|
} else {
|
|
creditsRemaining = nil
|
|
codeReviewRemaining = nil
|
|
}
|
|
let providerCost: ProviderCostSnapshot? = if provider == .devin,
|
|
self.settings.showOptionalCreditsAndExtraUsage
|
|
{
|
|
snapshot?.providerCost
|
|
} else {
|
|
nil
|
|
}
|
|
|
|
return WidgetSnapshot.ProviderEntry(
|
|
provider: provider,
|
|
updatedAt: snapshot?.updatedAt ?? tokenSnapshot?.updatedAt ?? now,
|
|
primary: snapshot?.primary,
|
|
secondary: snapshot?.secondary,
|
|
tertiary: snapshot?.tertiary,
|
|
usageRows: usageRows,
|
|
creditsRemaining: creditsRemaining,
|
|
codeReviewRemainingPercent: codeReviewRemaining,
|
|
tokenUsage: tokenUsage,
|
|
dailyUsage: dailyUsage,
|
|
providerCost: providerCost)
|
|
}
|
|
|
|
private nonisolated static func widgetTokenUsageSummary(
|
|
from snapshot: CostUsageTokenSnapshot?,
|
|
provider: UsageProvider) -> WidgetSnapshot.TokenUsageSummary?
|
|
{
|
|
guard let snapshot else { return nil }
|
|
let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +)
|
|
let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil)
|
|
let sessionLabel = provider == .bedrock || provider == .mistral ? "Latest billing day" : "Today"
|
|
let monthLabel = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "Today" : "\(snapshot.historyDays)d")
|
|
return WidgetSnapshot.TokenUsageSummary(
|
|
sessionCostUSD: snapshot.sessionCostUSD,
|
|
sessionTokens: snapshot.sessionTokens,
|
|
last30DaysCostUSD: snapshot.last30DaysCostUSD,
|
|
last30DaysTokens: monthTokensValue,
|
|
currencyCode: snapshot.currencyCode,
|
|
sessionLabel: sessionLabel,
|
|
last30DaysLabel: monthLabel,
|
|
updatedAt: snapshot.updatedAt)
|
|
}
|
|
|
|
private func widgetUsageRows(
|
|
provider: UsageProvider,
|
|
snapshot: UsageSnapshot,
|
|
now: Date) -> [WidgetSnapshot.WidgetUsageRowSnapshot]
|
|
{
|
|
let metadata = ProviderDefaults.metadata[provider]
|
|
if provider == .codex {
|
|
let projection = self.codexConsumerProjection(
|
|
surface: .widget,
|
|
snapshotOverride: snapshot,
|
|
now: now)
|
|
return projection.visibleRateLanes.compactMap { lane in
|
|
guard let window = projection.sourceRateWindow(for: lane) else { return nil }
|
|
let title = switch lane {
|
|
case .session:
|
|
metadata?.sessionLabel ?? "Session"
|
|
case .weekly:
|
|
metadata?.weeklyLabel ?? "Weekly"
|
|
}
|
|
return WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: lane.rawValue,
|
|
title: title,
|
|
percentLeft: window.remainingPercent,
|
|
window: window)
|
|
}
|
|
}
|
|
if provider == .antigravity,
|
|
let rows = Self.antigravityQuotaSummaryWidgetRows(snapshot: snapshot),
|
|
!rows.isEmpty
|
|
{
|
|
return rows
|
|
}
|
|
if provider == .antigravity,
|
|
snapshot.primary == nil,
|
|
snapshot.secondary == nil,
|
|
let rows = Self.antigravityLegacyExtraWidgetRows(snapshot: snapshot),
|
|
!rows.isEmpty
|
|
{
|
|
return rows
|
|
}
|
|
|
|
let primaryTitle: String = {
|
|
// Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
|
|
if provider == .cursor, snapshot.cursorRequests != nil {
|
|
return "Requests"
|
|
}
|
|
if provider == .grok,
|
|
let dyn = GrokProviderDescriptor.primaryLabel(window: snapshot.primary)
|
|
{
|
|
return dyn
|
|
}
|
|
if provider == .doubao,
|
|
let dyn = DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary)
|
|
{
|
|
return dyn
|
|
}
|
|
return metadata?.sessionLabel ?? "Session"
|
|
}()
|
|
|
|
var rows: [WidgetSnapshot.WidgetUsageRowSnapshot] = [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "primary",
|
|
title: primaryTitle,
|
|
percentLeft: snapshot.primary?.remainingPercent),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "secondary",
|
|
title: metadata?.weeklyLabel ?? "Weekly",
|
|
percentLeft: snapshot.secondary?.remainingPercent),
|
|
]
|
|
if metadata?.supportsOpus == true {
|
|
rows.append(WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "tertiary",
|
|
title: metadata?.opusLabel ?? "Opus",
|
|
percentLeft: snapshot.tertiary?.remainingPercent))
|
|
}
|
|
if provider == .kimi {
|
|
// Keep persisted widget order stable and include only Kimi's intentional subscription lanes.
|
|
let kimiWindowIDs = ["kimi-monthly", "kimi-code-7d"]
|
|
rows.append(contentsOf: kimiWindowIDs.compactMap { id in
|
|
guard let window = snapshot.extraRateWindows?.first(where: { $0.id == id }), window.usageKnown
|
|
else { return nil }
|
|
return WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: window.id,
|
|
title: window.title,
|
|
percentLeft: window.window.remainingPercent)
|
|
})
|
|
}
|
|
return rows.filter { $0.percentLeft != nil }
|
|
}
|
|
|
|
private nonisolated static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
|
|
private nonisolated static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-"
|
|
|
|
private nonisolated static func antigravityQuotaSummaryWidgetRows(
|
|
snapshot: UsageSnapshot) -> [WidgetSnapshot.WidgetUsageRowSnapshot]?
|
|
{
|
|
guard let windows = snapshot.extraRateWindows?.filter({
|
|
$0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix)
|
|
}), !windows.isEmpty else {
|
|
return nil
|
|
}
|
|
return windows.map { namedWindow in
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: namedWindow.id,
|
|
title: namedWindow.title,
|
|
percentLeft: namedWindow.usageKnown ? namedWindow.window.remainingPercent : nil)
|
|
}
|
|
}
|
|
|
|
private nonisolated static func antigravityLegacyExtraWidgetRows(
|
|
snapshot: UsageSnapshot) -> [WidgetSnapshot.WidgetUsageRowSnapshot]?
|
|
{
|
|
let windows = snapshot.extraRateWindows?
|
|
.filter { $0.id.hasPrefix(Self.antigravityCompactFallbackWindowIDPrefix) && $0.usageKnown }
|
|
guard let windows, !windows.isEmpty else { return nil }
|
|
return windows.map { namedWindow in
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: namedWindow.id,
|
|
title: namedWindow.title,
|
|
percentLeft: namedWindow.window.remainingPercent)
|
|
}
|
|
}
|
|
}
|