Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 3 additions & 44 deletions Sources/CodexBar/IconRemainingResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import Foundation

enum IconRemainingResolver {
private static let visibleZeroPercent = 0.0001
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
// Antigravity quota summaries expose exact 5-hour session and weekly buckets for the compact icon.
private static let sessionWindowMinutes = 5 * 60
private static let weeklyWindowMinutes = 7 * 24 * 60

private static func codexProjection(snapshot: UsageSnapshot, now: Date) -> CodexConsumerProjection {
CodexConsumerProjection.make(
Expand All @@ -28,43 +24,6 @@ enum IconRemainingResolver {
return projection.visibleRateLanes.compactMap { projection.menuBarSelectableRateWindow(for: $0) }
}

private static func antigravityQuotaSummaryWindows(
snapshot: UsageSnapshot)
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let quotaSummaryWindows = snapshot.extraRateWindows?
.filter {
$0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix)
} ?? []
guard !quotaSummaryWindows.isEmpty else { return nil }

return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown))
}

private static func antigravityQuotaSummaryPair(
in windows: [NamedRateWindow])
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let session = self.mostConstrainedWindow(in: windows, windowMinutes: Self.sessionWindowMinutes)
let weekly = self.mostConstrainedWindow(in: windows, windowMinutes: Self.weeklyWindowMinutes)
guard session != nil || weekly != nil else { return nil }
return (primary: session, secondary: weekly)
}

/// Returns the highest-usage window for an exact Antigravity compact-icon cadence.
private static func mostConstrainedWindow(in windows: [NamedRateWindow], windowMinutes: Int) -> RateWindow? {
windows
.filter { $0.window.windowMinutes == windowMinutes }
.max { lhs, rhs in
if lhs.window.usedPercent != rhs.window.usedPercent {
return lhs.window.usedPercent < rhs.window.usedPercent
}
// max(by:) keeps the right-hand element when this returns true; use `>` so the smallest id wins ties.
return lhs.id > rhs.id
}?
.window
}

static func resolvedWindows(
snapshot: UsageSnapshot,
style: IconStyle,
Expand All @@ -79,9 +38,9 @@ enum IconRemainingResolver {
secondary: windows.dropFirst().first)
}
if style == .antigravity {
// Only current quota-summary buckets define the fixed session/weekly icon lanes.
return self.antigravityQuotaSummaryWindows(snapshot: snapshot)
?? (primary: nil, secondary: nil)
return (
primary: snapshot.primary?.windowMinutes != nil ? snapshot.primary : nil,
secondary: snapshot.secondary?.windowMinutes != nil ? snapshot.secondary : nil)
}
if style == .codex {
let windows = self.codexVisibleWindows(snapshot: snapshot, now: now)
Expand Down
168 changes: 15 additions & 153 deletions Sources/CodexBar/MenuBarMetricWindowResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,21 +117,6 @@ enum MenuBarMetricWindowResolver {
now: Date)
-> RateWindow?
{
if provider == .antigravity {
if antigravityPrioritizeExhaustedQuotas,
let window = antigravityQuotaSummaryRankingWindow(snapshot: snapshot, now: now)
{
return window
}
if let window = mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot) {
return window
}
return self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: snapshot.tertiary)
?? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot)
}
if provider == .perplexity {
return snapshot.automaticPerplexityWindow()
}
Expand All @@ -141,10 +126,11 @@ enum MenuBarMetricWindowResolver {
secondary: snapshot.tertiary,
tertiary: nil) ?? snapshot.secondary
}
if provider == .factory || provider == .kimi {
return snapshot.secondary ?? snapshot.primary
}
if provider == .litellm {
if provider == .factory || provider == .kimi || provider == .litellm {
let windows = [snapshot.primary, snapshot.secondary].compactMap(\.self)
if let exhausted = windows.first(where: { $0.usedPercent >= 100 }) {
return exhausted
}
return snapshot.secondary ?? snapshot.primary
}
if provider == .copilot,
Expand All @@ -154,150 +140,29 @@ enum MenuBarMetricWindowResolver {
return primary.usedPercent >= secondary.usedPercent ? primary : secondary
}
if provider == .cursor {
return Self.mostConstrainedCursorWindow(
return self.mostConstrainedCursorWindow(
total: snapshot.primary,
auto: snapshot.secondary,
api: snapshot.tertiary)
}
if provider == .minimax {
return Self.mostConstrainedWindow(
return self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: snapshot.tertiary)
}
if provider == .claude, let spendLimit = Self.claudeSpendLimitWindow(snapshot: snapshot) {
if provider == .claude, let spendLimit = claudeSpendLimitWindow(snapshot: snapshot) {
return spendLimit
}
return snapshot.primary ?? snapshot.secondary
}

private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-"

private static func mostConstrainedAntigravityQuotaSummaryWindow(snapshot: UsageSnapshot) -> RateWindow? {
let windows = snapshot.extraRateWindows?
.filter { $0.usageKnown && $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) }
.map(\.window) ?? []
guard !windows.isEmpty else { return nil }

let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}

/// Picks the binding supported quota-summary lane for the exhausted-first opt-in.
static func antigravityQuotaSummaryRankingWindow(
snapshot: UsageSnapshot,
now: Date)
-> RateWindow?
{
let candidates = Self.antigravityQuotaSummaryRows(snapshot: snapshot)
.filter {
$0.usageKnown &&
$0.window.usedPercent.isFinite &&
Self.isSupportedAntigravityQuotaCadence($0.window.windowMinutes)
}
return candidates.max { lhs, rhs in
if lhs.window.usedPercent != rhs.window.usedPercent {
return lhs.window.usedPercent < rhs.window.usedPercent
}

let lhsFutureReset = lhs.window.resetsAt.flatMap { $0 > now ? $0 : nil }
let rhsFutureReset = rhs.window.resetsAt.flatMap { $0 > now ? $0 : nil }
if (lhsFutureReset != nil) != (rhsFutureReset != nil) {
return lhsFutureReset == nil
}
if let lhsFutureReset, let rhsFutureReset, lhsFutureReset != rhsFutureReset {
return lhsFutureReset > rhsFutureReset
}
return lhs.id < rhs.id
}?.window
}

/// True only when every fully understood quota family has an exhausted binding lane.
/// Any incomplete or unfamiliar summary row fails open so automatic provider rotation
/// does not hide quota that CodexBar cannot classify safely.
static func antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: UsageSnapshot) -> Bool {
let rows = Self.antigravityQuotaSummaryRows(snapshot: snapshot)
guard !rows.isEmpty else { return false }

var familyBlocked: [String: Bool] = [:]
for row in rows {
guard row.usageKnown,
row.window.usedPercent.isFinite,
Self.isSupportedAntigravityQuotaCadence(row.window.windowMinutes),
let family = Self.antigravityQuotaFamily(for: row)
else {
return false
}
familyBlocked[family, default: false] =
familyBlocked[family, default: false] || row.window.usedPercent >= 100
}
return !familyBlocked.isEmpty && familyBlocked.values.allSatisfy(\.self)
}

private static let antigravitySupportedQuotaCadences: Set<Int> = [300, 10080]

private static func isSupportedAntigravityQuotaCadence(_ windowMinutes: Int?) -> Bool {
guard let windowMinutes else { return false }
return Self.antigravitySupportedQuotaCadences.contains(windowMinutes)
}

private static func antigravityQuotaSummaryRows(snapshot: UsageSnapshot) -> [NamedRateWindow] {
snapshot.extraRateWindows?.filter {
$0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix)
} ?? []
}

private static func antigravityQuotaFamily(for row: NamedRateWindow) -> String? {
let suffix = row.id.dropFirst(Self.antigravityQuotaSummaryWindowIDPrefix.count)
var normalizedSuffix = suffix
.lowercased()
.replacingOccurrences(of: "_", with: "-")
if normalizedSuffix.hasSuffix(" limit") {
normalizedSuffix.removeLast(" limit".count)
}
let cadenceSuffixes: [String]
switch row.window.windowMinutes {
case 300:
cadenceSuffixes = ["-session", "-5h", "-5-hour", "-five hour", "-five-hour"]
case 10080:
cadenceSuffixes = ["-weekly"]
default:
return nil
}

guard let cadenceSuffix = cadenceSuffixes.first(where: normalizedSuffix.hasSuffix) else {
return nil
// Default path:
// 1. Prioritize any exhausted window (usedPercent >= 100)
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
if let exhausted = windows.filter({ $0.usedPercent >= 100 }).max(by: { $0.usedPercent < $1.usedPercent }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect the Antigravity exhausted-quota opt-out

When Antigravity is in Automatic mode with the default antigravityPrioritizeExhaustedQuotas == false, this default branch is still reached and returns any 100% lane before the normal session/primary preference; the setting is no longer consulted. In accounts with one exhausted Antigravity lane and another still usable, the “Prioritize exhausted quotas” toggle now behaves as if it is always enabled for the menu bar and highest-usage ranking, so gate this exhausted-first return for Antigravity on the setting or restore the provider-specific non-exhausted selection.

Useful? React with 👍 / 👎.

return exhausted
Comment on lines +160 to +162

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep default providers on supported automatic lanes

For default providers with independent lanes, such as Gemini's Pro/Flash/Flash Lite quotas, this exhausted-first branch now lets an exhausted lower-priority tertiary lane win automatic selection even though Gemini's automatic behavior is expected to stay on the primary lane (see StatusItemAnimationTests.swift:578-612, which only covers the 95% case). In merged-icon/highest-usage mode that chosen 100% metric then falls through to UsageStore+HighestUsage.swift:126 and excludes the provider entirely, so a user with Pro still usable but Flash Lite exhausted loses Gemini from the unified icon instead of seeing the usable primary quota; limit this default exhausted-first behavior to providers whose partial exhaustion is handled, or add matching all-lanes-exhausted exclusion logic.

Useful? React with 👍 / 👎.

}
let family = normalizedSuffix
.dropLast(cadenceSuffix.count)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !family.isEmpty,
family.first != "-",
family.last != "-",
family.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" || $0 == "." })
else {
return nil
}
return family
}

private static func mostConstrainedAntigravityLegacyExtraWindow(snapshot: UsageSnapshot) -> RateWindow? {
let windows = snapshot.extraRateWindows?
.filter {
$0.usageKnown && $0.id.hasPrefix(Self.antigravityCompactFallbackWindowIDPrefix)
}
.map(\.window) ?? []
guard !windows.isEmpty else { return nil }

let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
// 2. Otherwise, fall back to the default order
return snapshot.primary ?? snapshot.secondary

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Antigravity compact fallback in Automatic

For Antigravity local probes with only an unrecognized selectable text model, AntigravityStatusProbe still creates an antigravity-compact-fallback-* extra window, but after deleting the provider-specific branch this default fallback only returns snapshot.primary ?? snapshot.secondary. In that scenario both are nil, so Automatic produces no metric and the provider disappears from menu-bar/Overview ranking despite having usable quota data; include the compact fallback extra window before returning nil.

Useful? React with 👍 / 👎.

}

private static func requestedWindow(
Expand All @@ -306,9 +171,6 @@ enum MenuBarMetricWindowResolver {
lanes: [Lane]) -> RateWindow?
{
self.window(in: snapshot, following: lanes)
?? (provider == .antigravity
? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot)
: nil)
}

private static func window(in snapshot: UsageSnapshot, following lanes: [Lane]) -> RateWindow? {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ extension UsageMenuCardView.Model {

private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.locale = codexBarLocalizedLocale()
formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
return formatter.string(from: date)
Expand Down
10 changes: 6 additions & 4 deletions Sources/CodexBar/UsagePaceText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ enum UsagePaceText {

static func sessionEquivalentDetail(forecast: SessionEquivalentForecast) -> SessionEquivalentDetail {
let displayedEstimate = Self.boundedFullWindowCount(forecast.estimatedWindowsToExhaustWeekly)
let numberText = String.localizedStringWithFormat(
L("≈%d full 5h windows of weekly left · %d windows until reset"),
let numberText = String(
format: L("≈%d full 5h windows of weekly left · %d windows until reset"),
locale: codexBarLocalizedLocale(),
Comment on lines +42 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep stringsdict plural formatting intact

These strings are backed by Localizable.stringsdict entries with %#@...@ plural variables (en.lproj/Localizable.stringsdict:5-45). String.localizedStringWithFormat is the plural-aware path that expands those stringsdict variants; replacing it with plain String(format:locale:) no longer applies the singular/plural selections, so forecasts like 1 window regress from the expected singular text in SessionEquivalentForecastTests.swift:292-293 and localized plural forms are lost. Use a plural-aware formatting path while still controlling the app locale.

Useful? React with 👍 / 👎.

displayedEstimate,
forecast.windowsUntilReset)
let verdictText: String
Expand All @@ -49,8 +50,9 @@ enum UsagePaceText {
} else {
let windowsEarly = Self.boundedWindowCount(
forecast.availableWindowsUntilReset - forecast.estimatedWindowsToExhaustWeekly)
verdictText = String.localizedStringWithFormat(
L("Weekly can run out ≈%d windows early"),
verdictText = String(
format: L("Weekly can run out ≈%d windows early"),
locale: codexBarLocalizedLocale(),
max(1, windowsEarly))
}
return SessionEquivalentDetail(
Expand Down
45 changes: 9 additions & 36 deletions Sources/CodexBar/UsageStore+HighestUsage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ extension UsageStore {
now: Date) -> RateWindow?
{
let effectivePreference = self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot)
if provider == .antigravity,
effectivePreference == .automatic,
!self.settings.antigravityPrioritizeExhaustedQuotas
{
return Self.mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot)
}
if provider == .codex {
return self.codexMenuBarMetricWindow(snapshot: snapshot, now: now)
}
Expand Down Expand Up @@ -95,14 +89,6 @@ extension UsageStore {
guard !percents.isEmpty else { return true }
return percents.allSatisfy { $0 >= 100 }
}
if provider == .antigravity, effectivePreference == .automatic {
if self.settings.antigravityPrioritizeExhaustedQuotas {
return MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)
}
let windows = Self.antigravityRenderedQuotaSummaryWindows(snapshot: snapshot)
guard !windows.isEmpty else { return true }
return windows.allSatisfy { $0.usedPercent >= 100 }
}
if provider == .copilot,
effectivePreference == .automatic,
let primary = snapshot.primary,
Expand All @@ -122,29 +108,16 @@ extension UsageStore {
guard !percents.isEmpty else { return true }
return percents.allSatisfy { $0 >= 100 }
}

return true
}

private nonisolated static func mostConstrainedAntigravityQuotaSummaryWindow(
snapshot: UsageSnapshot)
-> RateWindow?
{
let windows = self.antigravityRenderedQuotaSummaryWindows(snapshot: snapshot)
guard !windows.isEmpty else { return nil }

let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
if provider == .antigravity {
// Antigravity has Gemini 5h (primary) and weekly (secondary). Keep it eligible
// when only one lane is exhausted so the user still sees the usable quota,
// regardless of whether the metric preference is automatic or explicit.
let percents = [snapshot.primary?.usedPercent, snapshot.secondary?.usedPercent]
.compactMap(\.self)
guard !percents.isEmpty else { return true }
return percents.allSatisfy { $0 >= 100 }
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}

private nonisolated static func antigravityRenderedQuotaSummaryWindows(
snapshot: UsageSnapshot)
-> [RateWindow]
{
let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity)
return [windows.primary, windows.secondary].compactMap(\.self)
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public enum AntigravityProviderDescriptor {
metadata: ProviderMetadata(
id: .antigravity,
displayName: "Antigravity",
sessionLabel: "Gemini Models",
weeklyLabel: "Claude and GPT",
sessionLabel: "Gemini 5-hour",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep model-quota Antigravity labels cadence-neutral

When Antigravity uses the remote/local modelQuotas path rather than quotaSummary, the primary window is built from a representative Gemini model with windowMinutes: nil in rateWindow(for:), but menu/card/widget fallback rendering titles that primary row from this metadata. This new label therefore shows a model-level Gemini quota as “Gemini 5-hour” even though no 5-hour cadence was parsed; keep a neutral label for non-quota-summary snapshots or derive the label from the window cadence.

Useful? React with 👍 / 👎.

weeklyLabel: "Gemini weekly",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
Expand Down
Loading