161 lines
6.9 KiB
Swift
161 lines
6.9 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
|
|
@MainActor
|
|
extension UsageStore {
|
|
func handleQuotaWarningTransitions(provider: UsageProvider, snapshot: UsageSnapshot) {
|
|
guard self.settings.quotaWarningNotificationsEnabled else { return }
|
|
if provider == .commandcode, snapshot.commandCodeSubscriptionEnrichmentUnavailable { return }
|
|
|
|
let accountDisplayName = self.quotaWarningAccountDisplayName(provider: provider, snapshot: snapshot)
|
|
let source: SessionQuotaWindowSource? = if provider == .antigravity {
|
|
Self.hasAntigravityQuotaSummaryWindows(snapshot: snapshot)
|
|
? .antigravityQuotaSummary
|
|
: .antigravityLegacy
|
|
} else {
|
|
nil
|
|
}
|
|
let primaryWindow: RateWindow?
|
|
let secondaryWindow: RateWindow?
|
|
if provider == .antigravity {
|
|
primaryWindow = Self.antigravityWindow(snapshot: snapshot, windowMinutes: 5 * 60)
|
|
secondaryWindow = Self.antigravityWindow(snapshot: snapshot, windowMinutes: 7 * 24 * 60)
|
|
} else {
|
|
primaryWindow = provider == .mimo || provider == .qoder ? nil : snapshot.primary
|
|
secondaryWindow = provider == .mimo || provider == .qoder ? nil : snapshot.secondary
|
|
}
|
|
self.handleQuotaWarningTransition(
|
|
provider: provider,
|
|
window: .session,
|
|
rateWindow: primaryWindow,
|
|
source: source,
|
|
accountDisplayName: accountDisplayName)
|
|
self.handleQuotaWarningTransition(
|
|
provider: provider,
|
|
window: .weekly,
|
|
rateWindow: secondaryWindow,
|
|
source: source,
|
|
accountDisplayName: accountDisplayName)
|
|
self.handleClaudeExtraWindowQuotaWarnings(
|
|
provider: provider,
|
|
snapshot: snapshot,
|
|
accountDisplayName: accountDisplayName)
|
|
}
|
|
|
|
/// Emit weekly-lane quota warnings for Claude's extra rate windows — model-scoped weekly
|
|
/// carve-outs (`claude-weekly-scoped-*`, e.g. Fable) and Daily Routines — which surface in the
|
|
/// menu but were otherwise silent. Antigravity's summary windows are already covered by the
|
|
/// primary and weekly lanes above, so they are excluded here.
|
|
private func handleClaudeExtraWindowQuotaWarnings(
|
|
provider: UsageProvider,
|
|
snapshot: UsageSnapshot,
|
|
accountDisplayName: String?)
|
|
{
|
|
guard provider == .claude else { return }
|
|
guard self.settings.quotaWarningEnabled(provider: provider, window: .weekly) else {
|
|
let extraWindowKeys = self.quotaWarningState.keys.filter {
|
|
$0.provider == provider && $0.windowID != nil
|
|
}
|
|
for key in extraWindowKeys {
|
|
self.quotaWarningState.removeValue(forKey: key)
|
|
}
|
|
return
|
|
}
|
|
|
|
let windows = (snapshot.extraRateWindows ?? []).filter(Self.isClaudeNotifiableExtraWindow)
|
|
for named in windows {
|
|
self.handleQuotaWarningTransition(
|
|
provider: provider,
|
|
window: .weekly,
|
|
rateWindow: named.window,
|
|
source: nil,
|
|
accountDisplayName: accountDisplayName,
|
|
windowID: named.id,
|
|
windowDisplayLabel: named.title)
|
|
}
|
|
// A missing extras payload is not authoritative, but when another notifiable window remains,
|
|
// reconcile tracked IDs so a later incarnation of a disappeared window can warn again.
|
|
guard !windows.isEmpty else { return }
|
|
let activeIDs = Set(windows.map(\.id))
|
|
let staleKeys = self.quotaWarningState.keys.filter { key in
|
|
guard key.provider == provider, let windowID = key.windowID else { return false }
|
|
return !activeIDs.contains(windowID)
|
|
}
|
|
for key in staleKeys {
|
|
self.quotaWarningState.removeValue(forKey: key)
|
|
}
|
|
}
|
|
|
|
private static func isClaudeNotifiableExtraWindow(_ named: NamedRateWindow) -> Bool {
|
|
guard named.usageKnown else { return false }
|
|
return named.id.hasPrefix("claude-weekly-scoped-") || named.id == "claude-routines"
|
|
}
|
|
|
|
private func handleQuotaWarningTransition(
|
|
provider: UsageProvider,
|
|
window: QuotaWarningWindow,
|
|
rateWindow: RateWindow?,
|
|
source: SessionQuotaWindowSource?,
|
|
accountDisplayName: String?,
|
|
windowID: String? = nil,
|
|
windowDisplayLabel: String? = nil)
|
|
{
|
|
let key = QuotaWarningStateKey(provider: provider, window: window, windowID: windowID)
|
|
guard self.settings.quotaWarningEnabled(provider: provider, window: window) else {
|
|
self.quotaWarningState.removeValue(forKey: key)
|
|
return
|
|
}
|
|
guard let rateWindow else {
|
|
self.quotaWarningState.removeValue(forKey: key)
|
|
return
|
|
}
|
|
guard !rateWindow.isSyntheticPlaceholder else { return }
|
|
|
|
let thresholds = self.settings.resolvedQuotaWarningThresholds(provider: provider, window: window)
|
|
let currentRemaining = rateWindow.remainingPercent
|
|
let previousState = self.quotaWarningState[key]
|
|
if let previousState, previousState.source != source {
|
|
self.quotaWarningState[key] = QuotaWarningState(
|
|
lastRemaining: currentRemaining,
|
|
source: source)
|
|
return
|
|
}
|
|
var state = previousState ?? QuotaWarningState(source: source)
|
|
let cleared = QuotaWarningNotificationLogic.thresholdsToClear(
|
|
currentRemaining: currentRemaining,
|
|
alreadyFired: state.firedThresholds)
|
|
state.firedThresholds.subtract(cleared)
|
|
|
|
if let threshold = QuotaWarningNotificationLogic.crossedThreshold(
|
|
previousRemaining: state.lastRemaining,
|
|
currentRemaining: currentRemaining,
|
|
thresholds: thresholds,
|
|
alreadyFired: state.firedThresholds)
|
|
{
|
|
state.firedThresholds.formUnion(QuotaWarningNotificationLogic.firedThresholdsAfterWarning(
|
|
threshold: threshold,
|
|
thresholds: thresholds))
|
|
self.postQuotaWarning(
|
|
QuotaWarningEvent(
|
|
window: window,
|
|
threshold: threshold,
|
|
currentRemaining: currentRemaining,
|
|
accountDisplayName: accountDisplayName,
|
|
windowID: windowID,
|
|
windowDisplayLabel: windowDisplayLabel),
|
|
provider: provider)
|
|
}
|
|
|
|
state.lastRemaining = currentRemaining
|
|
self.quotaWarningState[key] = state
|
|
}
|
|
|
|
private func quotaWarningAccountDisplayName(provider: UsageProvider, snapshot: UsageSnapshot) -> String? {
|
|
guard !self.settings.hidePersonalInfo else { return nil }
|
|
let account = snapshot.accountEmail(for: provider)?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let account, !account.isEmpty else { return nil }
|
|
return account
|
|
}
|
|
}
|