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
4 changes: 1 addition & 3 deletions Sources/CodexBar/MenuBarLayoutEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -659,9 +659,7 @@ private struct MenuBarLayoutPreview: View {
{
session = projection.menuBarSelectableRateWindow(for: .session)
weekly = projection.menuBarSelectableRateWindow(for: .weekly)
automatic = projection.visibleRateLanes.lazy
.compactMap { projection.menuBarSelectableRateWindow(for: $0) }
.first
automatic = projection.automaticMenuBarWindow()
} else {
let semanticWindows = MenuBarLayoutSemanticWindowResolver.windows(
provider: provider,
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBar/MenuCardQuotaWarningMarkers.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import CodexBarCore

extension CodexConsumerProjection.RateLane {
var quotaWarningWindow: QuotaWarningWindow {
var quotaWarningWindow: QuotaWarningWindow? {
switch self {
case .session:
.session
case .weekly:
.weekly
case .monthly:
nil
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions Sources/CodexBar/MenuCardView+SessionEquivalent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,34 @@ extension UsageMenuCardView.Model {
projection.visibleRateLanes.compactMap { lane in
guard let window = projection.rateWindow(for: lane) else { return nil }

let title: String
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:
title = L(input.metadata.sessionLabel)
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:
title = L(input.metadata.weeklyLabel)
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
}

return Metric(
Expand All @@ -57,7 +64,7 @@ extension UsageMenuCardView.Model {
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow],
thresholds: lane.quotaWarningWindow.flatMap { input.quotaWarningThresholds[$0] },
showUsed: input.usageBarsShowUsed),
workdayMarkerPercents: lane == .weekly
? workDayMarkerPercents(
Expand Down
16 changes: 14 additions & 2 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,13 @@ struct MenuDescriptor {
if provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
}
let primaryLabel = if provider == .grok {
let primaryLabel = if provider == .codex {
CodexConsumerProjection.rateTitle(
lane: .session,
windowMinutes: snapshot.primary?.windowMinutes,
sessionLabel: metadata.sessionLabel,
weeklyLabel: metadata.weeklyLabel)
Comment on lines +711 to +716

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 Suppress session pace for monthly Codex submenu windows

When the Codex primary is a 43,200-minute window, this now labels it “Monthly,” but usageSection still passes every raw primary to UsagePaceText.sessionSummary. That helper accepts Codex windows of any duration, so the provider submenu can render a session-style “Projected empty…” pace beneath the monthly quota, unlike the newly classified menu card where monthly pace is explicitly suppressed. Route the submenu through the classified lane or only add the session summary for the canonical 300-minute window.

Useful? React with 👍 / 👎.

} else if provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else if provider == .crof {
CrofProviderDescriptor.primaryLabel(snapshot: snapshot)
Expand All @@ -723,7 +729,13 @@ struct MenuDescriptor {
} else {
metadata.sessionLabel
}
let secondaryLabel = if provider == .amp {
let secondaryLabel = if provider == .codex {
CodexConsumerProjection.rateTitle(
lane: .weekly,
windowMinutes: snapshot.secondary?.windowMinutes,
sessionLabel: metadata.sessionLabel,
weeklyLabel: metadata.weeklyLabel)
} else if provider == .amp {
AmpProviderDescriptor.secondaryLabel(details: snapshot.ampUsage) ?? metadata.weeklyLabel
} else if provider == .alibabatokenplan {
AlibabaTokenPlanProviderDescriptor.secondaryLabel(window: snapshot.secondary) ?? metadata.weeklyLabel
Expand Down
30 changes: 23 additions & 7 deletions Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,18 +193,19 @@ struct PlanUtilizationHistoryChartMenuView: View {
for history in histories {
guard !history.entries.isEmpty else { continue }
guard history.windowMinutes > 0 else { continue }
guard allowedNames?.contains(history.name) ?? true else { continue }
let effectiveName = Self.effectiveSeriesName(provider: provider, history: history)
guard allowedNames?.contains(effectiveName) ?? true else { continue }

let canonicalWindowMinutes = history.name.canonicalWindowMinutes(history.windowMinutes)
let selection = SeriesSelection(name: history.name, windowMinutes: canonicalWindowMinutes)
let canonicalWindowMinutes = effectiveName.canonicalWindowMinutes(history.windowMinutes)
let selection = SeriesSelection(name: effectiveName, windowMinutes: canonicalWindowMinutes)
if let existingHistory = historiesBySelection[selection] {
historiesBySelection[selection] = PlanUtilizationSeriesHistory(
name: history.name,
name: effectiveName,
windowMinutes: canonicalWindowMinutes,
entries: Self.mergedEntries(existingHistory.entries + history.entries))
} else {
historiesBySelection[selection] = PlanUtilizationSeriesHistory(
name: history.name,
name: effectiveName,
windowMinutes: canonicalWindowMinutes,
entries: history.entries)
}
Expand All @@ -230,6 +231,22 @@ struct PlanUtilizationHistoryChartMenuView: View {
}
}

/// Histories recorded before duration-based classification stored a 43,200-minute Codex window
/// under its payload slot (session for primary, weekly for secondary). Fold those into the
/// monthly series so the chart does not split or hide the window's history.
private nonisolated static func effectiveSeriesName(
provider: UsageProvider,
history: PlanUtilizationSeriesHistory) -> PlanUtilizationSeriesName
{
if provider == .codex,
history.windowMinutes == CodexConsumerProjection.monthlyWindowMinutes,
history.name == .session || history.name == .weekly
{
return .monthly
}
return history.name
}

nonisolated static func mergedEntries(
_ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry]
{
Expand All @@ -248,8 +265,7 @@ struct PlanUtilizationHistoryChartMenuView: View {
var names: Set<PlanUtilizationSeriesName> = []
switch provider {
case .codex:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
names = CodexConsumerProjection.planUtilizationSeriesNames(snapshot: snapshot)
case .claude:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
Expand Down
66 changes: 61 additions & 5 deletions Sources/CodexBar/Providers/Codex/CodexConsumerProjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,13 @@ struct CodexConsumerProjection {
enum RateLane: String {
case session
case weekly
case monthly
}

static let sessionWindowMinutes = 5 * 60
static let weeklyWindowMinutes = 7 * 24 * 60
static let monthlyWindowMinutes = 30 * 24 * 60

enum SupplementalMetric: String {
case codeReview
}
Expand Down Expand Up @@ -311,7 +316,7 @@ struct CodexConsumerProjection {
session: window,
weekly: self.rateWindowsByLane[.weekly],
evaluationTime: self.evaluationTime)
case .weekly:
case .weekly, .monthly:
return window
}
}
Expand All @@ -335,6 +340,47 @@ struct CodexConsumerProjection {
return nil
}

/// Automatic keeps the standard session window unless a longer window (e.g. a 30-day
/// primary) would hide a genuine weekly quota from the menu bar.
func automaticMenuBarWindow() -> RateWindow? {
let windows = self.visibleRateLanes.compactMap {
self.menuBarSelectableRateWindow(for: $0)
}
guard let weekly = self.menuBarSelectableRateWindow(for: .weekly),
windows.contains(where: {
$0.windowMinutes.map { $0 > Self.weeklyWindowMinutes } ?? false
})
Comment on lines +349 to +352

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 Prefer the exhausted monthly lane before weekly

When a Codex snapshot has a 30-day primary and a weekly secondary, this guard makes .automatic return the weekly window whenever any longer window exists. If the monthly quota is exhausted or otherwise the limiting cap while the weekly window still has room, the default menu-bar metric can show the weekly remaining percentage instead of the out-of-monthly-quota state; only prefer weekly when the longer window is not the constrained quota.

Useful? React with 👍 / 👎.

else {
return windows.first
}
return weekly
}

static func rateTitle(
lane: RateLane,
windowMinutes: Int?,
sessionLabel: String,
weeklyLabel: String) -> String
{
switch windowMinutes {
case self.sessionWindowMinutes:
L(sessionLabel)
case self.weeklyWindowMinutes:
L(weeklyLabel)
case self.monthlyWindowMinutes:
L("Monthly")
default:
switch lane {
case .session:
L(sessionLabel)
case .weekly:
L(weeklyLabel)
case .monthly:
L("Monthly")
}
}
}

var nextMenuBarStateChangeAt: Date? {
self.rateWindowsByLane.values.compactMap { window in
guard window.remainingPercent <= 0,
Expand Down Expand Up @@ -407,7 +453,7 @@ struct CodexConsumerProjection {
}

private static func planUtilizationLanes(from rateWindowsByLane: [RateLane: RateWindow]) -> [PlanUtilizationLane] {
let semanticOrder: [RateLane] = [.session, .weekly]
let semanticOrder: [RateLane] = [.session, .weekly, .monthly]
return semanticOrder.compactMap { lane in
guard let window = rateWindowsByLane[lane] else { return nil }
return PlanUtilizationLane(role: self.planUtilizationRole(for: lane), window: window)
Expand All @@ -420,9 +466,15 @@ struct CodexConsumerProjection {
.session
case .weekly:
.weekly
case .monthly:
.monthly
Comment on lines +469 to +470

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 Allow monthly Codex history through the chart filter

For a Codex snapshot with a 43,200-minute primary, this new role causes UsageStore+PlanUtilization.swift to record new samples under .monthly, but PlanUtilizationHistoryChartMenuView.visibleSeriesNames still allows only .session for a nonnil primary and .weekly for a secondary. Its filter therefore drops every newly recorded monthly series, leaving the utilization chart stale or empty for these accounts. Update the chart's duration-based visibility logic, including handling existing 43,200-minute histories previously stored as .session.

Useful? React with 👍 / 👎.

}
}

static func planUtilizationSeriesNames(snapshot: UsageSnapshot) -> Set<PlanUtilizationSeriesName> {
Set(self.rateWindowsByLane(snapshot: snapshot).keys.map { self.planUtilizationRole(for: $0) })
}

private enum SnapshotSlot {
case primary
case secondary
Expand All @@ -432,10 +484,12 @@ struct CodexConsumerProjection {
guard let window else { return nil }

let lane: RateLane = switch window.windowMinutes {
case 300:
case Self.sessionWindowMinutes:
.session
case 10080:
case Self.weeklyWindowMinutes:
.weekly
case Self.monthlyWindowMinutes:
.monthly
Comment on lines +491 to +492

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve monthly windows during reset backfill

When a snapshot contains the targeted 43,200-minute primary plus a weekly secondary and a trusted prior snapshot is available, this classification makes sourceRateWindow(for: .session) return nil. codexMergedResetBackfillSnapshot consequently builds a weekly-only backfill snapshot, and codexBackfillingResetWindows replaces the freshly fetched primary with that nil session value. After the next normal or per-account refresh, the monthly quota is therefore removed from the published snapshot; the backfill helpers need to preserve/backfill the monthly lane while reconstructing the original slots.

Useful? React with 👍 / 👎.

default:
switch slot {
case .primary:
Expand Down Expand Up @@ -591,7 +645,9 @@ extension UsageStore {
usedPercent: usedPercent, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
case .primaryAndSecondary:
return windows.prefix(2).max(by: { $0.usedPercent < $1.usedPercent })
case .automatic, .primary, .monthlyPlan:
case .automatic:
return projection.automaticMenuBarWindow()
case .primary, .monthlyPlan:
return first
}
}
Expand Down
Loading