From 2087fb9984b68d7eb4498f051d4654dc682be22b Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:40:03 +0800 Subject: [PATCH 01/10] Fix automatic quota display priority for exhausted windows --- .../MenuBarMetricWindowResolver.swift | 27 ++---- Sources/CodexBar/UsagePaceText.swift | 10 ++- .../MenuBarMetricWindowResolverTests.swift | 85 +++++++++++++++++++ 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/Sources/CodexBar/MenuBarMetricWindowResolver.swift b/Sources/CodexBar/MenuBarMetricWindowResolver.swift index cab021cf87..9eb97d8dfa 100644 --- a/Sources/CodexBar/MenuBarMetricWindowResolver.swift +++ b/Sources/CodexBar/MenuBarMetricWindowResolver.swift @@ -141,34 +141,19 @@ enum MenuBarMetricWindowResolver { secondary: snapshot.tertiary, tertiary: nil) ?? snapshot.secondary } - if provider == .factory || provider == .kimi { - return snapshot.secondary ?? snapshot.primary - } - if provider == .litellm { - return snapshot.secondary ?? snapshot.primary - } - if provider == .copilot, - let primary = snapshot.primary, - let secondary = snapshot.secondary - { - 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( - 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 + return self.mostConstrainedWindow( + primary: snapshot.primary, + secondary: snapshot.secondary, + tertiary: snapshot.tertiary) } private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-" diff --git a/Sources/CodexBar/UsagePaceText.swift b/Sources/CodexBar/UsagePaceText.swift index 32823b6d22..11001bcc98 100644 --- a/Sources/CodexBar/UsagePaceText.swift +++ b/Sources/CodexBar/UsagePaceText.swift @@ -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(), displayedEstimate, forecast.windowsUntilReset) let verdictText: String @@ -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( diff --git a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift index 9dfb1ad72f..0849bfc75c 100644 --- a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift +++ b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift @@ -210,6 +210,40 @@ struct MenuBarMetricWindowResolverTests { #expect(window?.resetDescription == "Team") } + @Test + func `automatic metric prioritizes exhausted litellm personal budget over active team budget`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Personal"), + secondary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: "Team"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .litellm, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "Personal") + #expect(window?.usedPercent == 100) + } + + @Test + func `automatic metric prioritizes exhausted litellm team budget over active personal budget`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: "Personal"), + secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Team"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .litellm, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "Team") + #expect(window?.usedPercent == 100) + } + @Test func `automatic metric uses constrained antigravity family lane`() { let snapshot = UsageSnapshot( @@ -809,4 +843,55 @@ struct MenuBarMetricWindowResolverTests { #expect(window?.resetsAt == reset) } + + @Test + func `automatic metric prioritizes exhausted kimi weekly quota over active rate limit`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), + secondary: RateWindow(usedPercent: 4, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .kimi, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "Weekly") + #expect(window?.usedPercent == 100) + } + + @Test + func `automatic metric prioritizes exhausted kimi rate limit over active weekly quota`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), + secondary: RateWindow(usedPercent: 100, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .kimi, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "5-hour") + #expect(window?.usedPercent == 100) + } + + @Test + func `automatic metric shows most constrained kimi window when neither is exhausted`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), + secondary: RateWindow(usedPercent: 4, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .kimi, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "Weekly") + #expect(window?.usedPercent == 40) + } } From 183ee8a61a00b9b4571039fb3b36b31c8dc4f47f Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:48:28 +0800 Subject: [PATCH 02/10] Fix automatic quota display priority and date locale test issues --- .../MenuBarMetricWindowResolver.swift | 32 +++++++-- .../CodexBar/MenuCardView+ModelHelpers.swift | 2 +- Tests/CodexBarTests/MenuBarLayoutTests.swift | 67 +++++++++++++++++++ .../MenuBarMetricWindowResolverTests.swift | 6 +- .../MenuCardTestDateFormatting.swift | 3 +- 5 files changed, 101 insertions(+), 9 deletions(-) diff --git a/Sources/CodexBar/MenuBarMetricWindowResolver.swift b/Sources/CodexBar/MenuBarMetricWindowResolver.swift index 9eb97d8dfa..47a7b39dc0 100644 --- a/Sources/CodexBar/MenuBarMetricWindowResolver.swift +++ b/Sources/CodexBar/MenuBarMetricWindowResolver.swift @@ -141,19 +141,43 @@ enum MenuBarMetricWindowResolver { secondary: snapshot.tertiary, tertiary: nil) ?? snapshot.secondary } + 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, + let primary = snapshot.primary, + let secondary = snapshot.secondary + { + return primary.usedPercent >= secondary.usedPercent ? primary : secondary + } if provider == .cursor { return self.mostConstrainedCursorWindow( total: snapshot.primary, auto: snapshot.secondary, api: snapshot.tertiary) } + if provider == .minimax { + return self.mostConstrainedWindow( + primary: snapshot.primary, + secondary: snapshot.secondary, + tertiary: snapshot.tertiary) + } if provider == .claude, let spendLimit = claudeSpendLimitWindow(snapshot: snapshot) { return spendLimit } - return self.mostConstrainedWindow( - primary: snapshot.primary, - secondary: snapshot.secondary, - tertiary: snapshot.tertiary) + + // 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 }) { + return exhausted + } + // 2. Otherwise, fall back to the default order + return snapshot.primary ?? snapshot.secondary } private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-" diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index 8e32ed8ebb..392c35df1a 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -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) diff --git a/Tests/CodexBarTests/MenuBarLayoutTests.swift b/Tests/CodexBarTests/MenuBarLayoutTests.swift index 8a93e6c7dc..2aeed8f5a0 100644 --- a/Tests/CodexBarTests/MenuBarLayoutTests.swift +++ b/Tests/CodexBarTests/MenuBarLayoutTests.swift @@ -188,6 +188,73 @@ struct MenuBarLayoutTests { provider: .kimi) == MenuBarLayout(lines: [[.icon, .percent(window: .session)]])) } + @Test + @MainActor + func `reproduce pace percentage missing`() { + // Mock a pace where the user is 15% ahead of target usage + let pace = UsagePace( + stage: .ahead, + deltaPercent: 15, + expectedUsedPercent: 50, + actualUsedPercent: 65, + etaSeconds: nil, + willLastToReset: true) + + // 1. Calculate legacy pace rendering output + let legacyPaceString = MenuBarDisplayText.displayText( + mode: .pace, + percentWindow: nil, + pace: pace, + showUsed: true) ?? "nil" + + // 2. Calculate new migrated rendering output (which maps legacy `.pace` to `.runsOut` layout token) + let resolution = MenuBarLayoutResolution.legacy( + iconStyle: .iconAndPercent, + displayMode: .pace, + metricPreference: .primary, + resetTimeDisplayStyle: .countdown) + let migratedTokens = Array(resolution.layout.lines.joined()) + #expect(migratedTokens.contains(.runsOut)) + + // Get the text that .runsOut resolves to for the same pace + // weeklyDetail's rightLabel for willLastToReset is "Lasts until reset" + let runsOutText = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: Date()).rightLabel ?? "nil" + + let renderer = MenuBarLayoutRenderer() + let data = MenuBarLayoutRenderData( + iconKey: "codex", + providerName: "Codex", + accountLabel: "user@example.com", + session: nil, + weekly: nil, + automatic: nil, + runsOut: runsOutText, + costToday: nil, + cost30d: nil) + let output = renderer.render( + layout: MenuBarLayout(lines: [[.runsOut]]), + data: data, + icon: nil, + options: MenuBarLayoutRenderOptions( + size: .regular, + highContrast: false, + showUsed: true, + appearanceName: "aqua", + isDebugApp: false, + now: Date())) + let newPaceString = output.attributedTitle.string + + // 3. Print the comparison to verify the reproduction clearly in test output logs + print("\n==================================================") + print("REPRODUCTION - PACE METRIC DISCREPANCY COMPARISON") + print("Legacy rendering output (using .pace displayMode): \(legacyPaceString)") + print("New layout rendering output (migrated to .runsOut token): \(newPaceString)") + print("==================================================\n") + + #expect(legacyPaceString == "+15%") + #expect(newPaceString == "Lasts until reset") + } + @Test @MainActor func `global editing seeds the representative provider legacy layout`() throws { diff --git a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift index 0849bfc75c..58a9797c34 100644 --- a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift +++ b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift @@ -879,7 +879,7 @@ struct MenuBarMetricWindowResolverTests { } @Test - func `automatic metric shows most constrained kimi window when neither is exhausted`() { + func `automatic metric defaults to rate limit for kimi when neither is exhausted`() { let snapshot = UsageSnapshot( primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), secondary: RateWindow(usedPercent: 4, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), @@ -891,7 +891,7 @@ struct MenuBarMetricWindowResolverTests { snapshot: snapshot, supportsAverage: false) - #expect(window?.resetDescription == "Weekly") - #expect(window?.usedPercent == 40) + #expect(window?.resetDescription == "5-hour") + #expect(window?.usedPercent == 4) } } diff --git a/Tests/CodexBarTests/MenuCardTestDateFormatting.swift b/Tests/CodexBarTests/MenuCardTestDateFormatting.swift index 4f94f865d1..b13b161dca 100644 --- a/Tests/CodexBarTests/MenuCardTestDateFormatting.swift +++ b/Tests/CodexBarTests/MenuCardTestDateFormatting.swift @@ -1,8 +1,9 @@ import Foundation +@testable import CodexBar func minimaxRenewDate(_ timestamp: TimeInterval) -> String { let formatter = DateFormatter() - formatter.locale = .current + formatter.locale = codexBarLocalizedLocale() formatter.timeZone = TimeZone(identifier: "Asia/Shanghai") formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy") return formatter.string(from: Date(timeIntervalSince1970: timestamp)) From d5b40621022051360641731581adf3247a17815a Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:43:58 +0800 Subject: [PATCH 03/10] Prefer Antigravity 5-hour session window over weekly in automatic mode When no quota is exhausted, the automatic metric now prefers 5-hour session windows (windowMinutes=300) over weekly windows for the Antigravity provider. The session limit is the immediate constraint users care about; showing a weekly reset countdown (e.g. 'in 6d 18h') is less actionable than the session window. --- .../MenuBarMetricWindowResolver.swift | 16 ++++++- .../MenuBarMetricWindowResolverTests.swift | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/MenuBarMetricWindowResolver.swift b/Sources/CodexBar/MenuBarMetricWindowResolver.swift index 47a7b39dc0..7dc22f5dff 100644 --- a/Sources/CodexBar/MenuBarMetricWindowResolver.swift +++ b/Sources/CodexBar/MenuBarMetricWindowResolver.swift @@ -190,9 +190,20 @@ enum MenuBarMetricWindowResolver { 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 !usableWindows.isEmpty { + // Prefer 5-hour session windows over weekly windows: the session limit + // is the immediate constraint the user cares about. + let sessionWindows = usableWindows.filter { + $0.windowMinutes == Self.antigravitySessionCadence + } + if let mostConstrainedSession = sessionWindows.max(by: { + $0.usedPercent < $1.usedPercent + }) { + return mostConstrainedSession + } + return usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) } + // All exhausted: show the most exhausted window. return windows.max(by: { $0.usedPercent < $1.usedPercent }) } @@ -247,6 +258,7 @@ enum MenuBarMetricWindowResolver { return !familyBlocked.isEmpty && familyBlocked.values.allSatisfy(\.self) } + private static let antigravitySessionCadence = 300 private static let antigravitySupportedQuotaCadences: Set = [300, 10080] private static func isSupportedAntigravityQuotaCadence(_ windowMinutes: Int?) -> Bool { diff --git a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift index 58a9797c34..b7cad30823 100644 --- a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift +++ b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift @@ -3,6 +3,7 @@ import Foundation import Testing @testable import CodexBar +// swiftlint:disable:next type_body_length struct MenuBarMetricWindowResolverTests { @Test func `gemini metrics fall back to Flash when Pro is unavailable`() { @@ -305,6 +306,51 @@ struct MenuBarMetricWindowResolverTests { #expect(optInWindow?.windowMinutes == 300) } + @Test + func `automatic metric prefers antigravity session window over weekly when none are exhausted`() { + let snapshot = UsageSnapshot( + primary: nil, + secondary: nil, + extraRateWindows: [ + NamedRateWindow( + id: "antigravity-quota-summary-gemini-5h", + title: "Gemini Models Five Hour Limit", + window: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5h")), + NamedRateWindow( + id: "antigravity-quota-summary-gemini-weekly", + title: "Gemini Models Weekly Limit", + window: RateWindow( + usedPercent: 15, + windowMinutes: 10080, + resetsAt: nil, + resetDescription: "weekly")), + NamedRateWindow( + id: "antigravity-quota-summary-3p-5h", + title: "Claude and GPT models Five Hour Limit", + window: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5h")), + NamedRateWindow( + id: "antigravity-quota-summary-3p-weekly", + title: "Claude and GPT models Weekly Limit", + window: RateWindow( + usedPercent: 34, + windowMinutes: 10080, + resetsAt: nil, + resetDescription: "weekly")), + ], + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .antigravity, + snapshot: snapshot, + supportsAverage: false) + + // Session (5-hour) windows should be preferred even though weekly windows + // have higher usedPercent, because the session limit is the immediate constraint. + #expect(window?.windowMinutes == 300) + #expect(window?.resetDescription == "5h") + } + @Test func `automatic metric uses recognized antigravity gemini pool when claude gpt is reset only`() throws { let resetOnlyReset = Date(timeIntervalSince1970: 1000) From deba799c057603edd4ae80bedeb8181a74fd3add Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:19:30 +0800 Subject: [PATCH 04/10] Fix Antigravity exhausted family masking and align session preference --- Sources/CodexBar/IconRemainingResolver.swift | 30 +++++++++++++-- Sources/CodexBar/MenuBarLayout.swift | 5 ++- Sources/CodexBar/MenuBarLayoutEditor.swift | 11 +++++- Sources/CodexBar/MenuBarLayoutRenderer.swift | 7 ++++ .../MenuBarMetricWindowResolver.swift | 2 +- Sources/CodexBar/MenuContent.swift | 4 +- .../Resources/ar.lproj/Localizable.strings | 3 ++ .../Resources/ca.lproj/Localizable.strings | 3 ++ .../Resources/de.lproj/Localizable.strings | 3 ++ .../Resources/en.lproj/Localizable.strings | 3 ++ .../Resources/es.lproj/Localizable.strings | 3 ++ .../Resources/fa.lproj/Localizable.strings | 3 ++ .../Resources/fr.lproj/Localizable.strings | 3 ++ .../Resources/gl.lproj/Localizable.strings | 3 ++ .../Resources/id.lproj/Localizable.strings | 3 ++ .../Resources/it.lproj/Localizable.strings | 3 ++ .../Resources/ja.lproj/Localizable.strings | 3 ++ .../Resources/ko.lproj/Localizable.strings | 3 ++ .../Resources/nl.lproj/Localizable.strings | 3 ++ .../Resources/pl.lproj/Localizable.strings | 3 ++ .../Resources/pt-BR.lproj/Localizable.strings | 3 ++ .../Resources/ru.lproj/Localizable.strings | 3 ++ .../Resources/sv.lproj/Localizable.strings | 3 ++ .../Resources/th.lproj/Localizable.strings | 3 ++ .../Resources/tr.lproj/Localizable.strings | 3 ++ .../Resources/uk.lproj/Localizable.strings | 3 ++ .../Resources/vi.lproj/Localizable.strings | 3 ++ .../zh-Hans.lproj/Localizable.strings | 3 ++ .../zh-Hant.lproj/Localizable.strings | 3 ++ .../StatusItemController+Animation.swift | 1 + .../CodexBar/StatusItemController+Menu.swift | 1 + .../StatusItemController+MenuBarLayout.swift | 6 ++- .../CodexBar/UsageStore+HighestUsage.swift | 38 +++++++++++++++---- .../LocalizationLanguageCatalogTests.swift | 3 ++ .../MenuBarLayoutRendererTests.swift | 23 +++++++++++ Tests/CodexBarTests/MenuBarLayoutTests.swift | 17 ++++----- .../UsageStoreHighestUsageTests.swift | 37 ++++++++++++++++++ 37 files changed, 226 insertions(+), 28 deletions(-) diff --git a/Sources/CodexBar/IconRemainingResolver.swift b/Sources/CodexBar/IconRemainingResolver.swift index 49fd27b773..f1b1149c4f 100644 --- a/Sources/CodexBar/IconRemainingResolver.swift +++ b/Sources/CodexBar/IconRemainingResolver.swift @@ -29,7 +29,8 @@ enum IconRemainingResolver { } private static func antigravityQuotaSummaryWindows( - snapshot: UsageSnapshot) + snapshot: UsageSnapshot, + prioritizeExhausted: Bool) -> (primary: RateWindow?, secondary: RateWindow?)? { let quotaSummaryWindows = snapshot.extraRateWindows? @@ -38,7 +39,23 @@ enum IconRemainingResolver { } ?? [] guard !quotaSummaryWindows.isEmpty else { return nil } - return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown)) + var eligibleWindows = quotaSummaryWindows.filter(\.usageKnown) + if !prioritizeExhausted { + let exhaustedRows = eligibleWindows.filter { $0.window.usedPercent >= 100 } + let exhaustedFamilies = Set(exhaustedRows + .compactMap { MenuBarMetricWindowResolver.antigravityQuotaFamily(for: $0) }) + let filtered = eligibleWindows.filter { row in + guard let family = MenuBarMetricWindowResolver.antigravityQuotaFamily(for: row) else { + return true + } + return !exhaustedFamilies.contains(family) + } + if !filtered.isEmpty { + eligibleWindows = filtered + } + } + + return self.antigravityQuotaSummaryPair(in: eligibleWindows) } private static func antigravityQuotaSummaryPair( @@ -68,6 +85,7 @@ enum IconRemainingResolver { static func resolvedWindows( snapshot: UsageSnapshot, style: IconStyle, + antigravityPrioritizeExhaustedQuotas: Bool = false, secondaryOverrideWindowID: String? = nil, now: Date = Date()) -> (primary: RateWindow?, secondary: RateWindow?) @@ -80,7 +98,9 @@ enum IconRemainingResolver { } if style == .antigravity { // Only current quota-summary buckets define the fixed session/weekly icon lanes. - return self.antigravityQuotaSummaryWindows(snapshot: snapshot) + return self.antigravityQuotaSummaryWindows( + snapshot: snapshot, + prioritizeExhausted: antigravityPrioritizeExhaustedQuotas) ?? (primary: nil, secondary: nil) } if style == .codex { @@ -105,6 +125,7 @@ enum IconRemainingResolver { static func resolvedRemaining( snapshot: UsageSnapshot, style: IconStyle, + antigravityPrioritizeExhaustedQuotas: Bool = false, secondaryOverrideWindowID: String? = nil, now: Date = Date()) -> (primary: Double?, secondary: Double?) @@ -112,6 +133,7 @@ enum IconRemainingResolver { let windows = self.resolvedWindows( snapshot: snapshot, style: style, + antigravityPrioritizeExhaustedQuotas: antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: secondaryOverrideWindowID, now: now) return ( @@ -124,6 +146,7 @@ enum IconRemainingResolver { style: IconStyle, showUsed: Bool, renderingStyle: IconStyle? = nil, + antigravityPrioritizeExhaustedQuotas: Bool = false, secondaryOverrideWindowID: String? = nil, now: Date = Date()) -> (primary: Double?, secondary: Double?) @@ -131,6 +154,7 @@ enum IconRemainingResolver { let windows = Self.resolvedWindows( snapshot: snapshot, style: style, + antigravityPrioritizeExhaustedQuotas: antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: secondaryOverrideWindowID, now: now) var percents = ( diff --git a/Sources/CodexBar/MenuBarLayout.swift b/Sources/CodexBar/MenuBarLayout.swift index ea9444b292..f214265ba8 100644 --- a/Sources/CodexBar/MenuBarLayout.swift +++ b/Sources/CodexBar/MenuBarLayout.swift @@ -16,6 +16,7 @@ enum MenuBarLayoutToken: Codable, Hashable, Sendable { case resetCountdown case resetAbsolute case runsOut + case pacePercent case costToday case cost30d case separatorDot @@ -220,13 +221,13 @@ extension MenuBarLayout { .percent(window: Self.percentWindow(for: metricPreference, provider: provider)), ]]) case .pace: - return MenuBarLayout(lines: [[icon, .runsOut]]) + return MenuBarLayout(lines: [[icon, .pacePercent]]) case .both: return MenuBarLayout(lines: [[ icon, .percent(window: Self.percentWindow(for: metricPreference, provider: provider)), .separatorDot, - .runsOut, + .pacePercent, ]]) case .resetTime: let resetItem = resetTimeDisplayStyle == .absolute diff --git a/Sources/CodexBar/MenuBarLayoutEditor.swift b/Sources/CodexBar/MenuBarLayoutEditor.swift index 316cdd1768..78b2c40f53 100644 --- a/Sources/CodexBar/MenuBarLayoutEditor.swift +++ b/Sources/CodexBar/MenuBarLayoutEditor.swift @@ -251,7 +251,7 @@ struct MenuBarLayoutEditor: View { MenuBarLayoutPaletteGroup( id: "time", title: L("menu_bar_layout_group_time"), - tokens: [.resetCountdown, .resetAbsolute, .runsOut], + tokens: [.resetCountdown, .resetAbsolute, .runsOut, .pacePercent], includesLineBreak: false), MenuBarLayoutPaletteGroup( id: "money", @@ -674,9 +674,12 @@ private struct MenuBarLayoutPreview: View { now: now) } let paceWindow = weekly ?? automatic - let runsOut = paceWindow + let pace = paceWindow .flatMap { self.store.weeklyPace(provider: provider, window: $0, now: now) } + let runsOut = pace .flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel } + let pacePercent = pace + .flatMap { MenuBarDisplayText.paceText(pace: $0) } let cost = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot let costToday = MenuBarLayoutCostResolver.todayCostUSD(snapshot: cost, now: now) return MenuBarLayoutRenderData( @@ -687,6 +690,7 @@ private struct MenuBarLayoutPreview: View { weekly: MenuBarLayoutRenderWindow(weekly), automatic: MenuBarLayoutRenderWindow(automatic), runsOut: runsOut, + pacePercent: pacePercent, costToday: costToday.map { UsageFormatter.currencyString($0, currencyCode: cost?.currencyCode ?? "USD") }, @@ -715,6 +719,7 @@ private struct MenuBarLayoutPreview: View { weekly: MenuBarLayoutRenderWindow(weekly), automatic: MenuBarLayoutRenderWindow(session), runsOut: L("menu_bar_layout_sample_runs_out"), + pacePercent: L("menu_bar_layout_sample_pace_percent"), costToday: "$1.25", cost30d: "$20.00") } @@ -782,6 +787,7 @@ extension MenuBarLayoutToken { case .resetCountdown: L("menu_bar_layout_token_resets_in") case .resetAbsolute: L("menu_bar_layout_token_reset_at") case .runsOut: L("menu_bar_layout_token_runs_out") + case .pacePercent: L("menu_bar_layout_token_pace_percent") case .costToday: L("menu_bar_layout_token_cost_today") case .cost30d: L("menu_bar_layout_token_cost_30d") case .separatorDot: "·" @@ -806,6 +812,7 @@ extension MenuBarLayoutToken { case .resetCountdown: "timer" case .resetAbsolute: "clock" case .runsOut: "hourglass.bottomhalf.filled" + case .pacePercent: "percent" case .costToday: "dollarsign.circle" case .cost30d: "calendar.badge.clock" case .separatorDot: "smallcircle.filled.circle" diff --git a/Sources/CodexBar/MenuBarLayoutRenderer.swift b/Sources/CodexBar/MenuBarLayoutRenderer.swift index 27204013e0..d4470998b8 100644 --- a/Sources/CodexBar/MenuBarLayoutRenderer.swift +++ b/Sources/CodexBar/MenuBarLayoutRenderer.swift @@ -29,6 +29,7 @@ struct MenuBarLayoutRenderData: Hashable { let weekly: MenuBarLayoutRenderWindow? let automatic: MenuBarLayoutRenderWindow? let runsOut: String? + let pacePercent: String? let costToday: String? let cost30d: String? } @@ -276,6 +277,12 @@ final class MenuBarLayoutRenderer { data.runsOut, unavailableLabel: L("Run-out estimate unavailable"), attributes: style.attributes) + case .pacePercent: + return self.optionalTextToken( + data.pacePercent, + unavailableLabel: L("Pace percent unavailable"), + accessibilityPrefix: L("display_mode_pace"), + attributes: style.attributes) case .costToday: return self.optionalTextToken( data.costToday, diff --git a/Sources/CodexBar/MenuBarMetricWindowResolver.swift b/Sources/CodexBar/MenuBarMetricWindowResolver.swift index 7dc22f5dff..c25972e821 100644 --- a/Sources/CodexBar/MenuBarMetricWindowResolver.swift +++ b/Sources/CodexBar/MenuBarMetricWindowResolver.swift @@ -272,7 +272,7 @@ enum MenuBarMetricWindowResolver { } ?? [] } - private static func antigravityQuotaFamily(for row: NamedRateWindow) -> String? { + static func antigravityQuotaFamily(for row: NamedRateWindow) -> String? { let suffix = row.id.dropFirst(Self.antigravityQuotaSummaryWindowIDPrefix.count) var normalizedSuffix = suffix .lowercased() diff --git a/Sources/CodexBar/MenuContent.swift b/Sources/CodexBar/MenuContent.swift index 2a1c33fb46..3b5ca7e32f 100644 --- a/Sources/CodexBar/MenuContent.swift +++ b/Sources/CodexBar/MenuContent.swift @@ -225,7 +225,8 @@ struct StatusIconView: View { } let remaining = IconRemainingResolver.resolvedRemaining( snapshot: snap, - style: self.store.style(for: self.provider)) + style: self.store.style(for: self.provider), + antigravityPrioritizeExhaustedQuotas: self.store.settings.antigravityPrioritizeExhaustedQuotas) let primary = remaining.primary let percent = primary.map(Self.accessibilityPercentRemaining) ?? L("Unknown") let stale = self.store.isStale(provider: self.provider) @@ -243,6 +244,7 @@ struct StatusIconView: View { IconRemainingResolver.resolvedRemaining( snapshot: $0, style: self.store.style(for: self.provider), + antigravityPrioritizeExhaustedQuotas: self.store.settings.antigravityPrioritizeExhaustedQuotas, now: now) } let creditsProjection = self.store.codexConsumerProjectionIfNeeded( diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index be1b7a75dc..ac15a001cb 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -1345,3 +1345,6 @@ "Cost today unavailable" = "تكلفة اليوم: غير متوفر"; "30-day cost unavailable" = "تكلفة 30 يوماً: غير متوفر"; "Resets" = "إعادات الضبط"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 55985b0af6..ddb56ead34 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -1344,3 +1344,6 @@ "Cost today unavailable" = "Cost d’avui: No disponible"; "30-day cost unavailable" = "Cost de 30 dies: No disponible"; "Resets" = "Reinicis"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 37d2b62165..7db23c6d45 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -1342,3 +1342,6 @@ "Cost today unavailable" = "Kosten heute: Nicht verfügbar"; "30-day cost unavailable" = "Kosten 30 Tage: Nicht verfügbar"; "Resets" = "Zurücksetzungen"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Tempo %"; +"Pace percent unavailable" = "Tempoprozent nicht verfügbar"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 5cc6b2defb..31407ec9f9 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -1346,3 +1346,6 @@ "Cost today unavailable" = "Cost today unavailable"; "30-day cost unavailable" = "30-day cost unavailable"; "Resets" = "Resets"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 90159b2bc2..81a3a077ef 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -1340,3 +1340,6 @@ "Cost today unavailable" = "Coste de hoy: No disponible"; "30-day cost unavailable" = "Coste de 30 días: No disponible"; "Resets" = "Reinicios"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 789090531a..6889462d82 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -1345,3 +1345,6 @@ "Cost today unavailable" = "هزینهٔ امروز: در دسترس نیست"; "30-day cost unavailable" = "هزینهٔ ۳۰ روز: در دسترس نیست"; "Resets" = "بازنشانی‌ها"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index fafb4abed0..2445a921e9 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -1341,3 +1341,6 @@ "Cost today unavailable" = "Coût aujourd’hui: Indisponible"; "30-day cost unavailable" = "Coût sur 30 j: Indisponible"; "Resets" = "Réinitialisations"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Rythme %"; +"Pace percent unavailable" = "Pourcentage de rythme indisponible"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 82cc28af19..c1c14e6f01 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -1341,3 +1341,6 @@ "Cost today unavailable" = "Custo de hoxe: Non dispoñible"; "30-day cost unavailable" = "Custo de 30 días: Non dispoñible"; "Resets" = "Reinicios"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index abebb2f911..62644989f0 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -1345,3 +1345,6 @@ "Cost today unavailable" = "Biaya hari ini: Tidak tersedia"; "30-day cost unavailable" = "Biaya 30 hari: Tidak tersedia"; "Resets" = "Reset"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 402383c328..933a442ab1 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1345,3 +1345,6 @@ "Cost today unavailable" = "Costo oggi: Non disponibile"; "30-day cost unavailable" = "Costo 30 gg: Non disponibile"; "Resets" = "Ripristini"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 51f5987788..13889f9f54 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -1342,3 +1342,6 @@ "Cost today unavailable" = "今日のコスト: 利用不可"; "30-day cost unavailable" = "30日間のコスト: 利用不可"; "Resets" = "リセット"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 57d7245382..79413873a9 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -1309,3 +1309,6 @@ "Cost today unavailable" = "오늘 비용: 사용할 수 없음"; "30-day cost unavailable" = "30일 비용: 사용할 수 없음"; "Resets" = "재설정"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index fbf7cbf4df..66b1734b31 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -1341,3 +1341,6 @@ "Cost today unavailable" = "Kosten vandaag: Niet beschikbaar"; "30-day cost unavailable" = "Kosten 30 dagen: Niet beschikbaar"; "Resets" = "Resets"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 92351c1788..1aa13e948f 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -1345,3 +1345,6 @@ "Cost today unavailable" = "Koszt dzisiaj: Niedostępne"; "30-day cost unavailable" = "Koszt 30 dni: Niedostępne"; "Resets" = "Resety"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 9fc56f647d..e65a571500 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -1342,3 +1342,6 @@ "Cost today unavailable" = "Custo hoje: Indisponível"; "30-day cost unavailable" = "Custo em 30 dias: Indisponível"; "Resets" = "Redefinições"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index debe92c848..679189d61e 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -1343,3 +1343,6 @@ "Cost today unavailable" = "Расход сегодня: Недоступно"; "30-day cost unavailable" = "Расход за 30 дней: Недоступно"; "Resets" = "Сбросы"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 18e1d3c828..ca17e83221 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -1340,3 +1340,6 @@ "Cost today unavailable" = "Kostnad idag: Inte tillgänglig"; "30-day cost unavailable" = "Kostnad 30 dagar: Inte tillgänglig"; "Resets" = "Återställningar"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 686d42e6ef..a1ef4b7860 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -1345,3 +1345,6 @@ "Cost today unavailable" = "ค่าใช้จ่ายวันนี้: ไม่พร้อมใช้งาน"; "30-day cost unavailable" = "ค่าใช้จ่าย 30 วัน: ไม่พร้อมใช้งาน"; "Resets" = "การรีเซ็ต"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index b8eb70a926..f887c24915 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -1343,3 +1343,6 @@ "Cost today unavailable" = "Bugünkü maliyet: Kullanılamıyor"; "30-day cost unavailable" = "30 günlük maliyet: Kullanılamıyor"; "Resets" = "Sıfırlamalar"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 359780b371..7ce6c00027 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -1341,3 +1341,6 @@ "Cost today unavailable" = "Вартість сьогодні: Недоступний"; "30-day cost unavailable" = "Вартість за 30 днів: Недоступний"; "Resets" = "Скидання"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 14dbaabac6..0091287875 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -1342,3 +1342,6 @@ "Cost today unavailable" = "Chi phí hôm nay: Không có sẵn"; "30-day cost unavailable" = "Chi phí 30 ngày: Không có sẵn"; "Resets" = "Lần đặt lại"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "Pace %"; +"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index ae94fdb4b8..11bad1112b 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1317,3 +1317,6 @@ "Cost today unavailable" = "今日费用: 不可用"; "30-day cost unavailable" = "30 天费用: 不可用"; "Resets" = "重置"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "进度 %"; +"Pace percent unavailable" = "进度百分比不可用"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index c5a328b8c9..d4fd23ef45 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -1372,3 +1372,6 @@ "Cost today unavailable" = "今日費用: 無法使用"; "30-day cost unavailable" = "30 天費用: 無法使用"; "Resets" = "重設"; +"menu_bar_layout_sample_pace_percent" = "+15%"; +"menu_bar_layout_token_pace_percent" = "進度 %"; +"Pace percent unavailable" = "進度百分比不可用"; diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index d2efd57f76..5426bd909c 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -658,6 +658,7 @@ extension StatusItemController { style: style, showUsed: showUsed, renderingStyle: renderingStyle, + antigravityPrioritizeExhaustedQuotas: self.settings.antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: self.settings.copilotIconSecondaryWindowOverrideID(snapshot: snapshot)) } diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index f5d0eaa43c..03697e186b 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1384,6 +1384,7 @@ extension StatusItemController { snapshot: $0, style: style, showUsed: showUsed, + antigravityPrioritizeExhaustedQuotas: self.settings.antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: self.settings.copilotIconSecondaryWindowOverrideID(snapshot: $0), now: now) } diff --git a/Sources/CodexBar/StatusItemController+MenuBarLayout.swift b/Sources/CodexBar/StatusItemController+MenuBarLayout.swift index 133dac8a1c..a9807ab85a 100644 --- a/Sources/CodexBar/StatusItemController+MenuBarLayout.swift +++ b/Sources/CodexBar/StatusItemController+MenuBarLayout.swift @@ -57,9 +57,12 @@ extension StatusItemController { { let windows = self.menuBarLayoutWindows(provider: provider, snapshot: snapshot, now: now) let paceWindow = windows.weekly ?? windows.automatic - let runsOut = paceWindow + let pace = paceWindow .flatMap { self.store.weeklyPace(provider: provider, window: $0, now: now) } + let runsOut = pace .flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel } + let pacePercent = pace + .flatMap { MenuBarDisplayText.paceText(pace: $0) } let costStrings = self.menuBarLayoutCostStrings(provider: provider, now: now) let providerName = L(self.store.metadata(for: provider).displayName) let rawAccountLabel = snapshot?.accountEmail(for: provider)? @@ -76,6 +79,7 @@ extension StatusItemController { weekly: MenuBarLayoutRenderWindow(windows.weekly), automatic: MenuBarLayoutRenderWindow(windows.automatic), runsOut: runsOut, + pacePercent: pacePercent, costToday: costStrings.today, cost30d: costStrings.last30Days) } diff --git a/Sources/CodexBar/UsageStore+HighestUsage.swift b/Sources/CodexBar/UsageStore+HighestUsage.swift index e1f91866f9..639378ef0b 100644 --- a/Sources/CodexBar/UsageStore+HighestUsage.swift +++ b/Sources/CodexBar/UsageStore+HighestUsage.swift @@ -47,7 +47,9 @@ extension UsageStore { effectivePreference == .automatic, !self.settings.antigravityPrioritizeExhaustedQuotas { - return Self.mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot) + return Self.mostConstrainedAntigravityQuotaSummaryWindow( + snapshot: snapshot, + prioritizeExhausted: self.settings.antigravityPrioritizeExhaustedQuotas) } if provider == .codex { return self.codexMenuBarMetricWindow(snapshot: snapshot, now: now) @@ -99,7 +101,9 @@ extension UsageStore { if self.settings.antigravityPrioritizeExhaustedQuotas { return MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot) } - let windows = Self.antigravityRenderedQuotaSummaryWindows(snapshot: snapshot) + let windows = Self.antigravityRenderedQuotaSummaryWindows( + snapshot: snapshot, + prioritizeExhausted: self.settings.antigravityPrioritizeExhaustedQuotas) guard !windows.isEmpty else { return true } return windows.allSatisfy { $0.usedPercent >= 100 } } @@ -127,24 +131,42 @@ extension UsageStore { } private nonisolated static func mostConstrainedAntigravityQuotaSummaryWindow( - snapshot: UsageSnapshot) + snapshot: UsageSnapshot, + prioritizeExhausted: Bool) -> RateWindow? { - let windows = self.antigravityRenderedQuotaSummaryWindows(snapshot: snapshot) + let windows = self.antigravityRenderedQuotaSummaryWindows( + snapshot: snapshot, + prioritizeExhausted: prioritizeExhausted) 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 !usableWindows.isEmpty { + // Prefer 5-hour session windows over weekly windows: the session limit + // is the immediate constraint the user cares about. + let sessionWindows = usableWindows.filter { + $0.windowMinutes == 300 + } + if let mostConstrainedSession = sessionWindows.max(by: { + $0.usedPercent < $1.usedPercent + }) { + return mostConstrainedSession + } + return usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) } + // All exhausted: show the most exhausted window. return windows.max(by: { $0.usedPercent < $1.usedPercent }) } private nonisolated static func antigravityRenderedQuotaSummaryWindows( - snapshot: UsageSnapshot) + snapshot: UsageSnapshot, + prioritizeExhausted: Bool) -> [RateWindow] { - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) + let windows = IconRemainingResolver.resolvedWindows( + snapshot: snapshot, + style: .antigravity, + antigravityPrioritizeExhaustedQuotas: prioritizeExhausted) return [windows.primary, windows.secondary].compactMap(\.self) } } diff --git a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift index 3d423cede9..ccd7c7f307 100644 --- a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift +++ b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift @@ -515,6 +515,7 @@ struct LocalizationLanguageCatalogTests { "Google OAuth", "No", "Oasis-Token", + "Pace percent unavailable", "Password", "Provider", "Token", @@ -536,7 +537,9 @@ struct LocalizationLanguageCatalogTests { "link_email", "link_github", "menu_bar_layout_sample_account", + "menu_bar_layout_sample_pace_percent", "menu_bar_layout_token_account", + "menu_bar_layout_token_pace_percent", "ory_session_…=…; csrftoken=…", "section_privacy", "tab_menu", diff --git a/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift b/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift index 1bd43ddd00..365047a5bf 100644 --- a/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift +++ b/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift @@ -89,6 +89,7 @@ struct MenuBarLayoutRendererTests { weekly: nil, automatic: nil, runsOut: nil, + pacePercent: nil, costToday: nil, cost30d: nil) let layout = MenuBarLayout(lines: [[ @@ -205,6 +206,7 @@ struct MenuBarLayoutRendererTests { weekly: nil, automatic: textOnlyWindow, runsOut: nil, + pacePercent: nil, costToday: nil, cost30d: nil) @@ -262,10 +264,31 @@ struct MenuBarLayoutRendererTests { resetsAt: self.now.addingTimeInterval(2 * 60 * 60), resetDescription: nil)), runsOut: "Runs out tomorrow", + pacePercent: "+15%", costToday: "$1.25", cost30d: "$20.00") } + @Test + func `renders pace percent`() { + let renderer = MenuBarLayoutRenderer() + let data = MenuBarLayoutRenderData( + iconKey: "codex", + providerName: "Codex", + accountLabel: nil, + session: nil, + weekly: nil, + automatic: nil, + runsOut: nil, + pacePercent: "+15%", + costToday: nil, + cost30d: nil) + let layout = MenuBarLayout(lines: [[.pacePercent]]) + let output = renderer.render(layout: layout, data: data, icon: nil, options: self.options()) + #expect(output.attributedTitle.string == "+15%") + #expect(output.accessibilityLabel.contains("Pace")) + } + private func options() -> MenuBarLayoutRenderOptions { MenuBarLayoutRenderOptions( size: .regular, diff --git a/Tests/CodexBarTests/MenuBarLayoutTests.swift b/Tests/CodexBarTests/MenuBarLayoutTests.swift index 2aeed8f5a0..ec2d2557bc 100644 --- a/Tests/CodexBarTests/MenuBarLayoutTests.swift +++ b/Tests/CodexBarTests/MenuBarLayoutTests.swift @@ -207,18 +207,14 @@ struct MenuBarLayoutTests { pace: pace, showUsed: true) ?? "nil" - // 2. Calculate new migrated rendering output (which maps legacy `.pace` to `.runsOut` layout token) + // 2. Calculate new migrated rendering output (which maps legacy `.pace` to `.pacePercent` layout token) let resolution = MenuBarLayoutResolution.legacy( iconStyle: .iconAndPercent, displayMode: .pace, metricPreference: .primary, resetTimeDisplayStyle: .countdown) let migratedTokens = Array(resolution.layout.lines.joined()) - #expect(migratedTokens.contains(.runsOut)) - - // Get the text that .runsOut resolves to for the same pace - // weeklyDetail's rightLabel for willLastToReset is "Lasts until reset" - let runsOutText = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: Date()).rightLabel ?? "nil" + #expect(migratedTokens.contains(.pacePercent)) let renderer = MenuBarLayoutRenderer() let data = MenuBarLayoutRenderData( @@ -228,11 +224,12 @@ struct MenuBarLayoutTests { session: nil, weekly: nil, automatic: nil, - runsOut: runsOutText, + runsOut: nil, + pacePercent: legacyPaceString, costToday: nil, cost30d: nil) let output = renderer.render( - layout: MenuBarLayout(lines: [[.runsOut]]), + layout: MenuBarLayout(lines: [[.pacePercent]]), data: data, icon: nil, options: MenuBarLayoutRenderOptions( @@ -248,11 +245,11 @@ struct MenuBarLayoutTests { print("\n==================================================") print("REPRODUCTION - PACE METRIC DISCREPANCY COMPARISON") print("Legacy rendering output (using .pace displayMode): \(legacyPaceString)") - print("New layout rendering output (migrated to .runsOut token): \(newPaceString)") + print("New layout rendering output (migrated to .pacePercent token): \(newPaceString)") print("==================================================\n") #expect(legacyPaceString == "+15%") - #expect(newPaceString == "Lasts until reset") + #expect(newPaceString == "+15%") } @Test diff --git a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift index b47fd1383d..18b09f59fd 100644 --- a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift +++ b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift @@ -391,6 +391,43 @@ extension UsageStoreHighestUsageTests { #expect(failOpenHighest?.usedPercent == 100) } + @Test + func `antigravity automatic metric ignores exhausted family and prefers usable session window`() { + let settings = SettingsStore( + configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-exhausted-family"), + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + settings.refreshFrequency = .manual + settings.statusChecksEnabled = false + settings.setMenuBarMetricPreference(.automatic, for: .antigravity) + settings.antigravityPrioritizeExhaustedQuotas = false + + let registry = ProviderRegistry.shared + if let antigravityMeta = registry.metadata[.antigravity] { + settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) + } + + let store = UsageStore( + fetcher: UsageFetcher(), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings) + + // Claude/GPT session (otherSession) is exhausted (100%), and Claude/GPT weekly (otherWeekly) is 70% used. + // Gemini session is 15% used and weekly is 20% used. + let snapshot = self.antigravityQuotaSummarySnapshot( + geminiSessionUsed: 15, + geminiWeeklyUsed: 20, + otherSessionUsed: 100, + otherWeeklyUsed: 70) + store._setSnapshotForTesting(snapshot, provider: .antigravity) + + let highest = store.providerWithHighestUsage() + #expect(highest?.provider == .antigravity) + // Since prioritizeExhausted is false, we ignore the exhausted Claude family. + // The remaining usable family is Gemini, and we prefer its session window (15%) over weekly (20%). + #expect(highest?.usedPercent == 15) + } + @Test func `automatic metric ignores antigravity legacy detail rows`() { let settings = SettingsStore( From ff22cdf5681345217c825cd7780df49b537b12d9 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:30:29 +0800 Subject: [PATCH 05/10] Simplify Antigravity display to track Gemini only --- Sources/CodexBar/IconRemainingResolver.swift | 66 --- .../MenuBarMetricWindowResolver.swift | 159 ------- Sources/CodexBar/MenuContent.swift | 4 +- .../StatusItemController+Animation.swift | 1 - .../CodexBar/StatusItemController+Menu.swift | 1 - .../CodexBar/UsageStore+HighestUsage.swift | 58 --- .../Antigravity/AntigravityStatusProbe.swift | 12 +- Tests/CodexBarTests/CodexbarTests.swift | 198 +-------- .../MenuBarMetricWindowResolverTests.swift | 402 +----------------- .../UsageStoreHighestUsageTests.swift | 267 +----------- 10 files changed, 37 insertions(+), 1131 deletions(-) diff --git a/Sources/CodexBar/IconRemainingResolver.swift b/Sources/CodexBar/IconRemainingResolver.swift index f1b1149c4f..4e8a831d63 100644 --- a/Sources/CodexBar/IconRemainingResolver.swift +++ b/Sources/CodexBar/IconRemainingResolver.swift @@ -28,64 +28,9 @@ enum IconRemainingResolver { return projection.visibleRateLanes.compactMap { projection.menuBarSelectableRateWindow(for: $0) } } - private static func antigravityQuotaSummaryWindows( - snapshot: UsageSnapshot, - prioritizeExhausted: Bool) - -> (primary: RateWindow?, secondary: RateWindow?)? - { - let quotaSummaryWindows = snapshot.extraRateWindows? - .filter { - $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) - } ?? [] - guard !quotaSummaryWindows.isEmpty else { return nil } - - var eligibleWindows = quotaSummaryWindows.filter(\.usageKnown) - if !prioritizeExhausted { - let exhaustedRows = eligibleWindows.filter { $0.window.usedPercent >= 100 } - let exhaustedFamilies = Set(exhaustedRows - .compactMap { MenuBarMetricWindowResolver.antigravityQuotaFamily(for: $0) }) - let filtered = eligibleWindows.filter { row in - guard let family = MenuBarMetricWindowResolver.antigravityQuotaFamily(for: row) else { - return true - } - return !exhaustedFamilies.contains(family) - } - if !filtered.isEmpty { - eligibleWindows = filtered - } - } - - return self.antigravityQuotaSummaryPair(in: eligibleWindows) - } - - 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, - antigravityPrioritizeExhaustedQuotas: Bool = false, secondaryOverrideWindowID: String? = nil, now: Date = Date()) -> (primary: RateWindow?, secondary: RateWindow?) @@ -96,13 +41,6 @@ enum IconRemainingResolver { primary: windows.first, secondary: windows.dropFirst().first) } - if style == .antigravity { - // Only current quota-summary buckets define the fixed session/weekly icon lanes. - return self.antigravityQuotaSummaryWindows( - snapshot: snapshot, - prioritizeExhausted: antigravityPrioritizeExhaustedQuotas) - ?? (primary: nil, secondary: nil) - } if style == .codex { let windows = self.codexVisibleWindows(snapshot: snapshot, now: now) return ( @@ -125,7 +63,6 @@ enum IconRemainingResolver { static func resolvedRemaining( snapshot: UsageSnapshot, style: IconStyle, - antigravityPrioritizeExhaustedQuotas: Bool = false, secondaryOverrideWindowID: String? = nil, now: Date = Date()) -> (primary: Double?, secondary: Double?) @@ -133,7 +70,6 @@ enum IconRemainingResolver { let windows = self.resolvedWindows( snapshot: snapshot, style: style, - antigravityPrioritizeExhaustedQuotas: antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: secondaryOverrideWindowID, now: now) return ( @@ -146,7 +82,6 @@ enum IconRemainingResolver { style: IconStyle, showUsed: Bool, renderingStyle: IconStyle? = nil, - antigravityPrioritizeExhaustedQuotas: Bool = false, secondaryOverrideWindowID: String? = nil, now: Date = Date()) -> (primary: Double?, secondary: Double?) @@ -154,7 +89,6 @@ enum IconRemainingResolver { let windows = Self.resolvedWindows( snapshot: snapshot, style: style, - antigravityPrioritizeExhaustedQuotas: antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: secondaryOverrideWindowID, now: now) var percents = ( diff --git a/Sources/CodexBar/MenuBarMetricWindowResolver.swift b/Sources/CodexBar/MenuBarMetricWindowResolver.swift index c25972e821..1c4e571f98 100644 --- a/Sources/CodexBar/MenuBarMetricWindowResolver.swift +++ b/Sources/CodexBar/MenuBarMetricWindowResolver.swift @@ -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() } @@ -180,156 +165,12 @@ enum MenuBarMetricWindowResolver { 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 !usableWindows.isEmpty { - // Prefer 5-hour session windows over weekly windows: the session limit - // is the immediate constraint the user cares about. - let sessionWindows = usableWindows.filter { - $0.windowMinutes == Self.antigravitySessionCadence - } - if let mostConstrainedSession = sessionWindows.max(by: { - $0.usedPercent < $1.usedPercent - }) { - return mostConstrainedSession - } - return usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) - } - // All exhausted: show the most exhausted window. - 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 antigravitySessionCadence = 300 - private static let antigravitySupportedQuotaCadences: Set = [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) - } ?? [] - } - - 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 - } - 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 }) - } - private static func requestedWindow( provider: UsageProvider, snapshot: UsageSnapshot, 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? { diff --git a/Sources/CodexBar/MenuContent.swift b/Sources/CodexBar/MenuContent.swift index 3b5ca7e32f..2a1c33fb46 100644 --- a/Sources/CodexBar/MenuContent.swift +++ b/Sources/CodexBar/MenuContent.swift @@ -225,8 +225,7 @@ struct StatusIconView: View { } let remaining = IconRemainingResolver.resolvedRemaining( snapshot: snap, - style: self.store.style(for: self.provider), - antigravityPrioritizeExhaustedQuotas: self.store.settings.antigravityPrioritizeExhaustedQuotas) + style: self.store.style(for: self.provider)) let primary = remaining.primary let percent = primary.map(Self.accessibilityPercentRemaining) ?? L("Unknown") let stale = self.store.isStale(provider: self.provider) @@ -244,7 +243,6 @@ struct StatusIconView: View { IconRemainingResolver.resolvedRemaining( snapshot: $0, style: self.store.style(for: self.provider), - antigravityPrioritizeExhaustedQuotas: self.store.settings.antigravityPrioritizeExhaustedQuotas, now: now) } let creditsProjection = self.store.codexConsumerProjectionIfNeeded( diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 5426bd909c..d2efd57f76 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -658,7 +658,6 @@ extension StatusItemController { style: style, showUsed: showUsed, renderingStyle: renderingStyle, - antigravityPrioritizeExhaustedQuotas: self.settings.antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: self.settings.copilotIconSecondaryWindowOverrideID(snapshot: snapshot)) } diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 03697e186b..f5d0eaa43c 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1384,7 +1384,6 @@ extension StatusItemController { snapshot: $0, style: style, showUsed: showUsed, - antigravityPrioritizeExhaustedQuotas: self.settings.antigravityPrioritizeExhaustedQuotas, secondaryOverrideWindowID: self.settings.copilotIconSecondaryWindowOverrideID(snapshot: $0), now: now) } diff --git a/Sources/CodexBar/UsageStore+HighestUsage.swift b/Sources/CodexBar/UsageStore+HighestUsage.swift index 639378ef0b..751e023644 100644 --- a/Sources/CodexBar/UsageStore+HighestUsage.swift +++ b/Sources/CodexBar/UsageStore+HighestUsage.swift @@ -43,14 +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, - prioritizeExhausted: self.settings.antigravityPrioritizeExhaustedQuotas) - } if provider == .codex { return self.codexMenuBarMetricWindow(snapshot: snapshot, now: now) } @@ -97,16 +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, - prioritizeExhausted: self.settings.antigravityPrioritizeExhaustedQuotas) - guard !windows.isEmpty else { return true } - return windows.allSatisfy { $0.usedPercent >= 100 } - } if provider == .copilot, effectivePreference == .automatic, let primary = snapshot.primary, @@ -129,44 +111,4 @@ extension UsageStore { return true } - - private nonisolated static func mostConstrainedAntigravityQuotaSummaryWindow( - snapshot: UsageSnapshot, - prioritizeExhausted: Bool) - -> RateWindow? - { - let windows = self.antigravityRenderedQuotaSummaryWindows( - snapshot: snapshot, - prioritizeExhausted: prioritizeExhausted) - guard !windows.isEmpty else { return nil } - - let usableWindows = windows.filter { $0.usedPercent < 100 } - if !usableWindows.isEmpty { - // Prefer 5-hour session windows over weekly windows: the session limit - // is the immediate constraint the user cares about. - let sessionWindows = usableWindows.filter { - $0.windowMinutes == 300 - } - if let mostConstrainedSession = sessionWindows.max(by: { - $0.usedPercent < $1.usedPercent - }) { - return mostConstrainedSession - } - return usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) - } - // All exhausted: show the most exhausted window. - return windows.max(by: { $0.usedPercent < $1.usedPercent }) - } - - private nonisolated static func antigravityRenderedQuotaSummaryWindows( - snapshot: UsageSnapshot, - prioritizeExhausted: Bool) - -> [RateWindow] - { - let windows = IconRemainingResolver.resolvedWindows( - snapshot: snapshot, - style: .antigravity, - antigravityPrioritizeExhaustedQuotas: prioritizeExhausted) - return [windows.primary, windows.secondary].compactMap(\.self) - } } diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index dbe044f6fd..0a87cd41a0 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -208,11 +208,15 @@ public struct AntigravityStatusSnapshot: Sendable { } let primary = Self.quotaSummaryRepresentative( - matching: { $0.lowercased().contains("gemini") }, - in: namedWindows) + matching: { name in + name.lowercased().contains("gemini") + }, + in: namedWindows.filter { $0.window.windowMinutes == 5 * 60 }) let secondary = Self.quotaSummaryRepresentative( - matching: { $0.lowercased().contains("claude") || $0.lowercased().contains("gpt") }, - in: namedWindows) + matching: { name in + name.lowercased().contains("gemini") + }, + in: namedWindows.filter { $0.window.windowMinutes == 7 * 24 * 60 }) let identity = ProviderIdentitySnapshot( providerID: .antigravity, diff --git a/Tests/CodexBarTests/CodexbarTests.swift b/Tests/CodexBarTests/CodexbarTests.swift index 7502ab9155..6628291028 100644 --- a/Tests/CodexBarTests/CodexbarTests.swift +++ b/Tests/CodexBarTests/CodexbarTests.swift @@ -75,35 +75,17 @@ struct CodexBarTests { @Test func `antigravity quota summary icon shows session on top and weekly on bottom`() { let snapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 84, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - secondary: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), + primary: RateWindow(usedPercent: 97, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 84, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 84, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 97, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude + GPT Session", - window: RateWindow(usedPercent: 98, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - ], updatedAt: Date()) let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) #expect(windows.primary?.windowMinutes == 300) - #expect(windows.primary?.remainingPercent == 2) + #expect(windows.primary?.remainingPercent == 3) #expect(windows.secondary?.windowMinutes == 10080) - #expect(windows.secondary?.remainingPercent == 1) + #expect(windows.secondary?.remainingPercent == 16) } @Test @@ -138,178 +120,6 @@ struct CodexBarTests { #expect(visualTopRightAlpha > visualBottomRightAlpha + 0.2) } - @Test - func `antigravity quota summary icon uses most constrained quota summary lanes`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Renamed Weekly", - window: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Renamed Session", - window: RateWindow(usedPercent: 40, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 98, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.remainingPercent == 2) - #expect(windows.secondary?.remainingPercent == 1) - } - - @Test - func `antigravity quota summary icon can pair gemini session with claude gpt weekly`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 40, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.remainingPercent == 60) - #expect(windows.secondary?.remainingPercent == 1) - } - - @Test - func `antigravity quota summary icon ignores unknown rows while ranking known lanes`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 100, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - usageKnown: false), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary == nil) - #expect(windows.secondary?.remainingPercent == 1) - } - - @Test - func `antigravity used icon percent matches constrained claude gpt lane`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 20, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude + GPT Session", - window: RateWindow(usedPercent: 95, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 40, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let percents = IconRemainingResolver.resolvedPercents( - snapshot: snapshot, - style: .antigravity, - showUsed: true) - - #expect(percents.primary == 95) - #expect(percents.secondary == 40) - } - - @Test - func `antigravity quota summary icon falls back when gemini rows are absent`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude + GPT Session", - window: RateWindow(usedPercent: 75, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 88, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.remainingPercent == 25) - #expect(windows.secondary?.remainingPercent == 12) - } - - @Test - func `antigravity quota summary icon tie break is stable`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-z-5h", - title: "Gemini Session", - window: RateWindow( - usedPercent: 50, - windowMinutes: 300, - resetsAt: nil, - resetDescription: "second-by-id")), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-a-5h", - title: "Gemini Session", - window: RateWindow( - usedPercent: 50, - windowMinutes: 300, - resetsAt: nil, - resetDescription: "first-by-id")), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.resetDescription == "first-by-id") - #expect(windows.secondary == nil) - } - @Test func `perplexity icon falls back to purchased lane when bonus is exhausted`() { let snapshot = UsageSnapshot( diff --git a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift index b7cad30823..f322c5ca5b 100644 --- a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift +++ b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift @@ -3,7 +3,6 @@ import Foundation import Testing @testable import CodexBar -// swiftlint:disable:next type_body_length struct MenuBarMetricWindowResolverTests { @Test func `gemini metrics fall back to Flash when Pro is unavailable`() { @@ -263,80 +262,15 @@ struct MenuBarMetricWindowResolverTests { #expect(window?.resetDescription == "Gemini Pro") } - @Test - func `automatic metric preserves usable first by default and prioritizes exhausted lane when enabled`() { - 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 defaultWindow = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false) - let optInWindow = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false, - antigravityPrioritizeExhaustedQuotas: true) - - #expect(defaultWindow?.remainingPercent == 29) - #expect(defaultWindow?.windowMinutes == 300) - #expect(optInWindow?.remainingPercent == 0) - #expect(optInWindow?.windowMinutes == 300) - } - @Test func `automatic metric prefers antigravity session window over weekly when none are exhausted`() { let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Models Five Hour Limit", - window: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5h")), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Models Weekly Limit", - window: RateWindow( - usedPercent: 15, - windowMinutes: 10080, - resetsAt: nil, - resetDescription: "weekly")), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude and GPT models Five Hour Limit", - window: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5h")), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude and GPT models Weekly Limit", - window: RateWindow( - usedPercent: 34, - windowMinutes: 10080, - resetsAt: nil, - resetDescription: "weekly")), - ], + primary: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5h"), + secondary: RateWindow( + usedPercent: 15, + windowMinutes: 10080, + resetsAt: nil, + resetDescription: "weekly"), updatedAt: Date()) let window = MenuBarMetricWindowResolver.rateWindow( @@ -351,330 +285,6 @@ struct MenuBarMetricWindowResolverTests { #expect(window?.resetDescription == "5h") } - @Test - func `automatic metric uses recognized antigravity gemini pool when claude gpt is reset only`() throws { - let resetOnlyReset = Date(timeIntervalSince1970: 1000) - let exhaustedReset = Date(timeIntervalSince1970: 2000) - let antigravitySnapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Claude Sonnet 4.6", - modelId: "claude-sonnet-4-6", - remainingFraction: nil, - resetTime: resetOnlyReset, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3.1 Pro", - modelId: "gemini-3-1-pro", - remainingFraction: 0, - resetTime: exhaustedReset, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - let snapshot = try antigravitySnapshot.toUsageSnapshot() - #expect(snapshot.primary?.usedPercent == 100) - #expect(snapshot.primary?.resetsAt == exhaustedReset) - #expect(snapshot.secondary == nil) - - let window = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false) - - #expect(window?.usedPercent == 100) - #expect(window?.resetsAt == exhaustedReset) - } - - @Test - func `automatic metric uses unclassified antigravity compact fallback`() throws { - let antigravitySnapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Experimental Model", - modelId: "MODEL_PLACEHOLDER_NEW", - remainingFraction: 0.36, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - let snapshot = try antigravitySnapshot.toUsageSnapshot() - - let window = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false) - - #expect(window?.usedPercent == 64) - } - - @Test - func `automatic metric keeps legacy antigravity compact fallback usable first semantics`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-compact-fallback-exhausted", - title: "Exhausted", - window: RateWindow( - usedPercent: 100, - windowMinutes: nil, - resetsAt: nil, - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-compact-fallback-usable", - title: "Usable", - window: RateWindow( - usedPercent: 64, - windowMinutes: nil, - resetsAt: nil, - resetDescription: nil)), - ], - updatedAt: Date()) - - let window = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false) - - #expect(window?.usedPercent == 64) - } - - @Test - func `antigravity quota ranking filters unknown and unsupported lanes`() { - let now = Date(timeIntervalSince1970: 100_000) - let expectedReset = now.addingTimeInterval(120) - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-session", - title: "Gemini Session", - window: RateWindow( - usedPercent: 85, - windowMinutes: 300, - resetsAt: expectedReset, - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-daily", - title: "Gemini Daily", - window: RateWindow( - usedPercent: 100, - windowMinutes: 1440, - resetsAt: now.addingTimeInterval(60), - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow( - usedPercent: 99, - windowMinutes: 10080, - resetsAt: now.addingTimeInterval(30), - resetDescription: nil), - usageKnown: false), - NamedRateWindow( - id: "antigravity-quota-summary-invalid-session", - title: "Invalid Session", - window: RateWindow( - usedPercent: .nan, - windowMinutes: 300, - resetsAt: now.addingTimeInterval(10), - resetDescription: nil)), - ], - updatedAt: now) - - let window = MenuBarMetricWindowResolver.antigravityQuotaSummaryRankingWindow( - snapshot: snapshot, - now: now) - - #expect(window?.usedPercent == 85) - #expect(window?.resetsAt == expectedReset) - } - - @Test - func `antigravity quota ranking breaks usage ties by valid nearest reset`() { - let now = Date(timeIntervalSince1970: 100_000) - let nearestFutureReset = now.addingTimeInterval(60) - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-session", - title: "Gemini Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: nil, - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-claude-session", - title: "Claude Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: now.addingTimeInterval(-60), - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gpt-session", - title: "GPT Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: now.addingTimeInterval(120), - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-other-session", - title: "Other Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: nearestFutureReset, - resetDescription: nil)), - ], - updatedAt: now) - - let window = MenuBarMetricWindowResolver.antigravityQuotaSummaryRankingWindow( - snapshot: snapshot, - now: now) - - #expect(window?.resetsAt == nearestFutureReset) - } - - @Test - func `antigravity quota ranking breaks complete ties by stable row ID`() { - let now = Date(timeIntervalSince1970: 100_000) - let rows = [ - NamedRateWindow( - id: "antigravity-quota-summary-a-weekly", - title: "A Weekly", - window: RateWindow( - usedPercent: 90, - windowMinutes: 10080, - resetsAt: nil, - resetDescription: "a")), - NamedRateWindow( - id: "antigravity-quota-summary-z-session", - title: "Z Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: nil, - resetDescription: "z")), - ] - - for orderedRows in [rows, Array(rows.reversed())] { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: orderedRows, - updatedAt: now) - let window = MenuBarMetricWindowResolver.antigravityQuotaSummaryRankingWindow( - snapshot: snapshot, - now: now) - - #expect(window?.resetDescription == "z") - } - } - - @Test - func `antigravity families are blocked only when every understood family has an exhausted lane`() { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("gemini-session", 300, 100, true), - ("gemini-weekly", 10080, 20, true), - ("3p-5-hour", 300, 10, true), - ("3p-weekly", 10080, 100, true), - ]) - - #expect(MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - - let availableFamily = Self.antigravitySummarySnapshot(rows: [ - ("gemini-session", 300, 100, true), - ("3p-session", 300, 99, true), - ]) - #expect(!MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: availableFamily)) - } - - @Test - func `antigravity family blocking accepts underscore cadence delimiters`() { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("gemini_session", 300, 100, true), - ("gemini_weekly", 10080, 20, true), - ("third_party_five_hour", 300, 100, true), - ]) - - #expect(MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - @Test - func `antigravity family blocking accepts limit suffixed cadence`() { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("gemini-5h limit", 300, 100, true), - ("gemini-weekly limit", 10080, 20, true), - ("third-party-session limit", 300, 100, true), - ]) - - #expect(MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - @Test(arguments: [ - ("gemini-session", 300, 100.0, false), - ("gemini-daily", 1440, 100.0, true), - ("-session", 300, 100.0, true), - ("gemini-daily", 300, 100.0, true), - ("gem ini-session", 300, 100.0, true), - ("invalid-session", 300, Double.nan, true), - ]) - func `antigravity family blocking fails open for incomplete summary rows`( - idSuffix: String, - windowMinutes: Int, - usedPercent: Double, - usageKnown: Bool) - { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("safe-session", 300, 100, true), - (idSuffix, windowMinutes, usedPercent, usageKnown), - ]) - - #expect(!MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - @Test - func `antigravity family blocking fails open without quota summary rows`() { - let snapshot = UsageSnapshot(primary: nil, secondary: nil, updatedAt: Date()) - - #expect(!MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - private static func antigravitySummarySnapshot( - rows: [(idSuffix: String, windowMinutes: Int, usedPercent: Double, usageKnown: Bool)]) - -> UsageSnapshot - { - UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: rows.map { row in - NamedRateWindow( - id: "antigravity-quota-summary-\(row.idSuffix)", - title: row.idSuffix, - window: RateWindow( - usedPercent: row.usedPercent, - windowMinutes: row.windowMinutes, - resetsAt: nil, - resetDescription: nil), - usageKnown: row.usageKnown) - }, - updatedAt: Date()) - } - @Test func `explicit antigravity metric keeps requested family lane`() { let snapshot = UsageSnapshot( diff --git a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift index 18b09f59fd..82bbaca702 100644 --- a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift +++ b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift @@ -162,11 +162,9 @@ struct UsageStoreHighestUsageTests { } @Test - func `automatic metric ignores unclassified antigravity compact fallback until exhausted priority is enabled`() - throws - { + func `antigravity automatic metric prefers usable session window`() { let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-unclassified"), + configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-prefers-session"), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.refreshFrequency = .manual @@ -174,9 +172,6 @@ struct UsageStoreHighestUsageTests { settings.setMenuBarMetricPreference(.automatic, for: .antigravity) let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } if let antigravityMeta = registry.metadata[.antigravity] { settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) } @@ -185,234 +180,7 @@ struct UsageStoreHighestUsageTests { fetcher: UsageFetcher(), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) - let codexSnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()) - let antigravitySnapshot = try AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Experimental Model", - modelId: "MODEL_PLACEHOLDER_NEW", - remainingFraction: 0.36, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - .toUsageSnapshot() - - store._setSnapshotForTesting(codexSnapshot, provider: .codex) - store._setSnapshotForTesting(antigravitySnapshot, provider: .antigravity) - - let highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 50) - - settings.antigravityPrioritizeExhaustedQuotas = true - let optInHighest = store.providerWithHighestUsage() - #expect(optInHighest?.provider == .antigravity) - #expect(optInHighest?.usedPercent == 64) - } - - @Test - func `automatic metric ignores legacy antigravity family lanes without quota summary`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-constrained-gemini"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - - let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let fetcher = UsageFetcher() - let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) - - let codexSnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 70, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()) - let antigravitySnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: "Claude"), - secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Pro"), - tertiary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Flash"), - updatedAt: Date()) - - store._setSnapshotForTesting(codexSnapshot, provider: .codex) - store._setSnapshotForTesting(antigravitySnapshot, provider: .antigravity) - - let highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 70) - } -} - -extension UsageStoreHighestUsageTests { - @Test - func `antigravity automatic ranking keeps usable first until exhausted priority is enabled`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-all-summary"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - - let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let store = UsageStore( - fetcher: UsageFetcher(), - browserDetection: BrowserDetection(cacheTTL: 0), - settings: settings) - store._setSnapshotForTesting( - UsageSnapshot( - primary: RateWindow(usedPercent: 80, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()), - provider: .codex) - let antigravity = self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 10, - geminiWeeklyUsed: 20, - otherSessionUsed: 95, - otherWeeklyUsed: 90) - let unknownCadence = NamedRateWindow( - id: "antigravity-quota-summary-future-daily", - title: "Future daily lane", - window: RateWindow( - usedPercent: 99, - windowMinutes: 24 * 60, - resetsAt: nil, - resetDescription: nil)) - store._setSnapshotForTesting( - antigravity.with(extraRateWindows: (antigravity.extraRateWindows ?? []) + [unknownCadence]), - provider: .antigravity) - var highest = store.providerWithHighestUsage() - #expect(highest?.provider == .antigravity) - #expect(highest?.usedPercent == 95) - - store._setSnapshotForTesting( - self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 95, - geminiWeeklyUsed: 20, - otherSessionUsed: 10, - otherWeeklyUsed: 10), - provider: .antigravity) - highest = store.providerWithHighestUsage() - #expect(highest?.provider == .antigravity) - #expect(highest?.usedPercent == 95) - - store._setSnapshotForTesting( - self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 100, - geminiWeeklyUsed: 100, - otherSessionUsed: 50, - otherWeeklyUsed: 50), - provider: .antigravity) - highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 80) - - settings.antigravityPrioritizeExhaustedQuotas = true - highest = store.providerWithHighestUsage() - #expect(highest?.provider == .antigravity) - #expect(highest?.usedPercent == 100) - } - - @Test - func `opt in automatic metric excludes antigravity only when every summary family is blocked`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-summary-usable"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - settings.antigravityPrioritizeExhaustedQuotas = true - - let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let fetcher = UsageFetcher() - let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) - - let codexSnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 80, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()) - let antigravitySnapshot = self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 100, - geminiWeeklyUsed: 40, - otherSessionUsed: 100, - otherWeeklyUsed: 100) - - store._setSnapshotForTesting(codexSnapshot, provider: .codex) - store._setSnapshotForTesting(antigravitySnapshot, provider: .antigravity) - - let highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 80) - - let unsupportedRow = NamedRateWindow( - id: "antigravity-quota-summary-future-daily", - title: "Future daily lane", - window: RateWindow( - usedPercent: 100, - windowMinutes: 1440, - resetsAt: nil, - resetDescription: nil)) - store._setSnapshotForTesting( - antigravitySnapshot.with( - extraRateWindows: (antigravitySnapshot.extraRateWindows ?? []) + [unsupportedRow]), - provider: .antigravity) - - let failOpenHighest = store.providerWithHighestUsage() - #expect(failOpenHighest?.provider == .antigravity) - #expect(failOpenHighest?.usedPercent == 100) - } - - @Test - func `antigravity automatic metric ignores exhausted family and prefers usable session window`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-exhausted-family"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - settings.antigravityPrioritizeExhaustedQuotas = false - - let registry = ProviderRegistry.shared - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let store = UsageStore( - fetcher: UsageFetcher(), - browserDetection: BrowserDetection(cacheTTL: 0), - settings: settings) - - // Claude/GPT session (otherSession) is exhausted (100%), and Claude/GPT weekly (otherWeekly) is 70% used. // Gemini session is 15% used and weekly is 20% used. let snapshot = self.antigravityQuotaSummarySnapshot( geminiSessionUsed: 15, @@ -423,8 +191,6 @@ extension UsageStoreHighestUsageTests { let highest = store.providerWithHighestUsage() #expect(highest?.provider == .antigravity) - // Since prioritizeExhausted is false, we ignore the exhausted Claude family. - // The remaining usable family is Gemini, and we prefer its session window (15%) over weekly (20%). #expect(highest?.usedPercent == 15) } @@ -964,27 +730,30 @@ extension UsageStoreHighestUsageTests { otherSessionUsed: Double, otherWeeklyUsed: Double) -> UsageSnapshot { - UsageSnapshot( - primary: nil, - secondary: nil, + let primary = RateWindow( + usedPercent: geminiSessionUsed, + windowMinutes: 5 * 60, + resetsAt: nil, + resetDescription: "5h") + let secondary = RateWindow( + usedPercent: geminiWeeklyUsed, + windowMinutes: 7 * 24 * 60, + resetsAt: nil, + resetDescription: "weekly") + + return UsageSnapshot( + primary: primary, + secondary: secondary, tertiary: nil, extraRateWindows: [ NamedRateWindow( id: "antigravity-quota-summary-gemini-5h", title: "Gemini Session", - window: RateWindow( - usedPercent: geminiSessionUsed, - windowMinutes: 5 * 60, - resetsAt: nil, - resetDescription: nil)), + window: primary), NamedRateWindow( id: "antigravity-quota-summary-gemini-weekly", title: "Gemini Weekly", - window: RateWindow( - usedPercent: geminiWeeklyUsed, - windowMinutes: 7 * 24 * 60, - resetsAt: nil, - resetDescription: nil)), + window: secondary), NamedRateWindow( id: "antigravity-quota-summary-3p-5h", title: "Claude + GPT Session", From 2adda1c263a3d29ae6866b3e3179148ad18b168f Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:40:37 +0800 Subject: [PATCH 06/10] Fix CI: align test assertions with Gemini-only Antigravity mapping, keep partially exhausted Antigravity eligible for highest-usage --- Sources/CodexBar/UsageStore+HighestUsage.swift | 9 +++++++++ .../CodexBarTests/AntigravityQuotaSummaryTests.swift | 4 ++-- .../CodexBarTests/UsageStoreHighestUsageTests.swift | 12 ++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/UsageStore+HighestUsage.swift b/Sources/CodexBar/UsageStore+HighestUsage.swift index 751e023644..aee0c9e592 100644 --- a/Sources/CodexBar/UsageStore+HighestUsage.swift +++ b/Sources/CodexBar/UsageStore+HighestUsage.swift @@ -108,6 +108,15 @@ extension UsageStore { guard !percents.isEmpty else { return true } return percents.allSatisfy { $0 >= 100 } } + 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 true } diff --git a/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift b/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift index f29e408beb..9fc1d33921 100644 --- a/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift +++ b/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift @@ -54,8 +54,8 @@ struct AntigravityQuotaSummaryTests { ] #expect(windows.map(\.window.resetsAt) == expectedDates) - #expect(usage.primary?.remainingPercent.rounded() == 82) - #expect(usage.secondary?.remainingPercent.rounded() == 64) + #expect(usage.primary?.remainingPercent.rounded() == 91) + #expect(usage.secondary?.remainingPercent.rounded() == 82) #expect(usage.tertiary == nil) } diff --git a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift index 82bbaca702..7af3b3c90e 100644 --- a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift +++ b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift @@ -823,6 +823,18 @@ extension UsageStoreHighestUsageTests { primary: antigravity.primary, secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil)), provider: .antigravity) + // Partially exhausted: secondary is 100% but primary (10%) still has quota — + // Antigravity should remain eligible and surface the exhausted lane as the metric. + highest = store.providerWithHighestUsage() + #expect(highest?.provider == .antigravity) + #expect(highest?.usedPercent == 100) + + // Fully exhausted: both lanes at 100% → Antigravity is excluded. + store._setSnapshotForTesting( + antigravity.with( + primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil)), + provider: .antigravity) highest = store.providerWithHighestUsage() #expect(highest?.provider == .codex) } From cbbd748bff21d60fcb92551e815348653e7a9ede Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:46:52 +0800 Subject: [PATCH 07/10] Fix compact icon resolving for Antigravity, split status probe tests to satisfy length limits, and fix formatting --- Sources/CodexBar/IconRemainingResolver.swift | 9 +- Sources/CodexBar/MenuBarLayoutRenderer.swift | 5 +- ...StatusItemController+IconObservation.swift | 29 ++ .../AntigravityProviderDescriptor.swift | 4 +- .../Antigravity/AntigravityStatusProbe.swift | 16 +- .../AntigravityRemoteUsageFetcherTests.swift | 12 +- ...tigravityStatusProbeTests+Additional.swift | 378 ++++++++++++++++ .../AntigravityStatusProbeTests.swift | 424 ++---------------- .../CodexBarWidgetProviderTests.swift | 2 +- .../MenuCardAntigravityTests.swift | 14 +- .../StatusItemAnimationSignatureTests.swift | 6 +- 11 files changed, 495 insertions(+), 404 deletions(-) create mode 100644 Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift diff --git a/Sources/CodexBar/IconRemainingResolver.swift b/Sources/CodexBar/IconRemainingResolver.swift index 4e8a831d63..6e913b3571 100644 --- a/Sources/CodexBar/IconRemainingResolver.swift +++ b/Sources/CodexBar/IconRemainingResolver.swift @@ -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( @@ -41,6 +37,11 @@ enum IconRemainingResolver { primary: windows.first, secondary: windows.dropFirst().first) } + if style == .antigravity { + 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) return ( diff --git a/Sources/CodexBar/MenuBarLayoutRenderer.swift b/Sources/CodexBar/MenuBarLayoutRenderer.swift index d4470998b8..1e2848ef9b 100644 --- a/Sources/CodexBar/MenuBarLayoutRenderer.swift +++ b/Sources/CodexBar/MenuBarLayoutRenderer.swift @@ -278,8 +278,11 @@ final class MenuBarLayoutRenderer { unavailableLabel: L("Run-out estimate unavailable"), attributes: style.attributes) case .pacePercent: + let rateWindow = data.automatic + let percent = rateWindow.map { options.showUsed ? $0.usedPercent : $0.remainingPercent } + let fallbackValue = percent.map(UsageFormatter.percentString) return self.optionalTextToken( - data.pacePercent, + data.pacePercent ?? fallbackValue, unavailableLabel: L("Pace percent unavailable"), accessibilityPrefix: L("display_mode_pace"), attributes: style.attributes) diff --git a/Sources/CodexBar/StatusItemController+IconObservation.swift b/Sources/CodexBar/StatusItemController+IconObservation.swift index 20dd321b9f..e8ed109cc7 100644 --- a/Sources/CodexBar/StatusItemController+IconObservation.swift +++ b/Sources/CodexBar/StatusItemController+IconObservation.swift @@ -47,6 +47,9 @@ extension StatusItemController { let layoutCostSignature = showBrandPercent ? self.storedMenuBarLayoutCostSignature(for: provider) : nil + let layoutPaceSignature = showBrandPercent + ? self.storedMenuBarLayoutPaceSignature(for: provider, snapshot: snapshot) + : nil return [ provider.rawValue, @@ -60,9 +63,35 @@ extension StatusItemController { "refreshing=\(self.store.refreshingProviders.contains(provider) ? "1" : "0")", "text=\(displayText ?? "nil")", "layoutCost=\(layoutCostSignature ?? "nil")", + "layoutPace=\(layoutPaceSignature ?? "nil")", ].joined(separator: "|") } + private func storedMenuBarLayoutPaceSignature(for provider: UsageProvider, snapshot: UsageSnapshot?) -> String? { + let resolution = self.settings.menuBarLayoutResolution(for: provider) + guard !resolution.usesLegacyRendering else { return nil } + + let tokens = resolution.layout.lines.joined() + let showsRunsOut = tokens.contains(.runsOut) + let showsPacePercent = tokens.contains(.pacePercent) + guard showsRunsOut || showsPacePercent else { return nil } + + let now = Date() + let windows = self.menuBarLayoutWindows(provider: provider, snapshot: snapshot, now: now) + let paceWindow = windows.weekly ?? windows.automatic + let pace = paceWindow + .flatMap { self.store.weeklyPace(provider: provider, window: $0, now: now) } + let runsOut = pace + .flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel } + let pacePercent = pace + .flatMap { MenuBarDisplayText.paceText(pace: $0) } + + return [ + "runsOut=\(showsRunsOut ? runsOut ?? "nil" : "unused")", + "pacePercent=\(showsPacePercent ? pacePercent ?? "nil" : "unused")", + ].joined(separator: ",") + } + private func storedMenuBarLayoutCostSignature(for provider: UsageProvider) -> String? { let resolution = self.settings.menuBarLayoutResolution(for: provider) guard !resolution.usesLegacyRendering else { return nil } diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift index db210b6069..ab44f56d56 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift @@ -9,8 +9,8 @@ public enum AntigravityProviderDescriptor { metadata: ProviderMetadata( id: .antigravity, displayName: "Antigravity", - sessionLabel: "Gemini Models", - weeklyLabel: "Claude and GPT", + sessionLabel: "Gemini 5-hour", + weeklyLabel: "Gemini weekly", opusLabel: nil, supportsOpus: false, supportsCredits: false, diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index 0a87cd41a0..157046c1fd 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -155,14 +155,13 @@ public struct AntigravityStatusSnapshot: Sendable { } let primary = primaryQuota.map(Self.rateWindow(for:)) - let secondary = secondaryQuota.map(Self.rateWindow(for:)) + let secondary: RateWindow? = nil let extraWindows = Self.extraRateWindows( from: normalized, summaryCandidates: summaryCandidates, compactFallbackModelID: fallbackQuota?.modelId, representedPools: Set([ primaryQuota.map { _ in AntigravityUsagePool.gemini }, - secondaryQuota.map { _ in AntigravityUsagePool.claudeGPT }, ].compactMap(\.self))) let identity = ProviderIdentitySnapshot( @@ -644,7 +643,9 @@ public struct AntigravityStatusSnapshot: Sendable { let distinctWindows = models .filter { - $0.quota.modelId == compactFallbackModelID || Self.shouldShowDistinctExtraWindow($0) + $0.quota.modelId == compactFallbackModelID || Self.shouldShowDistinctExtraWindow( + $0, + representedPools: representedPools) } .sorted(by: Self.modelOrderPrecedes) .map { m in @@ -671,8 +672,13 @@ public struct AntigravityStatusSnapshot: Sendable { "antigravity-compact-fallback-\(modelID)" } - private static func shouldShowDistinctExtraWindow(_ model: AntigravityNormalizedModel) -> Bool { - guard !self.isSummaryCandidate(model) else { return false } + private static func shouldShowDistinctExtraWindow( + _ model: AntigravityNormalizedModel, + representedPools: Set) -> Bool + { + if let pool = usagePool(for: model), representedPools.contains(pool) { + guard !self.isSummaryCandidate(model) else { return false } + } if model.quota.remainingFraction == nil { return model.quota.resetTime != nil || model.quota.resetDescription != nil } diff --git a/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift b/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift index d6976c20bf..31a09309e0 100644 --- a/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift +++ b/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift @@ -333,8 +333,12 @@ struct AntigravityRemoteUsageFetcherTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 20) - #expect(usage.secondary?.remainingPercent.rounded() == 50) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 50) } @Test @@ -441,8 +445,9 @@ struct AntigravityRemoteUsageFetcherTests { #expect(quotaCalls.get() == 1) #expect(usage.primary?.remainingPercent == 60.0) - #expect(usage.secondary?.remainingPercent == 100.0) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + #expect(usage.extraRateWindows == nil) } @Test @@ -659,8 +664,9 @@ struct AntigravityRemoteUsageFetcherTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent == 100.0) - #expect(usage.secondary?.remainingPercent == 100.0) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + #expect(usage.extraRateWindows == nil) } @Test diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift new file mode 100644 index 0000000000..6e7b973902 --- /dev/null +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift @@ -0,0 +1,378 @@ +import CodexBarCore +import Foundation +import Testing + +extension AntigravityStatusProbeTests { + @Test + func `remote source collapses recognized family models and hides unconsumed junk`() throws { + // Fixture B: verified 13 remote models; recognized text models collapse into Gemini, + // and unconsumed junk stays hidden. + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + // junk: image + AntigravityModelQuota( + label: "Gemini 2.5 Flash Image", + modelId: "gemini-2-5-flash-image", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: tab autocomplete + AntigravityModelQuota( + label: "Tab Flash Lite Vertex", + modelId: "tab_flash_lite_vertex", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 2.5 Pro", + modelId: "gemini-2-5-pro", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3 Pro (High)", + modelId: "gemini-3-pro-high", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: lite + AntigravityModelQuota( + label: "Gemini 2.5 Flash Lite", + modelId: "gemini-2-5-flash-lite", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: image + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "gemini-3-flash", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: lite + AntigravityModelQuota( + label: "Gemini 3.1 Flash Lite", + modelId: "gemini-3-1-flash-lite", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3.1 Pro (Low)", + modelId: "gemini-3-1-pro-low", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3.1 Pro (High)", + modelId: "gemini-3-1-pro-high", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: tab autocomplete + AntigravityModelQuota( + label: "Tab Jump Flash Lite Vertex", + modelId: "tab_jump_flash_lite_vertex", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3 Pro (Low)", + modelId: "gemini-3-pro-low", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 2.5 Flash", + modelId: "gemini-2-5-flash", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 100) + #expect(usage.secondary == nil) + #expect(usage.extraRateWindows == nil) + } + + @Test + func `remote source shows consumed junk models despite filter`() throws { + // Fixture C: junk models with remainingFraction < 0.999 must be shown + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + // consumed tab - should be shown + AntigravityModelQuota( + label: "Tab Flash Lite Vertex", + modelId: "tab_flash_lite_vertex", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + // consumed image - should be shown + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + // unconsumed sibling tab (0.9995 >= 0.999) - should be hidden + AntigravityModelQuota( + label: "Tab Jump Flash Lite Vertex", + modelId: "tab_jump_flash_lite_vertex", + remainingFraction: 0.9995, + resetTime: nil, + resetDescription: nil), + // a clean survivor for non-empty guard + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "gemini-3-flash", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + let extraWindows = try #require(usage.extraRateWindows) + let ids = extraWindows.map(\.id) + + // Consumed junk models shown despite being junk type + #expect(ids.contains("tab_flash_lite_vertex")) + #expect(ids.contains("gemini-3-pro-image")) + + // Unconsumed sibling stays hidden + #expect(!ids.contains("tab_jump_flash_lite_vertex")) + } + + @Test + func `remote source image models do not drive family summary bars`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 0.2, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro (High)", + modelId: "gemini-3-pro-high", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash Image", + modelId: "gemini-3-flash-image", + remainingFraction: 0.1, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "gemini-3-flash", + remainingFraction: 0.8, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + + #expect(usage.primary?.usedPercent == 20) + #expect(usage.secondary == nil) + #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-pro-image") == true) + #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-flash-image") == true) + } + + @Test + func `remote source yields nil extra windows when all models are unconsumed junk`() throws { + // Fixture D: all-junk-unconsumed -> extraRateWindows nil + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Tab Flash Lite Vertex", + modelId: "tab_flash_lite_vertex", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 2.5 Flash Lite", + modelId: "gemini-2-5-flash-lite", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Unknown Model X", + modelId: "unknown-model-x", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary == nil) + #expect(usage.secondary == nil) + #expect(usage.tertiary == nil) + #expect(usage.extraRateWindows == nil) + } + + @Test + func `ordering edge cases collapse to most constrained usage pool`() throws { + // Fixture F: local source; known Gemini Pro rows collapse into the Gemini pool + // using the most constrained remaining fraction. + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro (Low)", + modelId: "MODEL_PLACEHOLDER_M70", + remainingFraction: 0.5, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro (High)", + modelId: "MODEL_PLACEHOLDER_M71", + remainingFraction: 0.8, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini Pro Experimental", + modelId: "MODEL_PLACEHOLDER_M72", + remainingFraction: 0.3, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Claude Sonnet 4", + modelId: "MODEL_PLACEHOLDER_M73", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .local) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M73" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 90) + } + + @Test + func `nil version unknown family models sort deterministically by label`() throws { + // Strict-weak-ordering guard: two .unknown models with unparseable versions + // should sort by label without trapping + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Zebra Unknown Model", + modelId: "MODEL_PLACEHOLDER_MA", + remainingFraction: 0.5, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Alpha Unknown Model", + modelId: "MODEL_PLACEHOLDER_MB", + remainingFraction: 0.5, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .local) + + let usage = try snapshot.toUsageSnapshot() + let extraWindows = try #require(usage.extraRateWindows) + let titles = extraWindows.map(\.title) + + // Deterministic: label tiebreaker -> Alpha before Zebra + #expect(titles == ["Alpha Unknown Model", "Zebra Unknown Model"]) + } + + @Test + func `hyphenated raw model ids without display name still map to gemini group`() throws { + // When the remote catalog omits displayName/label, the raw hyphenated model id + // becomes the label and still participates in the Gemini group. + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "gemini-3-pro-preview", + modelId: "gemini-3-pro-preview", + remainingFraction: 1, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "gemini-2.5-pro", + modelId: "gemini-2.5-pro", + remainingFraction: 1, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 100) + #expect(usage.extraRateWindows == nil) + } + + @Test + func `http probe errors still count as reachable`() { + #expect( + AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("HTTP 403: Forbidden"))) + #expect( + AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("HTTP 404: Not Found"))) + #expect( + !AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("Invalid response"))) + #expect(!AntigravityStatusProbe.isReachableProbeError(AntigravityStatusProbeError.notRunning)) + } + + @Test + func `fallback probe port prefers non extension candidate`() { + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [51170, 61775], + extensionPort: 61775) == 51170) + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [61775], + extensionPort: 61775) == 61775) + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [51170, 61775], + extensionPort: nil) == 51170) + } +} diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift index 5182cef097..1f6373ab79 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift @@ -760,8 +760,13 @@ extension AntigravityStatusProbeTests { return } #expect(primary.remainingPercent.rounded() == 20) - #expect(usage.secondary?.remainingPercent.rounded() == 50) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-3-5-sonnet" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 50) + #expect(claudeWindow.title == "Claude 3.5 Sonnet") } @Test @@ -850,7 +855,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary == nil) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let sonnetWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(sonnetWindow.window.remainingPercent.rounded() == 30) } @Test @@ -875,7 +884,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 40) - #expect(usage.secondary?.remainingPercent.rounded() == 70) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let thinkingWindow = try #require(extra.first(where: { $0.id == "claude-thinking" })) + #expect(thinkingWindow.window.remainingPercent.rounded() == 70) } @Test @@ -900,7 +913,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary == nil) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 30) } @Test @@ -1001,7 +1018,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.tertiary == nil) #expect(usage.primary == nil) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 30) } @Test @@ -1032,8 +1053,12 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 40) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M35" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 30) } @Test @@ -1083,9 +1108,10 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 100) - #expect(usage.secondary?.remainingPercent.rounded() == 100) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) #expect(usage.identity?.accountEmail == "user@example.com") + #expect(usage.extraRateWindows == nil) } } @@ -1126,8 +1152,13 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 50) - #expect(usage.secondary?.remainingPercent.rounded() == 25) - #expect(usage.extraRateWindows == nil) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let gptWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M55" })) + #expect(gptWindow.window.remainingPercent.rounded() == 25) + let claudeWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M50" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 75) } @Test @@ -1307,377 +1338,12 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 30) - #expect(usage.secondary?.remainingPercent.rounded() == 70) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `remote source collapses recognized family models and hides unconsumed junk`() throws { - // Fixture B: verified 13 remote models; recognized text models collapse into Gemini, - // and unconsumed junk stays hidden. - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - // junk: image - AntigravityModelQuota( - label: "Gemini 2.5 Flash Image", - modelId: "gemini-2-5-flash-image", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: tab autocomplete - AntigravityModelQuota( - label: "Tab Flash Lite Vertex", - modelId: "tab_flash_lite_vertex", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 2.5 Pro", - modelId: "gemini-2-5-pro", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3 Pro (High)", - modelId: "gemini-3-pro-high", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: lite - AntigravityModelQuota( - label: "Gemini 2.5 Flash Lite", - modelId: "gemini-2-5-flash-lite", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: image - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3 Flash", - modelId: "gemini-3-flash", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: lite - AntigravityModelQuota( - label: "Gemini 3.1 Flash Lite", - modelId: "gemini-3-1-flash-lite", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3.1 Pro (Low)", - modelId: "gemini-3-1-pro-low", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3.1 Pro (High)", - modelId: "gemini-3-1-pro-high", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: tab autocomplete - AntigravityModelQuota( - label: "Tab Jump Flash Lite Vertex", - modelId: "tab_jump_flash_lite_vertex", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3 Pro (Low)", - modelId: "gemini-3-pro-low", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 2.5 Flash", - modelId: "gemini-2-5-flash", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary?.remainingPercent.rounded() == 100) #expect(usage.secondary == nil) - #expect(usage.extraRateWindows == nil) - } - @Test - func `remote source shows consumed junk models despite filter`() throws { - // Fixture C: junk models with remainingFraction < 0.999 must be shown - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - // consumed tab - should be shown - AntigravityModelQuota( - label: "Tab Flash Lite Vertex", - modelId: "tab_flash_lite_vertex", - remainingFraction: 0.4, - resetTime: nil, - resetDescription: nil), - // consumed image - should be shown - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 0.4, - resetTime: nil, - resetDescription: nil), - // unconsumed sibling tab (0.9995 >= 0.999) - should be hidden - AntigravityModelQuota( - label: "Tab Jump Flash Lite Vertex", - modelId: "tab_jump_flash_lite_vertex", - remainingFraction: 0.9995, - resetTime: nil, - resetDescription: nil), - // a clean survivor for non-empty guard - AntigravityModelQuota( - label: "Gemini 3 Flash", - modelId: "gemini-3-flash", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - let extraWindows = try #require(usage.extraRateWindows) - let ids = extraWindows.map(\.id) - - // Consumed junk models shown despite being junk type - #expect(ids.contains("tab_flash_lite_vertex")) - #expect(ids.contains("gemini-3-pro-image")) - - // Unconsumed sibling stays hidden - #expect(!ids.contains("tab_jump_flash_lite_vertex")) - } - - @Test - func `remote source image models do not drive family summary bars`() throws { - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 0.2, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Pro (High)", - modelId: "gemini-3-pro-high", - remainingFraction: 0.9, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Flash Image", - modelId: "gemini-3-flash-image", - remainingFraction: 0.1, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Flash", - modelId: "gemini-3-flash", - remainingFraction: 0.8, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - - #expect(usage.primary?.usedPercent == 20) - #expect(usage.secondary == nil) - #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-pro-image") == true) - #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-flash-image") == true) - } - - @Test - func `remote source yields nil extra windows when all models are unconsumed junk`() throws { - // Fixture D: all-junk-unconsumed -> extraRateWindows nil - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Tab Flash Lite Vertex", - modelId: "tab_flash_lite_vertex", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 2.5 Flash Lite", - modelId: "gemini-2-5-flash-lite", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Unknown Model X", - modelId: "unknown-model-x", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary == nil) - #expect(usage.secondary == nil) - #expect(usage.tertiary == nil) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `ordering edge cases collapse to most constrained usage pool`() throws { - // Fixture F: local source; known Gemini Pro rows collapse into the Gemini pool - // using the most constrained remaining fraction. - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Gemini 3 Pro (Low)", - modelId: "MODEL_PLACEHOLDER_M70", - remainingFraction: 0.5, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Pro (High)", - modelId: "MODEL_PLACEHOLDER_M71", - remainingFraction: 0.8, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini Pro Experimental", - modelId: "MODEL_PLACEHOLDER_M72", - remainingFraction: 0.3, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Claude Sonnet 4", - modelId: "MODEL_PLACEHOLDER_M73", - remainingFraction: 0.9, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary?.remainingPercent.rounded() == 30) - #expect(usage.secondary?.remainingPercent.rounded() == 90) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `nil version unknown family models sort deterministically by label`() throws { - // Strict-weak-ordering guard: two .unknown models with unparseable versions - // should sort by label without trapping - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Zebra Unknown Model", - modelId: "MODEL_PLACEHOLDER_MA", - remainingFraction: 0.5, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Alpha Unknown Model", - modelId: "MODEL_PLACEHOLDER_MB", - remainingFraction: 0.5, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - - let usage = try snapshot.toUsageSnapshot() - let extraWindows = try #require(usage.extraRateWindows) - let titles = extraWindows.map(\.title) - - // Deterministic: label tiebreaker -> Alpha before Zebra - #expect(titles == ["Alpha Unknown Model", "Zebra Unknown Model"]) - } - - @Test - func `hyphenated raw model ids without display name still map to gemini group`() throws { - // When the remote catalog omits displayName/label, the raw hyphenated model id - // becomes the label and still participates in the Gemini group. - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "gemini-3-pro-preview", - modelId: "gemini-3-pro-preview", - remainingFraction: 1, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "gemini-2.5-pro", - modelId: "gemini-2.5-pro", - remainingFraction: 1, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary?.remainingPercent.rounded() == 100) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `http probe errors still count as reachable`() { - #expect( - AntigravityStatusProbe.isReachableProbeError( - AntigravityStatusProbeError.apiError("HTTP 403: Forbidden"))) - #expect( - AntigravityStatusProbe.isReachableProbeError( - AntigravityStatusProbeError.apiError("HTTP 404: Not Found"))) - #expect( - !AntigravityStatusProbe.isReachableProbeError( - AntigravityStatusProbeError.apiError("Invalid response"))) - #expect(!AntigravityStatusProbe.isReachableProbeError(AntigravityStatusProbeError.notRunning)) - } - - @Test - func `fallback probe port prefers non extension candidate`() { - #expect( - AntigravityStatusProbe.fallbackProbePort( - ports: [51170, 61775], - extensionPort: 61775) == 51170) - #expect( - AntigravityStatusProbe.fallbackProbePort( - ports: [61775], - extensionPort: 61775) == 61775) - #expect( - AntigravityStatusProbe.fallbackProbePort( - ports: [51170, 61775], - extensionPort: nil) == 51170) + let extra = try #require(usage.extraRateWindows) + let opusWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M61" })) + #expect(opusWindow.window.remainingPercent.rounded() == 70) + let sonnetWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M60" })) + #expect(sonnetWindow.window.remainingPercent.rounded() == 80) } } diff --git a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift index e5c995eb56..ec93b7e0e0 100644 --- a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift +++ b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift @@ -628,7 +628,7 @@ struct CodexBarWidgetProviderTests { let rows = WidgetUsageRow.rows(for: entry) #expect(rows.map(\.id) == ["primary", "secondary"]) - #expect(rows.map(\.title) == ["Gemini Models", "Claude and GPT"]) + #expect(rows.map(\.title) == ["Gemini 5-hour", "Gemini weekly"]) #expect(rows.compactMap(\.percentLeft) == [90, 80]) } diff --git a/Tests/CodexBarTests/MenuCardAntigravityTests.swift b/Tests/CodexBarTests/MenuCardAntigravityTests.swift index b07ef767c6..9272b869f7 100644 --- a/Tests/CodexBarTests/MenuCardAntigravityTests.swift +++ b/Tests/CodexBarTests/MenuCardAntigravityTests.swift @@ -86,7 +86,7 @@ struct MenuCardAntigravityTests { now: now)) #expect(model.metrics.count == 1) - #expect(model.metrics.map(\.title) == ["Gemini Models"]) + #expect(model.metrics.map(\.title) == ["Gemini 5-hour"]) #expect(model.metrics[0].percent == 95) #expect(model.metrics[0].percentLabel == "95% left") } @@ -188,7 +188,7 @@ struct MenuCardAntigravityTests { hidePersonalInfo: false, now: now)) - #expect(model.metrics.map(\.title) == ["Gemini Models", "Claude and GPT"]) + #expect(model.metrics.map(\.title) == ["Gemini 5-hour", "Claude Thinking"]) #expect(!model.metrics.contains { $0.title == "Gemini 3.1 Pro (Low)" }) } @@ -250,11 +250,13 @@ struct MenuCardAntigravityTests { now: now)) #expect(model.metrics.map(\.title) == [ - "Gemini Models", - "Claude and GPT", + "Gemini 5-hour", + "Claude Opus 4.6 (Thinking)", + "GPT-OSS 120B (Medium)", ]) #expect(model.metrics.map(\.percentLabel) == [ "50% left", + "75% left", "25% left", ]) } @@ -307,7 +309,7 @@ struct MenuCardAntigravityTests { // Distinct extra windows remain visible even with optional extras disabled. #expect(model.metrics.contains { $0.title == "Experimental Tool" }) - #expect(model.metrics.contains { $0.title == "Gemini Models" }) + #expect(model.metrics.contains { $0.title == "Gemini 5-hour" }) } @Test @@ -517,7 +519,7 @@ struct MenuCardAntigravityTests { now: now)) #expect(model.metrics.count == 1) - #expect(model.metrics[0].title == "Gemini Models") + #expect(model.metrics[0].title == "Gemini 5-hour") #expect(model.metrics[0].percent == 5) #expect(model.metrics[0].percentLabel == "5% used") } diff --git a/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift b/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift index a5986037d7..12c3ccdc1b 100644 --- a/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift @@ -98,8 +98,8 @@ struct StatusItemAnimationSignatureTests { store._setSnapshotForTesting( UsageSnapshot( - primary: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - secondary: RateWindow(usedPercent: 16, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), + primary: RateWindow(usedPercent: 1, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), tertiary: nil, extraRateWindows: [ NamedRateWindow( @@ -138,7 +138,7 @@ struct StatusItemAnimationSignatureTests { #expect(signature.contains("provider=antigravity")) #expect(signature.contains("style=combined")) - #expect(signature.contains("primary=98.000")) + #expect(signature.contains("primary=99.000")) #expect(signature.contains("weekly=1.000")) } From 01b89a6fabeff0ecdc91e9ec9bd2d9a5fc192bfa Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:45:43 +0800 Subject: [PATCH 08/10] Fix Antigravity test imports and update expected widget snapshot titles --- .../CodexBarTests/AntigravityStatusProbeTests+Additional.swift | 2 +- Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift index 6e7b973902..2c4e38dcf2 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift @@ -1,6 +1,6 @@ -import CodexBarCore import Foundation import Testing +@testable import CodexBarCore extension AntigravityStatusProbeTests { @Test diff --git a/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift b/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift index 670e68e3ea..b999a26010 100644 --- a/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift +++ b/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift @@ -162,7 +162,7 @@ struct UsageStoreWidgetSnapshotTests { let entry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .antigravity }) #expect(widgetSnapshots.last?.usageBarsShowUsed == true) #expect(entry.usageRows?.map(\.id) == ["primary", "secondary"]) - #expect(entry.usageRows?.map(\.title) == ["Gemini Models", "Claude and GPT"]) + #expect(entry.usageRows?.map(\.title) == ["Gemini 5-hour", "Gemini weekly"]) #expect(entry.usageRows?.compactMap(\.percentLeft) == [90, 80]) } From 94e168cde1dfb4eb60acc95e0616ecc842084ccf Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:45:16 +0800 Subject: [PATCH 09/10] Clean up .pacePercent changes from PR 2352 --- Sources/CodexBar/MenuBarLayout.swift | 5 +- Sources/CodexBar/MenuBarLayoutEditor.swift | 11 +--- Sources/CodexBar/MenuBarLayoutRenderer.swift | 10 --- .../Resources/ar.lproj/Localizable.strings | 3 - .../Resources/ca.lproj/Localizable.strings | 3 - .../Resources/de.lproj/Localizable.strings | 3 - .../Resources/en.lproj/Localizable.strings | 3 - .../Resources/es.lproj/Localizable.strings | 3 - .../Resources/fa.lproj/Localizable.strings | 3 - .../Resources/fr.lproj/Localizable.strings | 3 - .../Resources/gl.lproj/Localizable.strings | 3 - .../Resources/id.lproj/Localizable.strings | 3 - .../Resources/it.lproj/Localizable.strings | 3 - .../Resources/ja.lproj/Localizable.strings | 3 - .../Resources/ko.lproj/Localizable.strings | 3 - .../Resources/nl.lproj/Localizable.strings | 3 - .../Resources/pl.lproj/Localizable.strings | 3 - .../Resources/pt-BR.lproj/Localizable.strings | 3 - .../Resources/ru.lproj/Localizable.strings | 3 - .../Resources/sv.lproj/Localizable.strings | 3 - .../Resources/th.lproj/Localizable.strings | 3 - .../Resources/tr.lproj/Localizable.strings | 3 - .../Resources/uk.lproj/Localizable.strings | 3 - .../Resources/vi.lproj/Localizable.strings | 3 - .../zh-Hans.lproj/Localizable.strings | 3 - .../zh-Hant.lproj/Localizable.strings | 3 - ...StatusItemController+IconObservation.swift | 29 --------- .../StatusItemController+MenuBarLayout.swift | 6 +- .../LocalizationLanguageCatalogTests.swift | 3 - .../MenuBarLayoutRendererTests.swift | 23 ------- Tests/CodexBarTests/MenuBarLayoutTests.swift | 64 ------------------- 31 files changed, 5 insertions(+), 215 deletions(-) diff --git a/Sources/CodexBar/MenuBarLayout.swift b/Sources/CodexBar/MenuBarLayout.swift index f214265ba8..ea9444b292 100644 --- a/Sources/CodexBar/MenuBarLayout.swift +++ b/Sources/CodexBar/MenuBarLayout.swift @@ -16,7 +16,6 @@ enum MenuBarLayoutToken: Codable, Hashable, Sendable { case resetCountdown case resetAbsolute case runsOut - case pacePercent case costToday case cost30d case separatorDot @@ -221,13 +220,13 @@ extension MenuBarLayout { .percent(window: Self.percentWindow(for: metricPreference, provider: provider)), ]]) case .pace: - return MenuBarLayout(lines: [[icon, .pacePercent]]) + return MenuBarLayout(lines: [[icon, .runsOut]]) case .both: return MenuBarLayout(lines: [[ icon, .percent(window: Self.percentWindow(for: metricPreference, provider: provider)), .separatorDot, - .pacePercent, + .runsOut, ]]) case .resetTime: let resetItem = resetTimeDisplayStyle == .absolute diff --git a/Sources/CodexBar/MenuBarLayoutEditor.swift b/Sources/CodexBar/MenuBarLayoutEditor.swift index 78b2c40f53..316cdd1768 100644 --- a/Sources/CodexBar/MenuBarLayoutEditor.swift +++ b/Sources/CodexBar/MenuBarLayoutEditor.swift @@ -251,7 +251,7 @@ struct MenuBarLayoutEditor: View { MenuBarLayoutPaletteGroup( id: "time", title: L("menu_bar_layout_group_time"), - tokens: [.resetCountdown, .resetAbsolute, .runsOut, .pacePercent], + tokens: [.resetCountdown, .resetAbsolute, .runsOut], includesLineBreak: false), MenuBarLayoutPaletteGroup( id: "money", @@ -674,12 +674,9 @@ private struct MenuBarLayoutPreview: View { now: now) } let paceWindow = weekly ?? automatic - let pace = paceWindow + let runsOut = paceWindow .flatMap { self.store.weeklyPace(provider: provider, window: $0, now: now) } - let runsOut = pace .flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel } - let pacePercent = pace - .flatMap { MenuBarDisplayText.paceText(pace: $0) } let cost = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot let costToday = MenuBarLayoutCostResolver.todayCostUSD(snapshot: cost, now: now) return MenuBarLayoutRenderData( @@ -690,7 +687,6 @@ private struct MenuBarLayoutPreview: View { weekly: MenuBarLayoutRenderWindow(weekly), automatic: MenuBarLayoutRenderWindow(automatic), runsOut: runsOut, - pacePercent: pacePercent, costToday: costToday.map { UsageFormatter.currencyString($0, currencyCode: cost?.currencyCode ?? "USD") }, @@ -719,7 +715,6 @@ private struct MenuBarLayoutPreview: View { weekly: MenuBarLayoutRenderWindow(weekly), automatic: MenuBarLayoutRenderWindow(session), runsOut: L("menu_bar_layout_sample_runs_out"), - pacePercent: L("menu_bar_layout_sample_pace_percent"), costToday: "$1.25", cost30d: "$20.00") } @@ -787,7 +782,6 @@ extension MenuBarLayoutToken { case .resetCountdown: L("menu_bar_layout_token_resets_in") case .resetAbsolute: L("menu_bar_layout_token_reset_at") case .runsOut: L("menu_bar_layout_token_runs_out") - case .pacePercent: L("menu_bar_layout_token_pace_percent") case .costToday: L("menu_bar_layout_token_cost_today") case .cost30d: L("menu_bar_layout_token_cost_30d") case .separatorDot: "·" @@ -812,7 +806,6 @@ extension MenuBarLayoutToken { case .resetCountdown: "timer" case .resetAbsolute: "clock" case .runsOut: "hourglass.bottomhalf.filled" - case .pacePercent: "percent" case .costToday: "dollarsign.circle" case .cost30d: "calendar.badge.clock" case .separatorDot: "smallcircle.filled.circle" diff --git a/Sources/CodexBar/MenuBarLayoutRenderer.swift b/Sources/CodexBar/MenuBarLayoutRenderer.swift index 1e2848ef9b..27204013e0 100644 --- a/Sources/CodexBar/MenuBarLayoutRenderer.swift +++ b/Sources/CodexBar/MenuBarLayoutRenderer.swift @@ -29,7 +29,6 @@ struct MenuBarLayoutRenderData: Hashable { let weekly: MenuBarLayoutRenderWindow? let automatic: MenuBarLayoutRenderWindow? let runsOut: String? - let pacePercent: String? let costToday: String? let cost30d: String? } @@ -277,15 +276,6 @@ final class MenuBarLayoutRenderer { data.runsOut, unavailableLabel: L("Run-out estimate unavailable"), attributes: style.attributes) - case .pacePercent: - let rateWindow = data.automatic - let percent = rateWindow.map { options.showUsed ? $0.usedPercent : $0.remainingPercent } - let fallbackValue = percent.map(UsageFormatter.percentString) - return self.optionalTextToken( - data.pacePercent ?? fallbackValue, - unavailableLabel: L("Pace percent unavailable"), - accessibilityPrefix: L("display_mode_pace"), - attributes: style.attributes) case .costToday: return self.optionalTextToken( data.costToday, diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index ac15a001cb..be1b7a75dc 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -1345,6 +1345,3 @@ "Cost today unavailable" = "تكلفة اليوم: غير متوفر"; "30-day cost unavailable" = "تكلفة 30 يوماً: غير متوفر"; "Resets" = "إعادات الضبط"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index ddb56ead34..55985b0af6 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -1344,6 +1344,3 @@ "Cost today unavailable" = "Cost d’avui: No disponible"; "30-day cost unavailable" = "Cost de 30 dies: No disponible"; "Resets" = "Reinicis"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 7db23c6d45..37d2b62165 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -1342,6 +1342,3 @@ "Cost today unavailable" = "Kosten heute: Nicht verfügbar"; "30-day cost unavailable" = "Kosten 30 Tage: Nicht verfügbar"; "Resets" = "Zurücksetzungen"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Tempo %"; -"Pace percent unavailable" = "Tempoprozent nicht verfügbar"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 31407ec9f9..5cc6b2defb 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -1346,6 +1346,3 @@ "Cost today unavailable" = "Cost today unavailable"; "30-day cost unavailable" = "30-day cost unavailable"; "Resets" = "Resets"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 81a3a077ef..90159b2bc2 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -1340,6 +1340,3 @@ "Cost today unavailable" = "Coste de hoy: No disponible"; "30-day cost unavailable" = "Coste de 30 días: No disponible"; "Resets" = "Reinicios"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 6889462d82..789090531a 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -1345,6 +1345,3 @@ "Cost today unavailable" = "هزینهٔ امروز: در دسترس نیست"; "30-day cost unavailable" = "هزینهٔ ۳۰ روز: در دسترس نیست"; "Resets" = "بازنشانی‌ها"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index 2445a921e9..fafb4abed0 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -1341,6 +1341,3 @@ "Cost today unavailable" = "Coût aujourd’hui: Indisponible"; "30-day cost unavailable" = "Coût sur 30 j: Indisponible"; "Resets" = "Réinitialisations"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Rythme %"; -"Pace percent unavailable" = "Pourcentage de rythme indisponible"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index c1c14e6f01..82cc28af19 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -1341,6 +1341,3 @@ "Cost today unavailable" = "Custo de hoxe: Non dispoñible"; "30-day cost unavailable" = "Custo de 30 días: Non dispoñible"; "Resets" = "Reinicios"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 62644989f0..abebb2f911 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -1345,6 +1345,3 @@ "Cost today unavailable" = "Biaya hari ini: Tidak tersedia"; "30-day cost unavailable" = "Biaya 30 hari: Tidak tersedia"; "Resets" = "Reset"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 933a442ab1..402383c328 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1345,6 +1345,3 @@ "Cost today unavailable" = "Costo oggi: Non disponibile"; "30-day cost unavailable" = "Costo 30 gg: Non disponibile"; "Resets" = "Ripristini"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 13889f9f54..51f5987788 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -1342,6 +1342,3 @@ "Cost today unavailable" = "今日のコスト: 利用不可"; "30-day cost unavailable" = "30日間のコスト: 利用不可"; "Resets" = "リセット"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 79413873a9..57d7245382 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -1309,6 +1309,3 @@ "Cost today unavailable" = "오늘 비용: 사용할 수 없음"; "30-day cost unavailable" = "30일 비용: 사용할 수 없음"; "Resets" = "재설정"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 66b1734b31..fbf7cbf4df 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -1341,6 +1341,3 @@ "Cost today unavailable" = "Kosten vandaag: Niet beschikbaar"; "30-day cost unavailable" = "Kosten 30 dagen: Niet beschikbaar"; "Resets" = "Resets"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 1aa13e948f..92351c1788 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -1345,6 +1345,3 @@ "Cost today unavailable" = "Koszt dzisiaj: Niedostępne"; "30-day cost unavailable" = "Koszt 30 dni: Niedostępne"; "Resets" = "Resety"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index e65a571500..9fc56f647d 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -1342,6 +1342,3 @@ "Cost today unavailable" = "Custo hoje: Indisponível"; "30-day cost unavailable" = "Custo em 30 dias: Indisponível"; "Resets" = "Redefinições"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 679189d61e..debe92c848 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -1343,6 +1343,3 @@ "Cost today unavailable" = "Расход сегодня: Недоступно"; "30-day cost unavailable" = "Расход за 30 дней: Недоступно"; "Resets" = "Сбросы"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index ca17e83221..18e1d3c828 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -1340,6 +1340,3 @@ "Cost today unavailable" = "Kostnad idag: Inte tillgänglig"; "30-day cost unavailable" = "Kostnad 30 dagar: Inte tillgänglig"; "Resets" = "Återställningar"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index a1ef4b7860..686d42e6ef 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -1345,6 +1345,3 @@ "Cost today unavailable" = "ค่าใช้จ่ายวันนี้: ไม่พร้อมใช้งาน"; "30-day cost unavailable" = "ค่าใช้จ่าย 30 วัน: ไม่พร้อมใช้งาน"; "Resets" = "การรีเซ็ต"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index f887c24915..b8eb70a926 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -1343,6 +1343,3 @@ "Cost today unavailable" = "Bugünkü maliyet: Kullanılamıyor"; "30-day cost unavailable" = "30 günlük maliyet: Kullanılamıyor"; "Resets" = "Sıfırlamalar"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 7ce6c00027..359780b371 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -1341,6 +1341,3 @@ "Cost today unavailable" = "Вартість сьогодні: Недоступний"; "30-day cost unavailable" = "Вартість за 30 днів: Недоступний"; "Resets" = "Скидання"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 0091287875..14dbaabac6 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -1342,6 +1342,3 @@ "Cost today unavailable" = "Chi phí hôm nay: Không có sẵn"; "30-day cost unavailable" = "Chi phí 30 ngày: Không có sẵn"; "Resets" = "Lần đặt lại"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "Pace %"; -"Pace percent unavailable" = "Pace percent unavailable"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 11bad1112b..ae94fdb4b8 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1317,6 +1317,3 @@ "Cost today unavailable" = "今日费用: 不可用"; "30-day cost unavailable" = "30 天费用: 不可用"; "Resets" = "重置"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "进度 %"; -"Pace percent unavailable" = "进度百分比不可用"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index d4fd23ef45..c5a328b8c9 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -1372,6 +1372,3 @@ "Cost today unavailable" = "今日費用: 無法使用"; "30-day cost unavailable" = "30 天費用: 無法使用"; "Resets" = "重設"; -"menu_bar_layout_sample_pace_percent" = "+15%"; -"menu_bar_layout_token_pace_percent" = "進度 %"; -"Pace percent unavailable" = "進度百分比不可用"; diff --git a/Sources/CodexBar/StatusItemController+IconObservation.swift b/Sources/CodexBar/StatusItemController+IconObservation.swift index e8ed109cc7..20dd321b9f 100644 --- a/Sources/CodexBar/StatusItemController+IconObservation.swift +++ b/Sources/CodexBar/StatusItemController+IconObservation.swift @@ -47,9 +47,6 @@ extension StatusItemController { let layoutCostSignature = showBrandPercent ? self.storedMenuBarLayoutCostSignature(for: provider) : nil - let layoutPaceSignature = showBrandPercent - ? self.storedMenuBarLayoutPaceSignature(for: provider, snapshot: snapshot) - : nil return [ provider.rawValue, @@ -63,35 +60,9 @@ extension StatusItemController { "refreshing=\(self.store.refreshingProviders.contains(provider) ? "1" : "0")", "text=\(displayText ?? "nil")", "layoutCost=\(layoutCostSignature ?? "nil")", - "layoutPace=\(layoutPaceSignature ?? "nil")", ].joined(separator: "|") } - private func storedMenuBarLayoutPaceSignature(for provider: UsageProvider, snapshot: UsageSnapshot?) -> String? { - let resolution = self.settings.menuBarLayoutResolution(for: provider) - guard !resolution.usesLegacyRendering else { return nil } - - let tokens = resolution.layout.lines.joined() - let showsRunsOut = tokens.contains(.runsOut) - let showsPacePercent = tokens.contains(.pacePercent) - guard showsRunsOut || showsPacePercent else { return nil } - - let now = Date() - let windows = self.menuBarLayoutWindows(provider: provider, snapshot: snapshot, now: now) - let paceWindow = windows.weekly ?? windows.automatic - let pace = paceWindow - .flatMap { self.store.weeklyPace(provider: provider, window: $0, now: now) } - let runsOut = pace - .flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel } - let pacePercent = pace - .flatMap { MenuBarDisplayText.paceText(pace: $0) } - - return [ - "runsOut=\(showsRunsOut ? runsOut ?? "nil" : "unused")", - "pacePercent=\(showsPacePercent ? pacePercent ?? "nil" : "unused")", - ].joined(separator: ",") - } - private func storedMenuBarLayoutCostSignature(for provider: UsageProvider) -> String? { let resolution = self.settings.menuBarLayoutResolution(for: provider) guard !resolution.usesLegacyRendering else { return nil } diff --git a/Sources/CodexBar/StatusItemController+MenuBarLayout.swift b/Sources/CodexBar/StatusItemController+MenuBarLayout.swift index a9807ab85a..133dac8a1c 100644 --- a/Sources/CodexBar/StatusItemController+MenuBarLayout.swift +++ b/Sources/CodexBar/StatusItemController+MenuBarLayout.swift @@ -57,12 +57,9 @@ extension StatusItemController { { let windows = self.menuBarLayoutWindows(provider: provider, snapshot: snapshot, now: now) let paceWindow = windows.weekly ?? windows.automatic - let pace = paceWindow + let runsOut = paceWindow .flatMap { self.store.weeklyPace(provider: provider, window: $0, now: now) } - let runsOut = pace .flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel } - let pacePercent = pace - .flatMap { MenuBarDisplayText.paceText(pace: $0) } let costStrings = self.menuBarLayoutCostStrings(provider: provider, now: now) let providerName = L(self.store.metadata(for: provider).displayName) let rawAccountLabel = snapshot?.accountEmail(for: provider)? @@ -79,7 +76,6 @@ extension StatusItemController { weekly: MenuBarLayoutRenderWindow(windows.weekly), automatic: MenuBarLayoutRenderWindow(windows.automatic), runsOut: runsOut, - pacePercent: pacePercent, costToday: costStrings.today, cost30d: costStrings.last30Days) } diff --git a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift index ccd7c7f307..3d423cede9 100644 --- a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift +++ b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift @@ -515,7 +515,6 @@ struct LocalizationLanguageCatalogTests { "Google OAuth", "No", "Oasis-Token", - "Pace percent unavailable", "Password", "Provider", "Token", @@ -537,9 +536,7 @@ struct LocalizationLanguageCatalogTests { "link_email", "link_github", "menu_bar_layout_sample_account", - "menu_bar_layout_sample_pace_percent", "menu_bar_layout_token_account", - "menu_bar_layout_token_pace_percent", "ory_session_…=…; csrftoken=…", "section_privacy", "tab_menu", diff --git a/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift b/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift index 365047a5bf..1bd43ddd00 100644 --- a/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift +++ b/Tests/CodexBarTests/MenuBarLayoutRendererTests.swift @@ -89,7 +89,6 @@ struct MenuBarLayoutRendererTests { weekly: nil, automatic: nil, runsOut: nil, - pacePercent: nil, costToday: nil, cost30d: nil) let layout = MenuBarLayout(lines: [[ @@ -206,7 +205,6 @@ struct MenuBarLayoutRendererTests { weekly: nil, automatic: textOnlyWindow, runsOut: nil, - pacePercent: nil, costToday: nil, cost30d: nil) @@ -264,31 +262,10 @@ struct MenuBarLayoutRendererTests { resetsAt: self.now.addingTimeInterval(2 * 60 * 60), resetDescription: nil)), runsOut: "Runs out tomorrow", - pacePercent: "+15%", costToday: "$1.25", cost30d: "$20.00") } - @Test - func `renders pace percent`() { - let renderer = MenuBarLayoutRenderer() - let data = MenuBarLayoutRenderData( - iconKey: "codex", - providerName: "Codex", - accountLabel: nil, - session: nil, - weekly: nil, - automatic: nil, - runsOut: nil, - pacePercent: "+15%", - costToday: nil, - cost30d: nil) - let layout = MenuBarLayout(lines: [[.pacePercent]]) - let output = renderer.render(layout: layout, data: data, icon: nil, options: self.options()) - #expect(output.attributedTitle.string == "+15%") - #expect(output.accessibilityLabel.contains("Pace")) - } - private func options() -> MenuBarLayoutRenderOptions { MenuBarLayoutRenderOptions( size: .regular, diff --git a/Tests/CodexBarTests/MenuBarLayoutTests.swift b/Tests/CodexBarTests/MenuBarLayoutTests.swift index ec2d2557bc..8a93e6c7dc 100644 --- a/Tests/CodexBarTests/MenuBarLayoutTests.swift +++ b/Tests/CodexBarTests/MenuBarLayoutTests.swift @@ -188,70 +188,6 @@ struct MenuBarLayoutTests { provider: .kimi) == MenuBarLayout(lines: [[.icon, .percent(window: .session)]])) } - @Test - @MainActor - func `reproduce pace percentage missing`() { - // Mock a pace where the user is 15% ahead of target usage - let pace = UsagePace( - stage: .ahead, - deltaPercent: 15, - expectedUsedPercent: 50, - actualUsedPercent: 65, - etaSeconds: nil, - willLastToReset: true) - - // 1. Calculate legacy pace rendering output - let legacyPaceString = MenuBarDisplayText.displayText( - mode: .pace, - percentWindow: nil, - pace: pace, - showUsed: true) ?? "nil" - - // 2. Calculate new migrated rendering output (which maps legacy `.pace` to `.pacePercent` layout token) - let resolution = MenuBarLayoutResolution.legacy( - iconStyle: .iconAndPercent, - displayMode: .pace, - metricPreference: .primary, - resetTimeDisplayStyle: .countdown) - let migratedTokens = Array(resolution.layout.lines.joined()) - #expect(migratedTokens.contains(.pacePercent)) - - let renderer = MenuBarLayoutRenderer() - let data = MenuBarLayoutRenderData( - iconKey: "codex", - providerName: "Codex", - accountLabel: "user@example.com", - session: nil, - weekly: nil, - automatic: nil, - runsOut: nil, - pacePercent: legacyPaceString, - costToday: nil, - cost30d: nil) - let output = renderer.render( - layout: MenuBarLayout(lines: [[.pacePercent]]), - data: data, - icon: nil, - options: MenuBarLayoutRenderOptions( - size: .regular, - highContrast: false, - showUsed: true, - appearanceName: "aqua", - isDebugApp: false, - now: Date())) - let newPaceString = output.attributedTitle.string - - // 3. Print the comparison to verify the reproduction clearly in test output logs - print("\n==================================================") - print("REPRODUCTION - PACE METRIC DISCREPANCY COMPARISON") - print("Legacy rendering output (using .pace displayMode): \(legacyPaceString)") - print("New layout rendering output (migrated to .pacePercent token): \(newPaceString)") - print("==================================================\n") - - #expect(legacyPaceString == "+15%") - #expect(newPaceString == "+15%") - } - @Test @MainActor func `global editing seeds the representative provider legacy layout`() throws { From aae6f716f47aaee8198652898234e068531d5266 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:00:16 +0800 Subject: [PATCH 10/10] test: stabilize ZenMuxProviderTests locale formatting --- Tests/CodexBarTests/ZenMuxProviderTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/ZenMuxProviderTests.swift b/Tests/CodexBarTests/ZenMuxProviderTests.swift index f7ca2692e8..703fea60c7 100644 --- a/Tests/CodexBarTests/ZenMuxProviderTests.swift +++ b/Tests/CodexBarTests/ZenMuxProviderTests.swift @@ -243,7 +243,7 @@ struct ZenMuxProviderTests { let now = try #require(Self.date("2026-03-24T07:35:09.000Z")) let expiresAt = try #require(Self.date("2026-04-12T08:26:56.000Z")) let expiryFormatter = DateFormatter() - expiryFormatter.locale = .current + expiryFormatter.locale = codexBarLocalizedLocale() expiryFormatter.timeZone = .current expiryFormatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy") let transport = ProviderHTTPTransportStub { request in