Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
- Memory: release idle OpenAI WebViews under system pressure without blocking the main thread. Thanks @ProspectOre!
- Memory: trim rebuildable menu and OpenAI debug caches under system pressure. Thanks @ProspectOre!
- Provider plans: keep Claude and Kiro plan matching on one rendered line to avoid bogus labels from adjacent usage hints. Thanks @elijahfriedman!
- Antigravity: use current Gemini 5-hour and weekly quota-summary lanes for the compact menu bar icon. Thanks @Zihao-Qi!
- Usage bars: render values rounded to 0% or 100% as fully empty or full. Thanks @Zihao-Qi!
- Codex web: keep cookie-import deadlines responsive when browser cookie work blocks the shared worker pool.
- Codex pace: extrapolate historically exhausted weeks for run-out forecasts and avoid contradictory reset headlines. Thanks @Yuxin-Qiao!
- Localization: correct the German in-progress refresh label. Thanks @ChrisLauinger77!
Expand Down
75 changes: 63 additions & 12 deletions Sources/CodexBar/IconRemainingResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import CodexBarCore

enum IconRemainingResolver {
private static let visibleZeroPercent = 0.0001
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static let antigravityGeminiQuotaBucketIDPrefix = "gemini-"
// Antigravity quota summaries currently 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) -> CodexConsumerProjection {
CodexConsumerProjection.make(
Expand All @@ -23,13 +28,57 @@ enum IconRemainingResolver {
return projection.visibleRateLanes.compactMap { projection.rateWindow(for: $0) }
}

private static func antigravityVisibleWindows(snapshot: UsageSnapshot) -> [RateWindow] {
var windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
let compactFallbacks = snapshot.extraRateWindows?
.filter { $0.usageKnown && $0.id.hasPrefix("antigravity-compact-fallback-") }
.map(\.window) ?? []
windows.append(contentsOf: compactFallbacks)
return windows
private static func antigravityQuotaSummaryWindows(
snapshot: UsageSnapshot)
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let quotaSummaryWindows = snapshot.extraRateWindows?
.filter {
$0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix)
} ?? []
guard !quotaSummaryWindows.isEmpty else { return nil }

let geminiWindows = quotaSummaryWindows.filter(Self.isAntigravityGeminiQuotaSummaryWindow)
// The Antigravity menu-bar icon represents Gemini quotas. If any Gemini cadence is present,
// keep missing Gemini lanes empty instead of silently borrowing Claude + GPT quota.
if !geminiWindows.isEmpty {
return self.antigravityQuotaSummaryPair(in: geminiWindows.filter(\.usageKnown))
?? (primary: nil, secondary: nil)
}
return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown))
}

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)
}

private static func isAntigravityGeminiQuotaSummaryWindow(_ window: NamedRateWindow) -> Bool {
self.antigravityQuotaSummaryBucketID(for: window)?.hasPrefix(self.antigravityGeminiQuotaBucketIDPrefix) == true
}

private static func antigravityQuotaSummaryBucketID(for window: NamedRateWindow) -> String? {
guard window.id.hasPrefix(self.antigravityQuotaSummaryWindowIDPrefix) else { return nil }
return String(window.id.dropFirst(self.antigravityQuotaSummaryWindowIDPrefix.count))
}

/// 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(
Expand All @@ -45,10 +94,9 @@ enum IconRemainingResolver {
secondary: windows.dropFirst().first)
}
if style == .antigravity {
let windows = self.antigravityVisibleWindows(snapshot: snapshot)
return (
primary: windows.first,
secondary: windows.dropFirst().first)
// Only current quota-summary buckets define the fixed session/weekly icon lanes.
return self.antigravityQuotaSummaryWindows(snapshot: snapshot)
?? (primary: nil, secondary: nil)
Comment thread
steipete marked this conversation as resolved.
}
if style == .codex {
let windows = self.codexVisibleWindows(snapshot: snapshot)
Expand Down Expand Up @@ -88,6 +136,7 @@ enum IconRemainingResolver {
snapshot: UsageSnapshot,
style: IconStyle,
showUsed: Bool,
renderingStyle: IconStyle? = nil,
secondaryOverrideWindowID: String? = nil)
-> (primary: Double?, secondary: Double?)
{
Expand All @@ -98,7 +147,9 @@ enum IconRemainingResolver {
var percents = (
primary: showUsed ? windows.primary?.usedPercent : windows.primary?.remainingPercent,
secondary: showUsed ? windows.secondary?.usedPercent : windows.secondary?.remainingPercent)
if showUsed, style == .warp, let secondary = windows.secondary {
// Provider style chooses the usage lanes; rendering style controls renderer-specific layout sentinels.
// Merged icons still resolve Warp's lanes, but render as `.combined` and must keep the real percentage.
if showUsed, style == .warp, (renderingStyle ?? style) == .warp, let secondary = windows.secondary {
if secondary.remainingPercent <= 0 {
// Preserve Warp's exhausted/no-bonus layout even though used percent is 100.
percents.secondary = 0
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ extension StatusItemController {
let showUsed = self.settings.usageBarsShowUsed
let showBrandPercent = self.settings.menuBarShowsBrandIconWithPercent
let primaryProvider = self.primaryProviderForUnifiedIcon()
let resolverStyle = self.store.style(for: primaryProvider)
let snapshot = self.store.snapshot(for: primaryProvider)
let warningFlash = self.quotaWarningFlashActive(provider: primaryProvider)

Expand All @@ -252,8 +253,9 @@ extension StatusItemController {
let resolved = snapshot.map {
IconRemainingResolver.resolvedPercents(
snapshot: $0,
style: style,
style: resolverStyle,
Comment thread
steipete marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep highest-usage mode on rendered lanes

In merged mode with menuBarShowsHighestUsage, primaryProviderForUnifiedIcon() still chooses Antigravity via providerWithHighestUsage()/MenuBarMetricWindowResolver, which can rank the provider by a high Claude+GPT quota-summary row. Passing the provider style here then renders only Antigravity's Gemini session/weekly lanes, so a provider can be selected because Claude+GPT is near its limit while the merged icon shows low Gemini usage instead of the quota that triggered the switch; please align the ranking with these rendered Gemini lanes or render the selected high-usage lane in this mode.

Useful? React with 👍 / 👎.

showUsed: showUsed,
renderingStyle: style,
secondaryOverrideWindowID: self.settings.copilotIconSecondaryWindowOverrideID(snapshot: $0))
}
var primary = resolved?.primary
Expand Down
20 changes: 17 additions & 3 deletions Sources/CodexBar/UsageProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ struct UsageProgressBar: View {
// which caused the status item icon to disappear (issue #805).
Canvas { context, size in
let scale = max(self.displayScale, 1)
let fillWidth = size.width * self.clamped / 100
let fillPercent = Self.renderedFillPercent(self.clamped)
let fillWidth = size.width * fillPercent / 100
let paceWidth = size.width * Self.clampedPercent(self.pacePercent) / 100
let tipWidth = max(25, size.height * 6.5)
let stripeInset = 1 / scale
Expand Down Expand Up @@ -118,7 +119,16 @@ struct UsageProgressBar: View {
}
.frame(height: 6)
.accessibilityLabel(self.accessibilityLabel)
.accessibilityValue("\(Int(self.clamped)) percent")
.accessibilityValue("\(Self.displayPercent(self.clamped)) percent")
}

/// Aligns edge rendering with the rounded percent label: sub-0.5% is empty and 99.5%+ is full.
nonisolated static func renderedFillPercent(_ percent: Double) -> Double {
let clamped = Self.clampedPercent(percent)
let displayPercent = Self.displayPercent(clamped)
if displayPercent <= 0 { return 0 }
if displayPercent >= 100 { return 100 }
return clamped
}

private static func paceStripePaths(size: CGSize, scale: CGFloat) -> (punched: Path, center: Path) {
Expand Down Expand Up @@ -188,7 +198,11 @@ struct UsageProgressBar: View {
isHighlighted ? .white.opacity(0.72) : .primary.opacity(0.32)
}

private static func clampedPercent(_ value: Double?) -> Double {
private nonisolated static func displayPercent(_ percent: Double) -> Int {
Int(self.clampedPercent(percent).rounded())
}

private nonisolated static func clampedPercent(_ value: Double?) -> Double {
guard let value else { return 0 }
return min(100, max(0, value))
}
Expand Down
Loading