From 0ebe92ebf325babba5c8726df069551c3a10a143 Mon Sep 17 00:00:00 2001 From: spaceman1412 Date: Wed, 18 Mar 2026 10:57:04 +0700 Subject: [PATCH 1/4] feat: refine Antigravity model usage calculation and menu card display for Claude and Gemini model variants. --- Sources/CodexBar/MenuCardView.swift | 164 +++++++++++--- .../AntigravityProviderImplementation.swift | 3 + .../Antigravity/AntigravityStatusProbe.swift | 146 ++++++++++--- .../AntigravityStatusProbeTests.swift | 204 ++++++++++++++++++ .../MenuCardAntigravityTests.swift | 58 +++++ .../MenuDescriptorAntigravityTests.swift | 60 ++++++ 6 files changed, 569 insertions(+), 66 deletions(-) create mode 100644 Tests/CodexBarTests/MenuCardAntigravityTests.swift create mode 100644 Tests/CodexBarTests/MenuDescriptorAntigravityTests.swift diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 34ac51846..98c94651c 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -29,6 +29,7 @@ struct UsageMenuCardView: View { let title: String let percent: Double let percentStyle: PercentStyle + let statusText: String? let resetText: String? let detailText: String? let detailLeftText: String? @@ -36,6 +37,32 @@ struct UsageMenuCardView: View { let pacePercent: Double? let paceOnTop: Bool + init( + id: String, + title: String, + percent: Double, + percentStyle: PercentStyle, + statusText: String? = nil, + resetText: String?, + detailText: String?, + detailLeftText: String?, + detailRightText: String?, + pacePercent: Double?, + paceOnTop: Bool) + { + self.id = id + self.title = title + self.percent = percent + self.percentStyle = percentStyle + self.statusText = statusText + self.resetText = resetText + self.detailText = detailText + self.detailLeftText = detailLeftText + self.detailRightText = detailRightText + self.pacePercent = pacePercent + self.paceOnTop = paceOnTop + } + var percentLabel: String { String(format: "%.0f%% %@", self.percent, self.percentStyle.labelSuffix) } @@ -330,49 +357,56 @@ private struct MetricRow: View { Text(self.title) .font(.body) .fontWeight(.medium) - UsageProgressBar( - percent: self.metric.percent, - tint: self.progressColor, - accessibilityLabel: self.metric.percentStyle.accessibilityLabel, - pacePercent: self.metric.pacePercent, - paceOnTop: self.metric.paceOnTop) - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .firstTextBaseline) { - Text(self.metric.percentLabel) - .font(.footnote) - .lineLimit(1) - Spacer() - if let rightLabel = self.metric.resetText { - Text(rightLabel) + if let statusText = self.metric.statusText { + Text(statusText) + .font(.footnote) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(1) + } else { + UsageProgressBar( + percent: self.metric.percent, + tint: self.progressColor, + accessibilityLabel: self.metric.percentStyle.accessibilityLabel, + pacePercent: self.metric.pacePercent, + paceOnTop: self.metric.paceOnTop) + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .firstTextBaseline) { + Text(self.metric.percentLabel) .font(.footnote) - .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) - } - } - if self.metric.detailLeftText != nil || self.metric.detailRightText != nil { - HStack(alignment: .firstTextBaseline) { - if let detailLeft = self.metric.detailLeftText { - Text(detailLeft) - .font(.footnote) - .foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted)) - .lineLimit(1) - } Spacer() - if let detailRight = self.metric.detailRightText { - Text(detailRight) + if let rightLabel = self.metric.resetText { + Text(rightLabel) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } + if self.metric.detailLeftText != nil || self.metric.detailRightText != nil { + HStack(alignment: .firstTextBaseline) { + if let detailLeft = self.metric.detailLeftText { + Text(detailLeft) + .font(.footnote) + .foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted)) + .lineLimit(1) + } + Spacer() + if let detailRight = self.metric.detailRightText { + Text(detailRight) + .font(.footnote) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(1) + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + if let detail = self.metric.detailText { + Text(detail) + .font(.footnote) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(1) } - } - .frame(maxWidth: .infinity, alignment: .leading) - if let detail = self.metric.detailText { - Text(detail) - .font(.footnote) - .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) - .lineLimit(1) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -898,6 +932,9 @@ extension UsageMenuCardView.Model { private static func metrics(input: Input) -> [Metric] { guard let snapshot = input.snapshot else { return [] } + if input.provider == .antigravity { + return Self.antigravityMetrics(input: input, snapshot: snapshot) + } var metrics: [Metric] = [] let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left let zaiUsage = input.provider == .zai ? snapshot.zaiUsage : nil @@ -1013,6 +1050,65 @@ extension UsageMenuCardView.Model { return metrics } + private static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] { + let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left + return [ + Self.antigravityMetric( + id: "primary", + title: input.metadata.sessionLabel, + window: snapshot.primary, + input: input, + percentStyle: percentStyle), + Self.antigravityMetric( + id: "secondary", + title: input.metadata.weeklyLabel, + window: snapshot.secondary, + input: input, + percentStyle: percentStyle), + Self.antigravityMetric( + id: "tertiary", + title: input.metadata.opusLabel ?? "Gemini Flash", + window: snapshot.tertiary, + input: input, + percentStyle: percentStyle), + ] + } + + private static func antigravityMetric( + id: String, + title: String, + window: RateWindow?, + input: Input, + percentStyle: PercentStyle) -> Metric + { + guard let window else { + return Metric( + id: id, + title: title, + percent: 0, + percentStyle: percentStyle, + statusText: nil, + resetText: nil, + detailText: nil, + detailLeftText: nil, + detailRightText: nil, + pacePercent: nil, + paceOnTop: true) + } + let percent = input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent + return Metric( + id: id, + title: title, + percent: Self.clamped(percent), + percentStyle: percentStyle, + resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now), + detailText: nil, + detailLeftText: nil, + detailRightText: nil, + pacePercent: nil, + paceOnTop: true) + } + private static func zaiLimitDetailText(limit: ZaiLimitEntry?) -> String? { guard let limit else { return nil } diff --git a/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift b/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift index 88492f070..fcac10ed1 100644 --- a/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift @@ -10,6 +10,9 @@ struct AntigravityProviderImplementation: ProviderImplementation { await AntigravityStatusProbe.detectVersion() } + @MainActor + func appendUsageMenuEntries(context _: ProviderMenuUsageContext, entries _: inout [ProviderMenuEntry]) {} + @MainActor func runLoginFlow(context: ProviderLoginContext) async -> Bool { await context.controller.runAntigravityLoginFlow() diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index 444ae97b6..fc9ab87a8 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -10,26 +10,67 @@ public struct AntigravityModelQuota: Sendable { public let resetTime: Date? public let resetDescription: String? + public init( + label: String, + modelId: String, + remainingFraction: Double?, + resetTime: Date?, + resetDescription: String?) + { + self.label = label + self.modelId = modelId + self.remainingFraction = remainingFraction + self.resetTime = resetTime + self.resetDescription = resetDescription + } + public var remainingPercent: Double { guard let remainingFraction else { return 0 } return max(0, min(100, remainingFraction * 100)) } } +private enum AntigravityModelFamily: Sendable { + case claude + case geminiPro + case geminiFlash + case unknown +} + +private struct AntigravityNormalizedModel: Sendable { + let quota: AntigravityModelQuota + let family: AntigravityModelFamily + let selectionPriority: Int? +} + public struct AntigravityStatusSnapshot: Sendable { public let modelQuotas: [AntigravityModelQuota] public let accountEmail: String? public let accountPlan: String? + public init( + modelQuotas: [AntigravityModelQuota], + accountEmail: String?, + accountPlan: String?) + { + self.modelQuotas = modelQuotas + self.accountEmail = accountEmail + self.accountPlan = accountPlan + } + public func toUsageSnapshot() throws -> UsageSnapshot { - let ordered = Self.selectModels(self.modelQuotas) - guard let primaryQuota = ordered.first else { + let normalized = Self.normalizedModels(self.modelQuotas) + let primaryQuota = Self.representative(for: .claude, in: normalized) + let secondaryQuota = Self.representative(for: .geminiPro, in: normalized) + let tertiaryQuota = Self.representative(for: .geminiFlash, in: normalized) + + guard primaryQuota != nil || secondaryQuota != nil || tertiaryQuota != nil else { throw AntigravityStatusProbeError.parseFailed("No quota models available") } - let primary = Self.rateWindow(for: primaryQuota) - let secondary = ordered.count > 1 ? Self.rateWindow(for: ordered[1]) : nil - let tertiary = ordered.count > 2 ? Self.rateWindow(for: ordered[2]) : nil + let primary = primaryQuota.map(Self.rateWindow(for:)) + let secondary = secondaryQuota.map(Self.rateWindow(for:)) + let tertiary = tertiaryQuota.map(Self.rateWindow(for:)) let identity = ProviderIdentitySnapshot( providerID: .antigravity, @@ -52,40 +93,81 @@ public struct AntigravityStatusSnapshot: Sendable { resetDescription: quota.resetDescription) } - private static func selectModels(_ models: [AntigravityModelQuota]) -> [AntigravityModelQuota] { - var ordered: [AntigravityModelQuota] = [] - if let claude = models.first(where: { Self.isClaudeWithoutThinking($0.label) }) { - ordered.append(claude) - } - if let pro = models.first(where: { Self.isGeminiProLow($0.label) }), - !ordered.contains(where: { $0.label == pro.label }) - { - ordered.append(pro) - } - if let flash = models.first(where: { Self.isGeminiFlash($0.label) }), - !ordered.contains(where: { $0.label == flash.label }) - { - ordered.append(flash) - } - if ordered.isEmpty { - ordered.append(contentsOf: models.sorted(by: { $0.remainingPercent < $1.remainingPercent })) + private static func normalizedModels(_ models: [AntigravityModelQuota]) -> [AntigravityNormalizedModel] { + models.map { self.normalizeModel($0) } + } + + private static func normalizeModel(_ quota: AntigravityModelQuota) -> AntigravityNormalizedModel { + let modelId = quota.modelId.lowercased() + let label = quota.label.lowercased() + let family = Self.family(forModelID: modelId, label: label) + + let isLite = modelId.contains("lite") || label.contains("lite") + let isAutocomplete = modelId.contains("autocomplete") || label.contains("autocomplete") || modelId + .hasPrefix("tab_") + let isLowPriorityGeminiPro = modelId.contains("pro-low") + || (label.contains("pro") && label.contains("low")) + + let selectionPriority: Int? = switch family { + case .claude: + 0 + case .geminiPro: + if isLowPriorityGeminiPro { + 0 + } else if !isLite, !isAutocomplete { + 1 + } else { + nil + } + case .geminiFlash: + (!isLite && !isAutocomplete) ? 0 : nil + case .unknown: + nil } - return ordered + + return AntigravityNormalizedModel( + quota: quota, + family: family, + selectionPriority: selectionPriority) } - private static func isClaudeWithoutThinking(_ label: String) -> Bool { - let lower = label.lowercased() - return lower.contains("claude") && !lower.contains("thinking") + private static func representative( + for family: AntigravityModelFamily, + in models: [AntigravityNormalizedModel]) -> AntigravityModelQuota? + { + let candidates = models.filter { + $0.family == family && $0.selectionPriority != nil && $0.quota.remainingFraction != nil + } + guard !candidates.isEmpty else { return nil } + return candidates.min { lhs, rhs in + let lhsPriority = lhs.selectionPriority ?? Int.max + let rhsPriority = rhs.selectionPriority ?? Int.max + if lhsPriority != rhsPriority { + return lhsPriority < rhsPriority + } + return lhs.quota.remainingPercent < rhs.quota.remainingPercent + }?.quota } - private static func isGeminiProLow(_ label: String) -> Bool { - let lower = label.lowercased() - return lower.contains("pro") && lower.contains("low") + private static func family(forModelID modelId: String, label: String) -> AntigravityModelFamily { + let modelIDFamily = Self.family(from: modelId) + if modelIDFamily != .unknown { + return modelIDFamily + } + return Self.family(from: label) } - private static func isGeminiFlash(_ label: String) -> Bool { - let lower = label.lowercased() - return lower.contains("gemini") && lower.contains("flash") + private static func family(from text: String) -> AntigravityModelFamily { + if text.contains("claude") { + return .claude + } + if text.contains("gemini"), text.contains("pro") { + return .geminiPro + } + if text.contains("gemini"), text.contains("flash") { + return .geminiFlash + } + return .unknown } } diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift index 3f38b6b16..fcd4f88b4 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift @@ -52,4 +52,208 @@ struct AntigravityStatusProbeTests { #expect(usage.secondary?.remainingPercent.rounded() == 80) #expect(usage.tertiary?.remainingPercent.rounded() == 20) } + + @Test + func `claude bar can use thinking variants`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Claude Thinking", + modelId: "claude-thinking", + remainingFraction: 0.7, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Claude Sonnet 4", + modelId: "claude-sonnet-4", + remainingFraction: 0.3, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 30) + } + + @Test + func `claude bar uses thinking model when it is the only claude option`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Claude Thinking", + modelId: "claude-thinking", + remainingFraction: 0.7, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro Low", + modelId: "gemini-3-pro-low", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 70) + #expect(usage.secondary?.remainingPercent.rounded() == 40) + } + + @Test + func `gemini pro bar unavailable when only excluded variants exist`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini Pro Lite", + modelId: "gemini-3-pro-lite", + remainingFraction: 0.6, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Claude Sonnet 4", + modelId: "claude-sonnet-4", + remainingFraction: 0.3, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.secondary == nil) + #expect(usage.primary?.remainingPercent.rounded() == 30) + } + + @Test + func `gemini pro chooses pro low model`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro", + modelId: "gemini-3-pro", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro Low", + modelId: "gemini-3-pro-low", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.secondary?.remainingPercent.rounded() == 40) + } + + @Test + func `gemini pro low wins over standard pro when both exist`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro", + modelId: "gemini-3-pro", + remainingFraction: 0.1, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro Low", + modelId: "gemini-3-pro-low", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.secondary?.remainingPercent.rounded() == 90) + } + + @Test + func `gemini flash does not fallback to lite variant`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 2 Flash Lite", + modelId: "gemini-2-flash-lite", + remainingFraction: 0.2, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Claude Sonnet 4", + modelId: "claude-sonnet-4", + remainingFraction: 0.3, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.tertiary == nil) + #expect(usage.primary?.remainingPercent.rounded() == 30) + } + + @Test + func `falls back to labels when model ids are placeholders`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Claude Sonnet 4.6", + modelId: "MODEL_PLACEHOLDER_M35", + remainingFraction: 0.3, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3.1 Pro (Low)", + modelId: "MODEL_PLACEHOLDER_M36", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "MODEL_PLACEHOLDER_M47", + remainingFraction: 1, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 30) + #expect(usage.secondary?.remainingPercent.rounded() == 40) + #expect(usage.tertiary?.remainingPercent.rounded() == 100) + } + + @Test + func `model without remaining fraction is unavailable`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3.1 Pro (Low)", + modelId: "MODEL_PLACEHOLDER_M36", + remainingFraction: nil, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "MODEL_PLACEHOLDER_M47", + remainingFraction: 1, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.secondary == nil) + #expect(usage.tertiary?.remainingPercent.rounded() == 100) + } } diff --git a/Tests/CodexBarTests/MenuCardAntigravityTests.swift b/Tests/CodexBarTests/MenuCardAntigravityTests.swift new file mode 100644 index 000000000..99a190573 --- /dev/null +++ b/Tests/CodexBarTests/MenuCardAntigravityTests.swift @@ -0,0 +1,58 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct MenuCardAntigravityTests { + @Test + func `antigravity metrics show zero percent for missing families`() throws { + let now = Date() + let identity = ProviderIdentitySnapshot( + providerID: .antigravity, + accountEmail: nil, + accountOrganization: nil, + loginMethod: "Pro") + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 5, + windowMinutes: nil, + resetsAt: now.addingTimeInterval(3600), + resetDescription: nil), + secondary: nil, + tertiary: nil, + updatedAt: now, + identity: identity) + let metadata = try #require(ProviderDefaults.metadata[.antigravity]) + + let model = 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: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + + #expect(model.metrics.count == 3) + #expect(model.metrics.map(\.title) == ["Claude", "Gemini Pro", "Gemini Flash"]) + #expect(model.metrics[1].percent == 0) + #expect(model.metrics[1].percentLabel == "0% left") + #expect(model.metrics[1].statusText == nil) + #expect(model.metrics[1].detailText == nil) + #expect(model.metrics[2].percent == 0) + #expect(model.metrics[2].percentLabel == "0% left") + #expect(model.metrics[2].statusText == nil) + #expect(model.metrics[2].detailText == nil) + } +} diff --git a/Tests/CodexBarTests/MenuDescriptorAntigravityTests.swift b/Tests/CodexBarTests/MenuDescriptorAntigravityTests.swift new file mode 100644 index 000000000..153425060 --- /dev/null +++ b/Tests/CodexBarTests/MenuDescriptorAntigravityTests.swift @@ -0,0 +1,60 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +@MainActor +struct MenuDescriptorAntigravityTests { + @Test + func `antigravity menu does not add unavailable notes for missing families`() throws { + let suite = "MenuDescriptorAntigravityTests-missing-gemini" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + + let settings = SettingsStore( + userDefaults: defaults, + configStore: testConfigStore(suiteName: suite), + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + settings.statusChecksEnabled = false + + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings) + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 10, + windowMinutes: nil, + resetsAt: Date().addingTimeInterval(3600), + resetDescription: nil), + secondary: nil, + tertiary: nil, + updatedAt: Date(), + identity: ProviderIdentitySnapshot( + providerID: .antigravity, + accountEmail: nil, + accountOrganization: nil, + loginMethod: "Pro")) + store._setSnapshotForTesting(snapshot, provider: .antigravity) + + let descriptor = MenuDescriptor.build( + provider: .antigravity, + store: store, + settings: settings, + account: AccountInfo(email: nil, plan: nil), + updateReady: false, + includeContextualActions: false) + + let lines = descriptor.sections + .flatMap( + \.entries) + .compactMap { entry -> String? in + guard case let .text(text, _) = entry else { return nil } + return text + } + + #expect(!lines.contains("Gemini Pro unavailable.")) + #expect(!lines.contains("Gemini Flash unavailable.")) + } +} From 5be8634ad7f84cbf96558b3034b901daf7e862b2 Mon Sep 17 00:00:00 2001 From: spaceman1412 Date: Wed, 18 Mar 2026 11:13:18 +0700 Subject: [PATCH 2/4] Preserve Antigravity reset time at zero percent --- .../Antigravity/AntigravityStatusProbe.swift | 9 ++- .../AntigravityStatusProbeTests.swift | 8 ++- .../MenuCardAntigravityTests.swift | 55 +++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index fc9ab87a8..05564e6cb 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -135,9 +135,7 @@ public struct AntigravityStatusSnapshot: Sendable { for family: AntigravityModelFamily, in models: [AntigravityNormalizedModel]) -> AntigravityModelQuota? { - let candidates = models.filter { - $0.family == family && $0.selectionPriority != nil && $0.quota.remainingFraction != nil - } + let candidates = models.filter { $0.family == family && $0.selectionPriority != nil } guard !candidates.isEmpty else { return nil } return candidates.min { lhs, rhs in let lhsPriority = lhs.selectionPriority ?? Int.max @@ -145,6 +143,11 @@ public struct AntigravityStatusSnapshot: Sendable { if lhsPriority != rhsPriority { return lhsPriority < rhsPriority } + let lhsHasRemainingFraction = lhs.quota.remainingFraction != nil + let rhsHasRemainingFraction = rhs.quota.remainingFraction != nil + if lhsHasRemainingFraction != rhsHasRemainingFraction { + return lhsHasRemainingFraction && !rhsHasRemainingFraction + } return lhs.quota.remainingPercent < rhs.quota.remainingPercent }?.quota } diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift index fcd4f88b4..f6177bcf8 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift @@ -233,14 +233,15 @@ struct AntigravityStatusProbeTests { } @Test - func `model without remaining fraction is unavailable`() throws { + func `model without remaining fraction keeps reset time`() throws { + let resetTime = Date(timeIntervalSince1970: 1_735_000_000) let snapshot = AntigravityStatusSnapshot( modelQuotas: [ AntigravityModelQuota( label: "Gemini 3.1 Pro (Low)", modelId: "MODEL_PLACEHOLDER_M36", remainingFraction: nil, - resetTime: nil, + resetTime: resetTime, resetDescription: nil), AntigravityModelQuota( label: "Gemini 3 Flash", @@ -253,7 +254,8 @@ struct AntigravityStatusProbeTests { accountPlan: nil) let usage = try snapshot.toUsageSnapshot() - #expect(usage.secondary == nil) + #expect(usage.secondary?.remainingPercent.rounded() == 0) + #expect(usage.secondary?.resetsAt == resetTime) #expect(usage.tertiary?.remainingPercent.rounded() == 100) } } diff --git a/Tests/CodexBarTests/MenuCardAntigravityTests.swift b/Tests/CodexBarTests/MenuCardAntigravityTests.swift index 99a190573..6583ab0a7 100644 --- a/Tests/CodexBarTests/MenuCardAntigravityTests.swift +++ b/Tests/CodexBarTests/MenuCardAntigravityTests.swift @@ -55,4 +55,59 @@ struct MenuCardAntigravityTests { #expect(model.metrics[2].statusText == nil) #expect(model.metrics[2].detailText == nil) } + + @Test + func `antigravity zero percent metric still shows reset text`() throws { + let now = Date(timeIntervalSince1970: 1_735_000_000) + let resetTime = now.addingTimeInterval(3600) + let antigravitySnapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Claude Thinking", + modelId: "MODEL_PLACEHOLDER_M35", + remainingFraction: 0.4, + resetTime: resetTime, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3.1 Pro (Low)", + modelId: "MODEL_PLACEHOLDER_M36", + remainingFraction: nil, + resetTime: resetTime, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "MODEL_PLACEHOLDER_M47", + remainingFraction: 1, + resetTime: resetTime, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: "Pro") + let snapshot = try antigravitySnapshot.toUsageSnapshot() + let metadata = try #require(ProviderDefaults.metadata[.antigravity]) + + let model = 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: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + + #expect(model.metrics[1].percent == 0) + #expect(model.metrics[1].percentLabel == "0% left") + #expect(model.metrics[1].resetText != nil) + } } From c9e8647aa07168ae36a8e4418a241ab407b37a2d Mon Sep 17 00:00:00 2001 From: spaceman1412 Date: Wed, 18 Mar 2026 11:22:50 +0700 Subject: [PATCH 3/4] Fix Antigravity used-mode placeholders --- Sources/CodexBar/MenuCardView.swift | 3 +- .../MenuCardAntigravityTests.swift | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 98c94651c..13bcc7927 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -1082,10 +1082,11 @@ extension UsageMenuCardView.Model { percentStyle: PercentStyle) -> Metric { guard let window else { + let placeholderPercent = input.usageBarsShowUsed ? 100.0 : 0.0 return Metric( id: id, title: title, - percent: 0, + percent: placeholderPercent, percentStyle: percentStyle, statusText: nil, resetText: nil, diff --git a/Tests/CodexBarTests/MenuCardAntigravityTests.swift b/Tests/CodexBarTests/MenuCardAntigravityTests.swift index 6583ab0a7..6916e6d35 100644 --- a/Tests/CodexBarTests/MenuCardAntigravityTests.swift +++ b/Tests/CodexBarTests/MenuCardAntigravityTests.swift @@ -110,4 +110,50 @@ struct MenuCardAntigravityTests { #expect(model.metrics[1].percentLabel == "0% left") #expect(model.metrics[1].resetText != nil) } + + @Test + func `antigravity missing families show full usage in used mode`() throws { + let now = Date() + let identity = ProviderIdentitySnapshot( + providerID: .antigravity, + accountEmail: nil, + accountOrganization: nil, + loginMethod: "Pro") + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 5, + windowMinutes: nil, + resetsAt: now.addingTimeInterval(3600), + resetDescription: nil), + secondary: nil, + tertiary: nil, + updatedAt: now, + identity: identity) + let metadata = try #require(ProviderDefaults.metadata[.antigravity]) + + let model = 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: true, + hidePersonalInfo: false, + now: now)) + + #expect(model.metrics[1].percent == 100) + #expect(model.metrics[1].percentLabel == "100% used") + #expect(model.metrics[2].percent == 100) + #expect(model.metrics[2].percentLabel == "100% used") + } } From 3b8b3c32a08dba3bca17a63b5a8a8bb39ae76ec1 Mon Sep 17 00:00:00 2001 From: spaceman1412 Date: Fri, 20 Mar 2026 16:00:42 +0700 Subject: [PATCH 4/4] Handle filtered Antigravity quotas --- .../Antigravity/AntigravityStatusProbe.swift | 8 ++--- .../AntigravityStatusProbeTests.swift | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index 05564e6cb..06726444f 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -59,15 +59,15 @@ public struct AntigravityStatusSnapshot: Sendable { } public func toUsageSnapshot() throws -> UsageSnapshot { + guard !self.modelQuotas.isEmpty else { + throw AntigravityStatusProbeError.parseFailed("No quota models available") + } + let normalized = Self.normalizedModels(self.modelQuotas) let primaryQuota = Self.representative(for: .claude, in: normalized) let secondaryQuota = Self.representative(for: .geminiPro, in: normalized) let tertiaryQuota = Self.representative(for: .geminiFlash, in: normalized) - guard primaryQuota != nil || secondaryQuota != nil || tertiaryQuota != nil else { - throw AntigravityStatusProbeError.parseFailed("No quota models available") - } - let primary = primaryQuota.map(Self.rateWindow(for:)) let secondary = secondaryQuota.map(Self.rateWindow(for:)) let tertiary = tertiaryQuota.map(Self.rateWindow(for:)) diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift index f6177bcf8..d165e6ca7 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift @@ -258,4 +258,38 @@ struct AntigravityStatusProbeTests { #expect(usage.secondary?.resetsAt == resetTime) #expect(usage.tertiary?.remainingPercent.rounded() == 100) } + + @Test + func `filtered variants still produce a snapshot`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro Lite", + modelId: "gemini-3-pro-lite", + remainingFraction: 0.6, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash Lite", + modelId: "gemini-3-flash-lite", + remainingFraction: 0.2, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Tab Autocomplete", + modelId: "tab_autocomplete_model", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: "test@example.com", + accountPlan: "Pro") + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary == nil) + #expect(usage.secondary == nil) + #expect(usage.tertiary == nil) + #expect(usage.accountEmail(for: .antigravity) == "test@example.com") + #expect(usage.loginMethod(for: .antigravity) == "Pro") + } }