Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.36.2 — Unreleased

### Changed
- Antigravity: use current backend quota labels in menus and widgets while preferring a usable quota lane over an exhausted one. Thanks @Yuxin-Qiao!
- Menu bar: reuse the icon-observation signature during provider refreshes instead of computing it twice. Thanks @abe238!
- LiteLLM: show personal and team spend amounts directly on budget rows while suppressing duplicate budget sections. Thanks @hololee!

Expand Down
10 changes: 10 additions & 0 deletions Sources/CodexBar/MenuBarMetricWindowResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ enum MenuBarMetricWindowResolver {
.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 })
}

Expand All @@ -156,6 +161,11 @@ enum MenuBarMetricWindowResolver {
}
.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 })
}

Expand Down
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",
weeklyLabel: "Claude + GPT",
sessionLabel: "Gemini Models",
weeklyLabel: "Claude and GPT",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ private enum AntigravityUsagePool: Hashable {

var title: String {
switch self {
case .gemini: "Gemini"
case .claudeGPT: "Claude + GPT"
case .gemini: "Gemini Models"
case .claudeGPT: "Claude and GPT models"
}
}

Expand Down Expand Up @@ -207,8 +207,12 @@ public struct AntigravityStatusSnapshot: Sendable {
throw AntigravityStatusProbeError.parseFailed("No quota buckets available")
}

let primary = Self.quotaSummaryRepresentative(title: "Gemini", in: namedWindows)
let secondary = Self.quotaSummaryRepresentative(title: "Claude + GPT", in: namedWindows)
let primary = Self.quotaSummaryRepresentative(
matching: { $0.lowercased().contains("gemini") },
in: namedWindows)
let secondary = Self.quotaSummaryRepresentative(
matching: { $0.lowercased().contains("claude") || $0.lowercased().contains("gpt") },
in: namedWindows)

let identity = ProviderIdentitySnapshot(
providerID: .antigravity,
Expand All @@ -225,11 +229,11 @@ public struct AntigravityStatusSnapshot: Sendable {
}

private static func quotaSummaryRepresentative(
title: String,
matching predicate: (String) -> Bool,
in windows: [NamedRateWindow]) -> RateWindow?
{
windows
.filter { $0.usageKnown && $0.title.hasPrefix("\(title) ") }
.filter { $0.usageKnown && predicate($0.title) }
.max { lhs, rhs in
if lhs.window.usedPercent != rhs.window.usedPercent {
return lhs.window.usedPercent < rhs.window.usedPercent
Expand Down Expand Up @@ -295,29 +299,11 @@ public struct AntigravityStatusSnapshot: Sendable {

private static func displayTitle(forQuotaGroup group: AntigravityQuotaSummaryGroup) -> String {
let title = group.displayName.trimmingCharacters(in: .whitespacesAndNewlines)
let lower = title.lowercased()
if lower.contains("gemini") {
return "Gemini"
}
if lower.contains("claude") || lower.contains("gpt") {
return "Claude + GPT"
}
let stripped = title.replacingOccurrences(
of: #"(?i)\s+models?$"#,
with: "",
options: .regularExpression)
return stripped.isEmpty ? title : stripped
return title.isEmpty ? "Quota" : title
Comment thread
steipete marked this conversation as resolved.
}

private static func displayTitle(forQuotaBucket bucket: AntigravityQuotaSummaryBucket) -> String {
switch self.quotaBucketKind(for: bucket) {
case .session:
"Session"
case .weekly:
"Weekly"
case .other:
bucket.displayName
}
bucket.displayName
}

private static func windowMinutes(forQuotaBucket bucket: AntigravityQuotaSummaryBucket) -> Int? {
Expand Down
55 changes: 41 additions & 14 deletions Sources/CodexBarWidget/CodexBarWidgetViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,11 @@ struct WidgetUsageRow: Identifiable, Equatable {
let title: String
let percentLeft: Double?

private enum AntigravityQuotaFamily {
case gemini
case claudeGPT
}

static func smallWidgetRowLimit(for entry: WidgetSnapshot.ProviderEntry) -> Int? {
self.antigravityQuotaSummaryRowLimit(for: entry, limit: 2)
}
Expand Down Expand Up @@ -574,21 +579,10 @@ struct WidgetUsageRow: Identifiable, Equatable {
limit >= 2,
rows.contains(where: { $0.id.hasPrefix("antigravity-quota-summary-") })
{
var selected = ["Gemini ", "Claude + GPT "].compactMap { titlePrefix in
var selected = [AntigravityQuotaFamily.gemini, .claudeGPT].compactMap { family in
rows
.filter { $0.title.hasPrefix(titlePrefix) }
.min { lhs, rhs in
switch (lhs.percentLeft, rhs.percentLeft) {
case let (.some(left), .some(right)):
left < right
case (.some, .none):
true
case (.none, .some):
false
case (.none, .none):
false
}
}
.filter { self.antigravityQuotaFamily(for: $0) == family }
.min(by: self.isMoreConstrained)
}
let selectedIDs = Set(selected.map(\.id))
let fallbackRows = rows.enumerated()
Expand All @@ -611,6 +605,39 @@ struct WidgetUsageRow: Identifiable, Equatable {
}
return Array(rows.prefix(max(0, limit)))
}

private static func antigravityQuotaFamily(for row: WidgetUsageRow) -> AntigravityQuotaFamily? {
guard row.id.hasPrefix("antigravity-quota-summary-") else { return nil }
let id = row.id.lowercased()
if id.contains("gemini") {
return .gemini
}
if id.contains("3p") || id.contains("third-party") {
return .claudeGPT
}

let title = row.title.lowercased()
if title.contains("gemini") {
return .gemini
}
if title.contains("claude") || title.contains("gpt") {
return .claudeGPT
}
return nil
}

private static func isMoreConstrained(_ lhs: WidgetUsageRow, than rhs: WidgetUsageRow) -> Bool {
switch (lhs.percentLeft, rhs.percentLeft) {
case let (.some(left), .some(right)):
left < right
case (.some, .none):
true
case (.none, .some):
false
case (.none, .none):
false
}
}
}

private struct HistoryView: View {
Expand Down
8 changes: 4 additions & 4 deletions Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ struct AntigravityQuotaSummaryTests {
"antigravity-quota-summary-3p-weekly",
])
#expect(windows.map(\.title) == [
"Gemini Session",
"Gemini Weekly",
"Claude + GPT Session",
"Claude + GPT Weekly",
"Gemini Models Five Hour Limit",
"Gemini Models Weekly Limit",
"Claude and GPT models Five Hour Limit",
"Claude and GPT models Weekly Limit",
])
#expect(windows.map(\.window.windowMinutes) == [300, 10080, 300, 10080])
#expect(windows.map { $0.window.remainingPercent.rounded() } == [91, 82, 73, 64])
Expand Down
2 changes: 1 addition & 1 deletion Tests/CodexBarTests/AntigravityStatusProbeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,7 @@ extension AntigravityStatusProbeTests {
#expect(usage.primary == nil)
let modelWindow = try #require(usage.extraRateWindows?.first)
#expect(modelWindow.id == "antigravity-gemini")
#expect(modelWindow.title == "Gemini")
#expect(modelWindow.title == "Gemini Models")
#expect(modelWindow.window.resetsAt == resetTime)
#expect(modelWindow.usageKnown == false)
}
Expand Down
65 changes: 52 additions & 13 deletions Tests/CodexBarTests/CodexBarWidgetProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ struct CodexBarWidgetProviderTests {
usageRows: [
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-gemini-session",
title: "Gemini Session",
title: "Gemini Models Five Hour Limit",
percentLeft: 80),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-gemini-weekly",
title: "Gemini Weekly",
title: "Gemini Models Weekly Limit",
percentLeft: 20),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-third-party-session",
title: "Claude + GPT Session",
title: "Claude and GPT models Five Hour Limit",
percentLeft: 5),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-third-party-weekly",
title: "Claude + GPT Weekly",
title: "Claude and GPT models Weekly Limit",
percentLeft: 60),
],
creditsRemaining: nil,
Expand All @@ -60,17 +60,56 @@ struct CodexBarWidgetProviderTests {

let rows = WidgetUsageRow.rows(for: entry, limit: 2)

#expect(rows.map(\.title) == ["Gemini Weekly", "Claude + GPT Session"])
#expect(rows.map(\.title) == ["Gemini Models Weekly Limit", "Claude and GPT models Five Hour Limit"])
#expect(rows.compactMap(\.percentLeft) == [20, 5])
#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == 2)
#expect(WidgetUsageRow.mediumWidgetRowLimit(for: entry) == 3)
let mediumRows = WidgetUsageRow.rows(
for: entry,
limit: WidgetUsageRow.mediumWidgetRowLimit(for: entry))
#expect(mediumRows.map(\.title) == [
"Gemini Weekly",
"Claude + GPT Session",
"Claude + GPT Weekly",
"Gemini Models Weekly Limit",
"Claude and GPT models Five Hour Limit",
"Claude and GPT models Weekly Limit",
])
}

@Test
func `small antigravity widget keeps claude gpt family when fallback rows are more constrained`() {
let entry = WidgetSnapshot.ProviderEntry(
provider: .antigravity,
updatedAt: Date(),
primary: nil,
secondary: nil,
tertiary: nil,
usageRows: [
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-gemini-5h",
title: "Gemini Models Five Hour Limit",
percentLeft: 40),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-gemini-weekly",
title: "Gemini Models Weekly Limit",
percentLeft: 70),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-3p-5h",
title: "Claude and GPT models Five Hour Limit",
percentLeft: 60),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-other-5h",
title: "Other Five Hour Limit",
percentLeft: 1),
],
creditsRemaining: nil,
codeReviewRemainingPercent: nil,
tokenUsage: nil,
dailyUsage: [])

let rows = WidgetUsageRow.rows(for: entry, limit: 2)

#expect(rows.map(\.id) == [
"antigravity-quota-summary-gemini-5h",
"antigravity-quota-summary-3p-5h",
])
}

Expand Down Expand Up @@ -109,15 +148,15 @@ struct CodexBarWidgetProviderTests {
usageRows: [
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-gemini-session",
title: "Gemini Session",
title: "Gemini Models Five Hour Limit",
percentLeft: nil),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-gemini-weekly",
title: "Gemini Weekly",
title: "Gemini Models Weekly Limit",
percentLeft: 100),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "antigravity-quota-summary-third-party-session",
title: "Claude + GPT Session",
title: "Claude and GPT models Five Hour Limit",
percentLeft: 80),
],
creditsRemaining: nil,
Expand All @@ -127,7 +166,7 @@ struct CodexBarWidgetProviderTests {

let rows = WidgetUsageRow.rows(for: entry, limit: 2)

#expect(rows.map(\.title) == ["Gemini Weekly", "Claude + GPT Session"])
#expect(rows.map(\.title) == ["Gemini Models Weekly Limit", "Claude and GPT models Five Hour Limit"])
}

@Test
Expand Down Expand Up @@ -305,7 +344,7 @@ struct CodexBarWidgetProviderTests {
let rows = WidgetUsageRow.rows(for: entry)

#expect(rows.map(\.id) == ["primary", "secondary"])
#expect(rows.map(\.title) == ["Gemini", "Claude + GPT"])
#expect(rows.map(\.title) == ["Gemini Models", "Claude and GPT"])
#expect(rows.compactMap(\.percentLeft) == [90, 80])
}

Expand Down
35 changes: 35 additions & 0 deletions Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ struct MenuBarMetricWindowResolverTests {
#expect(window?.resetDescription == "Gemini Pro")
}

@Test
func `automatic metric skips exhausted antigravity five hour bucket when another remains usable`() {
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 67, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
extraRateWindows: [
NamedRateWindow(
id: "antigravity-quota-summary-gemini-5h",
title: "Gemini Models Five Hour Limit",
window: RateWindow(usedPercent: 71, windowMinutes: 300, resetsAt: nil, resetDescription: nil)),
NamedRateWindow(
id: "antigravity-quota-summary-gemini-weekly",
title: "Gemini Models Weekly Limit",
window: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)),
NamedRateWindow(
id: "antigravity-quota-summary-3p-5h",
title: "Claude and GPT models Five Hour Limit",
window: RateWindow(usedPercent: 100, windowMinutes: 300, resetsAt: nil, resetDescription: nil)),
NamedRateWindow(
id: "antigravity-quota-summary-3p-weekly",
title: "Claude and GPT models Weekly Limit",
window: RateWindow(usedPercent: 67, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)),
],
updatedAt: Date())

let window = MenuBarMetricWindowResolver.rateWindow(
preference: .automatic,
provider: .antigravity,
snapshot: snapshot,
supportsAverage: false)

#expect(window?.remainingPercent == 29)
#expect(window?.windowMinutes == 300)
}

@Test
func `automatic metric uses recognized antigravity gemini pool when claude gpt is reset only`() throws {
let resetOnlyReset = Date(timeIntervalSince1970: 1000)
Expand Down
Loading