Skip to content
Closed
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
5 changes: 4 additions & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -784,10 +784,13 @@ extension UsageMenuCardView.Model {
static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {
let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
if Self.hasAntigravityQuotaSummaryWindows(snapshot) {
return Self.extraRateWindowMetrics(
let metrics = Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle)
guard !input.showsAllUsageLanes else { return metrics }
let idleIDs = AntigravityQuotaFamilyVisibility.idleWindowIDs(in: snapshot)
return idleIDs.isEmpty ? metrics : metrics.filter { !idleIDs.contains($0.id) }
}

var metrics: [Metric] = []
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ extension UsageMenuCardView.Model {
let claudeDailyRoutinesUsageVisible: Bool
let codexSparkUsageVisible: Bool
let copilotBudgetExtrasEnabled: Bool
/// Provider details is the diagnostic surface and lists every usage lane a provider reports.
/// The menu and widgets stay curated and may drop lanes that carry no information.
let showsAllUsageLanes: Bool
let sourceLabel: String?
let subtitleOverride: String?
let kiloAutoMode: Bool
Expand Down Expand Up @@ -72,6 +75,7 @@ extension UsageMenuCardView.Model {
claudeDailyRoutinesUsageVisible: Bool = true,
codexSparkUsageVisible: Bool = true,
copilotBudgetExtrasEnabled: Bool = false,
showsAllUsageLanes: Bool = false,
sourceLabel: String? = nil,
subtitleOverride: String? = nil,
kiloAutoMode: Bool = false,
Expand Down Expand Up @@ -112,6 +116,7 @@ extension UsageMenuCardView.Model {
self.claudeDailyRoutinesUsageVisible = claudeDailyRoutinesUsageVisible
self.codexSparkUsageVisible = codexSparkUsageVisible
self.copilotBudgetExtrasEnabled = copilotBudgetExtrasEnabled
self.showsAllUsageLanes = showsAllUsageLanes
self.sourceLabel = sourceLabel
self.subtitleOverride = subtitleOverride
self.kiloAutoMode = kiloAutoMode
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ struct ProvidersPane: View {
claudeDailyRoutinesUsageVisible: self.settings.claudeDailyRoutinesUsageVisible,
codexSparkUsageVisible: self.settings.codexSparkUsageVisible,
copilotBudgetExtrasEnabled: self.settings.copilotBudgetExtrasEnabled,
showsAllUsageLanes: true,
hidePersonalInfo: self.settings.hidePersonalInfo,
weeklyPace: weeklyPace,
quotaWarningThresholds: [
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBar/UsageStore+WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,9 @@ extension UsageStore {
}), !windows.isEmpty else {
return nil
}
return windows.map { namedWindow in
// Match the menu card and drop model families the account never touches.
let idleIDs = AntigravityQuotaFamilyVisibility.idleWindowIDs(in: snapshot)
return windows.filter { !idleIDs.contains($0.id) }.map { namedWindow in
WidgetSnapshot.WidgetUsageRowSnapshot(
id: namedWindow.id,
title: namedWindow.title,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

/// Antigravity reports every model family the plan covers, so an account that only runs Gemini still
/// receives a Claude/GPT pair pinned at 0%. Display surfaces hide a family once no lane in it reports
/// known usage above zero. Menu bar and icon selection rank by highest used, so an untouched family
/// never wins there and this stays a display-only filter.
public enum AntigravityQuotaFamilyVisibility {
/// Window IDs that display surfaces should drop. Empty when every family is untouched, so a card
/// or widget right after a reset still renders its lanes instead of an empty list.
public static func idleWindowIDs(in snapshot: UsageSnapshot) -> Set<String> {
let windows = (snapshot.extraRateWindows ?? [])
.filter { AntigravityStatusSnapshot.isQuotaSummaryWindowID($0.id) }
guard !windows.isEmpty else { return [] }
let families = Dictionary(grouping: windows, by: Self.familyKey)
let idleFamilies = families.filter { _, lanes in
lanes.allSatisfy { !$0.usageKnown || $0.window.usedPercent <= 0 }
}
guard idleFamilies.count < families.count else { return [] }
return Set(idleFamilies.values.flatMap { lanes in lanes.map(\.id) })
}

/// Classifies a lane the same way the widget row resolver does: the bucket ID carries the family and
/// survives a display-text change, so it wins over the rendered title. A group with neither signal
/// falls back to the title with its bucket suffix removed, which keeps an unfamiliar family's lanes
/// on one key so a reset lane never hides while its active sibling stays.
private static func familyKey(_ namedWindow: NamedRateWindow) -> String {
// Provider-specific by design: these tokens are Antigravity's own quota families, not CodexBar providers.
let id = namedWindow.id.lowercased()
if id.contains("gemini") {
return "gemini"
}
if id.contains("3p") || id.contains("third-party") {
return "claude-gpt"
}
let title = namedWindow.title.lowercased()
if title.contains("gemini") {
return "gemini"
}
if title.contains("claude") || title.contains("gpt") {
return "claude-gpt"
}
// Titles render as a group title plus a bucket title, so drop the bucket half to key per family.
for suffix in Self.bucketTitleSuffixes where title.hasSuffix(suffix) {
let stripped = String(title.dropLast(suffix.count)).trimmingCharacters(in: .whitespaces)
if !stripped.isEmpty {
return stripped
}
}
return title
Comment thread
urda marked this conversation as resolved.
}

private static let bucketTitleSuffixes = [" 5-hour", " weekly"]
}
225 changes: 225 additions & 0 deletions Tests/CodexBarTests/MenuCardAntigravityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -521,4 +521,229 @@ struct MenuCardAntigravityTests {
#expect(model.metrics[0].percent == 5)
#expect(model.metrics[0].percentLabel == "5% used")
}

@Test
func `antigravity quota summary hides an untouched model family`() throws {
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "3p-5h", title: "Claude/GPT 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(id: "3p-weekly", title: "Claude/GPT weekly", usedPercent: 0, weekly: true),
],
now: now)

#expect(model.metrics.map(\.id) == [
"antigravity-quota-summary-gemini-5h",
"antigravity-quota-summary-gemini-weekly",
])
}

@Test
func `antigravity quota summary hides a family whose only known lane is untouched`() throws {
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "3p-5h", title: "Claude/GPT 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(
id: "3p-weekly",
title: "Claude/GPT weekly",
usedPercent: 0,
weekly: true,
usageKnown: false),
],
now: now)

#expect(model.metrics.map(\.id) == [
"antigravity-quota-summary-gemini-5h",
"antigravity-quota-summary-gemini-weekly",
])
}

@Test
func `antigravity quota summary keeps every family when all are untouched`() throws {
// Right after a weekly reset every lane sits at zero; the card must not render empty.
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 0, weekly: true),
Self.quotaSummaryWindow(id: "3p-5h", title: "Claude/GPT 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(id: "3p-weekly", title: "Claude/GPT weekly", usedPercent: 0, weekly: true),
],
now: now)

#expect(model.metrics.count == 4)
}

@Test
func `antigravity keeps a renamed third party pair together`() throws {
// The bucket ID carries the family, so a group title that never says Claude or GPT must still
// pair its lanes; otherwise a freshly reset weekly lane would hide beside an active 5-hour lane.
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "3p-5h", title: "Third-party models 5-hour", usedPercent: 27),
Self.quotaSummaryWindow(
id: "3p-weekly",
title: "Third-party models weekly",
usedPercent: 0,
weekly: true),
],
now: now)

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

@Test
func `antigravity hides a renamed third party pair when it is untouched`() throws {
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "3p-5h", title: "Third-party models 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(
id: "3p-weekly",
title: "Third-party models weekly",
usedPercent: 0,
weekly: true),
],
now: now)

#expect(model.metrics.map(\.id) == [
"antigravity-quota-summary-gemini-5h",
"antigravity-quota-summary-gemini-weekly",
])
}

@Test
func `antigravity keeps an unfamiliar family pair together`() throws {
// Neither the ID nor the title names a known family, so the title fallback decides. Titles render
// as a group title plus a bucket title, and the fallback must drop the bucket half to pair lanes.
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "grok-5h", title: "Grok 5-hour", usedPercent: 30),
Self.quotaSummaryWindow(id: "grok-weekly", title: "Grok weekly", usedPercent: 0, weekly: true),
],
now: now)

#expect(model.metrics.map(\.id) == [
"antigravity-quota-summary-gemini-5h",
"antigravity-quota-summary-gemini-weekly",
"antigravity-quota-summary-grok-5h",
"antigravity-quota-summary-grok-weekly",
])
}

@Test
func `antigravity hides an unfamiliar family pair when it is untouched`() throws {
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "grok-5h", title: "Grok 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(id: "grok-weekly", title: "Grok weekly", usedPercent: 0, weekly: true),
],
now: now)

#expect(model.metrics.map(\.id) == [
"antigravity-quota-summary-gemini-5h",
"antigravity-quota-summary-gemini-weekly",
])
}

@Test
func `antigravity provider details keep every family`() throws {
// Provider details is the diagnostic surface, so it lists lanes the menu curates away.
let now = Date(timeIntervalSince1970: 1_735_000_000)
let model = try Self.quotaSummaryModel(
windows: [
Self.quotaSummaryWindow(id: "gemini-5h", title: "Gemini 5-hour", usedPercent: 1),
Self.quotaSummaryWindow(id: "gemini-weekly", title: "Gemini weekly", usedPercent: 4, weekly: true),
Self.quotaSummaryWindow(id: "3p-5h", title: "Claude/GPT 5-hour", usedPercent: 0),
Self.quotaSummaryWindow(id: "3p-weekly", title: "Claude/GPT weekly", usedPercent: 0, weekly: true),
],
now: now,
showsAllUsageLanes: true)

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

private static func quotaSummaryWindow(
id: String,
title: String,
usedPercent: Double,
weekly: Bool = false,
usageKnown: Bool = true) -> NamedRateWindow
{
NamedRateWindow(
id: "antigravity-quota-summary-\(id)",
title: title,
window: RateWindow(
usedPercent: usedPercent,
windowMinutes: weekly ? 10080 : 300,
resetsAt: nil,
resetDescription: nil),
usageKnown: usageKnown)
}

private static func quotaSummaryModel(
windows: [NamedRateWindow],
now: Date,
showsAllUsageLanes: Bool = false) throws -> UsageMenuCardView.Model
{
let snapshot = UsageSnapshot(
primary: nil,
secondary: nil,
tertiary: nil,
extraRateWindows: windows,
updatedAt: now,
identity: ProviderIdentitySnapshot(
providerID: .antigravity,
accountEmail: nil,
accountOrganization: nil,
loginMethod: "Google AI Pro"))
let metadata = try #require(ProviderDefaults.metadata[.antigravity])

return UsageMenuCardView.Model.make(.init(
provider: .antigravity,
metadata: metadata,
snapshot: snapshot,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: nil,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: true,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: false,
showsAllUsageLanes: showsAllUsageLanes,
hidePersonalInfo: false,
now: now))
}
}
10 changes: 5 additions & 5 deletions Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1870,39 +1870,39 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift",
line: 834,
line: 837,
anchor: "if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {",
expectedProviderIDs: ["claude", "codex", "copilot"],
expectedReferenceCount: 4,
expectedReferenceFingerprint: ["codex@0", "copilot@3", "codex@6", "claude@11"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift",
line: 859,
line: 862,
anchor: "let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil",
expectedProviderIDs: ["doubao", "sub2api"],
expectedReferenceCount: 3,
expectedReferenceFingerprint: ["sub2api@0", "sub2api@3", "doubao@15"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift",
line: 930,
line: 933,
anchor: "if input.provider == .antigravity,",
expectedProviderIDs: ["antigravity"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["antigravity@0"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift",
line: 964,
line: 967,
anchor: "if provider == .claude, window.windowMinutes != 10080 {",
expectedProviderIDs: ["antigravity", "claude", "codex"],
expectedReferenceCount: 4,
expectedReferenceFingerprint: ["claude@0", "antigravity@3", "claude@3", "codex@3"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift",
line: 996,
line: 999,
anchor: "guard input.provider == .antigravity else { return nil }",
expectedProviderIDs: ["antigravity"],
expectedReferenceCount: 1,
Expand Down
Loading