From f9686dfeb536d0e162a6d09a5e89114a755889e9 Mon Sep 17 00:00:00 2001 From: Peter Urda Date: Sat, 15 Aug 2026 19:46:07 -0700 Subject: [PATCH] fix: render one Antigravity lane per quota bucket on the dashboard The dashboard snapshot emitted primary, secondary, and every extra window. Antigravity copies its busiest Gemini and Claude/GPT buckets into primary and secondary so the icon and menu bar have standard slots to read, so the web card repeated two buckets as extra "Gemini Models" and "Claude and GPT" rows: six rows for four windows. Render the quota-summary lanes alone when the snapshot carries them, one row per bucket. Every family stays visible, since the dashboard is a detail surface. Snapshots without summary lanes are unchanged. --- CHANGELOG.md | 1 + .../DashboardSnapshotBuilder.swift | 18 +++ .../Antigravity/AntigravityStatusProbe.swift | 2 +- .../DashboardAntigravityWindowTests.swift | 131 ++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 Tests/CodexBarTests/DashboardAntigravityWindowTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7341bb53e2..47108ea343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed - Vertex AI: match Cloud Monitoring quota usage without a `limit_name` to its unambiguous same-metric, same-location limit, restoring quota percentages (#2958). Thanks @MachApple! +- Antigravity: render one lane per quota bucket on the serve dashboard and in `codexbar dashboard`, instead of repeating two buckets as extra "Gemini Models" and "Claude and GPT" rows. ## 0.50.0 — 2026-08-15 diff --git a/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift b/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift index bc168c8ce6..7727c4d304 100644 --- a/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift +++ b/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift @@ -286,6 +286,13 @@ enum DashboardSnapshotBuilder { usage: UsageSnapshot?) -> [DashboardWindowPayload] { guard let usage else { return [] } + // Provider-specific by design: Antigravity's primary and secondary are representatives + // copied out of its own quota-summary lanes so the icon and menu bar have standard slots + // to read. Emitting all three repeats two lanes under the generic session and weekly + // labels, so the dashboard renders the summary lanes alone, one per quota bucket. + if provider == .antigravity, let windows = self.antigravityQuotaSummaryWindows(usage) { + return windows + } let labels = self.rateWindowLabels(provider: provider, metadata: metadata, usage: usage) var windows: [DashboardWindowPayload] = [] // Provider-specific by design: Amp subscription payloads model balance and orb as non-time-window kinds. @@ -309,6 +316,17 @@ enum DashboardSnapshotBuilder { return windows } + /// Display lanes for an Antigravity quota-summary snapshot, or `nil` when the snapshot has no + /// summary lanes and must keep the standard primary and secondary rows. Every family stays + /// visible: the dashboard is a detail surface, so only the duplicated representatives go away. + private static func antigravityQuotaSummaryWindows(_ usage: UsageSnapshot) -> [DashboardWindowPayload]? { + let extras = usage.extraRateWindows ?? [] + guard extras.contains(where: { AntigravityStatusSnapshot.isQuotaSummaryWindowID($0.id) }) else { + return nil + } + return extras.map { self.makeWindow(kind: $0.id, label: $0.title, window: $0.window) } + } + private struct RateWindowLabels { let primary: String let secondary: String diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index 88b0ad1447..0ef0bd767c 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -282,7 +282,7 @@ public struct AntigravityStatusSnapshot: Sendable { } } - static func isQuotaSummaryWindowID(_ id: String) -> Bool { + public static func isQuotaSummaryWindowID(_ id: String) -> Bool { id.hasPrefix(self.quotaSummaryWindowIDPrefix) } diff --git a/Tests/CodexBarTests/DashboardAntigravityWindowTests.swift b/Tests/CodexBarTests/DashboardAntigravityWindowTests.swift new file mode 100644 index 0000000000..a6796ffec0 --- /dev/null +++ b/Tests/CodexBarTests/DashboardAntigravityWindowTests.swift @@ -0,0 +1,131 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBarCLI + +/// The dashboard snapshot renders one Antigravity lane per quota bucket. The primary and secondary +/// representatives are copies of two of those buckets, kept only so the icon and menu bar have +/// standard slots to read, so the dashboard must not repeat them as rows of their own. +struct DashboardAntigravityWindowTests { + @Test + func `dashboard renders one Antigravity lane per quota bucket`() throws { + let windows = try self.antigravityWindows(geminiSessionPercent: 3.9, geminiWeeklyPercent: 8.1) + + #expect(windows.map { $0["label"] as? String } == [ + "Gemini 5-hour", + "Gemini weekly", + "Claude/GPT 5-hour", + "Claude/GPT weekly", + ]) + #expect(windows.map { $0["usedPercent"] as? Double } == [3.9, 8.1, 0, 0]) + } + + @Test + func `dashboard omits the Antigravity representative labels`() throws { + let windows = try self.antigravityWindows(geminiSessionPercent: 3.9, geminiWeeklyPercent: 8.1) + let labels = windows.compactMap { $0["label"] as? String } + + #expect(!labels.contains("Gemini Models")) + #expect(!labels.contains("Claude and GPT")) + } + + @Test + func `dashboard keeps standard lanes for an Antigravity snapshot without summary windows`() throws { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let usage = UsageSnapshot( + primary: RateWindow(usedPercent: 12, windowMinutes: 300, resetsAt: now, resetDescription: nil), + secondary: RateWindow(usedPercent: 34, windowMinutes: 7 * 24 * 60, resetsAt: now, resetDescription: nil), + tertiary: nil, + updatedAt: now) + + let windows = try self.windows(for: usage, now: now) + + #expect(windows.map { $0["kind"] as? String } == ["session", "weekly"]) + #expect(windows.map { $0["label"] as? String } == ["Gemini Models", "Claude and GPT"]) + } + + // MARK: - Fixtures + + /// Mirrors the shape the quota-summary probe produces: four lanes, with `primary` and + /// `secondary` set to the most-used lane of each family rather than to distinct windows. + private func antigravityWindows( + geminiSessionPercent: Double, + geminiWeeklyPercent: Double) throws -> [[String: Any]] + { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let geminiSession = RateWindow( + usedPercent: geminiSessionPercent, + windowMinutes: 300, + resetsAt: now, + resetDescription: nil) + let geminiWeekly = RateWindow( + usedPercent: geminiWeeklyPercent, + windowMinutes: 7 * 24 * 60, + resetsAt: now, + resetDescription: nil) + let thirdPartySession = RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: now, resetDescription: nil) + let thirdPartyWeekly = RateWindow( + usedPercent: 0, + windowMinutes: 7 * 24 * 60, + resetsAt: now, + resetDescription: nil) + let extras = [ + NamedRateWindow( + id: "antigravity-quota-summary-gemini-5h", + title: "Gemini 5-hour", + window: geminiSession, + usageKnown: true), + NamedRateWindow( + id: "antigravity-quota-summary-gemini-weekly", + title: "Gemini weekly", + window: geminiWeekly, + usageKnown: true), + NamedRateWindow( + id: "antigravity-quota-summary-3p-5h", + title: "Claude/GPT 5-hour", + window: thirdPartySession, + usageKnown: true), + NamedRateWindow( + id: "antigravity-quota-summary-3p-weekly", + title: "Claude/GPT weekly", + window: thirdPartyWeekly, + usageKnown: true), + ] + let usage = UsageSnapshot( + primary: geminiWeeklyPercent >= geminiSessionPercent ? geminiWeekly : geminiSession, + secondary: thirdPartySession, + tertiary: nil, + extraRateWindows: extras, + updatedAt: now) + + return try self.windows(for: usage, now: now) + } + + private func windows(for usage: UsageSnapshot, now: Date) throws -> [[String: Any]] { + let payload = ProviderPayload( + provider: .antigravity, + account: nil, + version: nil, + source: "cli", + status: nil, + usage: usage, + credits: nil, + antigravityPlanInfo: nil, + openaiDashboard: nil, + error: nil) + let snapshot = DashboardSnapshotBuilder.makeSnapshot( + usagePayloads: [payload], + costPayloads: [], + config: CodexBarConfig(providers: [ProviderConfig(id: .antigravity, enabled: true)]), + identityMode: .redacted, + generatedAt: now, + refreshInterval: 60, + codexBarVersion: nil) + + let json = try #require(CodexBarCLI.encodeJSON(snapshot, pretty: false)) + let data = try #require(json.data(using: .utf8)) + let object = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) + let provider = try #require((object["providers"] as? [[String: Any]])?.first) + return try #require(provider["windows"] as? [[String: Any]]) + } +}