Files
steipete--codexbar/Sources/CodexBar/MenuCardView+SessionEquivalent.swift
sf-jin-ku d2ef26296a fix(codex): show Business monthly credit on stacked cards (#3112)
* fix(codex): show Business monthly credit on stacked cards

Stacked account fetches skipped the spend-controls credits path, so
Enterprise/Business cards showed Limits not available instead of used vs cap.

* docs: credit #3112 changelog entry

* fix(codex): keep cached monthly limits after spend-controls failures

Transient enrichment errors were persisted as missing credits, so stacked Business cards fell back to “Limits not available.” Restore the unreleased changelog heading as well.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): clear published credits when the monthly cap is gone

Successful spend-controls absence skipped publication and left the old cap on the selected account. Retarget gatekeeper fingerprints after that cluster split.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): keep monthly-only usage out of reset-credit rescue

A transient spend-controls failure still produced a success with no windows, and OAuth reset-credit rescue converted that into noRateLimitsFound before the cached monthly cap could be preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): treat unmappable monthly limits as enrichment failures

A nested spend-controls limit that is present but not numeric is no longer treated as confirmed absence, so a cached Business cap is preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): honor disabled caps and unmappable monthly usage

Inactive enforcement still confirms absence even if limit JSON is malformed, and a non-numeric current_month_usage is treated as enrichment failure instead of zero used.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): let an absent monthly cap win over malformed usage

Malformed current_month_usage only counts as enrichment failure when an active numeric limit actually needs that used amount.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): hide monthly credit when optional credits are off

Override-derived monthly lanes now follow Show credits + extra usage, so stacked Business cards no longer keep a credit meter the user asked to hide.

* fix(codex): include monthly credits in compact account planning

Inactive compact rows now see per-account monthly caps so an exhausted Business limit cannot hide behind a window-less usage snapshot.

* fix(codex): preserve only the monthly cap after enrichment failure

When spend-controls fails and usage confirmed there is no generic credit balance, keep the cached monthly limit without republishing stale remaining/events.

* fix(codex): fail monthly enrichment when account id or enforcement is unknown

A gated spend-controls fetch without an account id, or a non-string enforcement_mode, is now inconclusive so a cached Business cap is not cleared or published as active.

* fix(codex): hide menu-bar monthly credit when optional credits are off

Menu-bar projections now omit the optional monthly cap when the credits toggle is off, and a mappable nonpositive limit confirms cap removal even if enforcement is unreadable.

* fix(codex): reject non-finite monthly credit values

NaN and Infinity usage or limit strings are treated as mapping failures so they cannot zero out cached usage or persist invalid credit snapshots.

* fix(codex): keep monthly cap through standalone credits refresh

When spend-controls enrichment fails, the follow-up credits refresh now merges the prior monthly limit instead of publishing a generic-only snapshot.

* fix(codex): clear generic credits when monthly enrichment fails

A standalone credits refresh that confirms the generic balance is gone now publishes nil instead of throwing, so stale credits are not restored while a monthly cap is still preserved when one exists.

* fix(codex): retarget snapshot-store architecture suppressions

The monthly-cap helper shifted the Codex identity anchors, so the provider-architecture allowlist now points at the new lines.

* fix(codex): fetch monthly credits when plan type is unknown

Spend-control accounts with a missing or unmapped plan type now query the monthly endpoint, so a cached Business cap is not treated as confirmed removal.

* fix(codex): hydrate cached monthly credits and clear stale generic balance

Offline startup left the menu-bar projection without the disk-cached cap, and enrichment failure kept a generic balance when the monthly cap was already absent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): persist standalone credits onto account snapshots

Usage refresh stored the selected account snapshot before the credits pass, so a later cap add or removal never reached disk and offline hydration replayed the stale value.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): persist credits using workspace identity

Same-email workspace accounts made the snapshot write a no-op; match the published cap with the scoped refresh identity so only the selected workspace row is stored.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger CI

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-21 16:18:15 -07:00

86 lines
3.6 KiB
Swift

import CodexBarCore
extension UsageMenuCardView.Model {
static func sessionEquivalentDetail(
input: Input,
weeklyWindow: RateWindow,
weeklyWindowID: String?) -> UsagePaceText.SessionEquivalentDetail?
{
guard let forecast = input.sessionEquivalentForecast,
forecast.applies(to: weeklyWindow, windowID: weeklyWindowID)
else {
return nil
}
return UsagePaceText.sessionEquivalentDetail(forecast: forecast)
}
static func codexRateMetrics(
input: Input,
projection: CodexConsumerProjection,
percentStyle: PercentStyle) -> [Metric]
{
projection.displayedRateLanes(
showOptionalCreditsAndExtraUsage: input.showOptionalCreditsAndExtraUsage).compactMap { lane in
guard let window = projection.rateWindow(for: lane) else { return nil }
let title = CodexConsumerProjection.rateTitle(
lane: lane,
windowMinutes: window.windowMinutes,
sessionLabel: input.metadata.sessionLabel,
weeklyLabel: input.metadata.weeklyLabel)
let id: String
let paceDetail: PaceDetail?
switch lane {
case .session:
id = "primary"
// UsagePaceText.sessionPace suppresses weekly/monthly durations centrally;
// unknown durations in the session lane keep their existing pace.
paceDetail = Self.sessionPaceDetail(
provider: input.provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case .weekly:
id = "secondary"
paceDetail = Self.weeklyPaceDetail(
provider: input.provider,
window: window,
now: input.now,
pace: Self.standardWeeklyPace(input: input, window: window),
showUsed: input.usageBarsShowUsed)
case .monthly:
id = "monthly"
paceDetail = nil
}
let workdayMarkerPercents: [Double] = if lane == .weekly, input.workdayTickAppearance != .hidden {
workDayMarkerPercents(
workDays: input.workDaysPerWeek,
windowMinutes: window.windowMinutes)
} else {
[]
}
return Metric(
id: id,
title: title,
percent: Self.clamped(input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent),
percentStyle: percentStyle,
resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now),
detailText: nil,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
pacePercent: paceDetail?.pacePercent,
detailIsPaceDerived: paceDetail?.isPaceDerived ?? false,
paceOnTop: paceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
thresholds: lane.quotaWarningWindow.flatMap { input.quotaWarningThresholds[$0] },
showUsed: input.usageBarsShowUsed),
workdayMarkerPercents: workdayMarkerPercents,
workdayTickAppearance: input.workdayTickAppearance,
sessionEquivalentDetail: lane == .weekly
? Self.sessionEquivalentDetail(input: input, weeklyWindow: window, weeklyWindowID: nil)
: nil)
}
}
}