Skip to content

Commit f2ada4a

Browse files
author
kiranmagic7
committed
Use descriptor pace rules in CLI
1 parent e92a6fa commit f2ada4a

6 files changed

Lines changed: 240 additions & 54 deletions

File tree

Sources/CodexBar/MenuCardView+ModelHelpers.swift

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -560,27 +560,7 @@ extension UsageMenuCardView.Model {
560560

561561
private static func resetWindowForPace(provider: UsageProvider, window: RateWindow) -> RateWindow {
562562
// Provider snapshots use 30 days as a monthly sentinel; use the reset date for the real calendar-cycle length.
563-
let pace = ProviderDescriptorRegistry.descriptor(for: provider).pace
564-
guard pace.usesInferredMonthlyDuration(window: window),
565-
let resetsAt = window.resetsAt,
566-
let minutes = self.inferredMonthlyWindowMinutes(endingAt: resetsAt)
567-
else { return window }
568-
return RateWindow(
569-
usedPercent: window.usedPercent,
570-
windowMinutes: minutes,
571-
resetsAt: window.resetsAt,
572-
resetDescription: window.resetDescription,
573-
nextRegenPercent: window.nextRegenPercent,
574-
isSyntheticPlaceholder: window.isSyntheticPlaceholder)
575-
}
576-
577-
private static func inferredMonthlyWindowMinutes(endingAt resetsAt: Date) -> Int? {
578-
var calendar = Calendar(identifier: .gregorian)
579-
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? calendar.timeZone
580-
guard let startsAt = calendar.date(byAdding: .month, value: -1, to: resetsAt) else { return nil }
581-
let minutes = resetsAt.timeIntervalSince(startsAt) / 60
582-
guard minutes.isFinite, minutes > 0 else { return nil }
583-
return Int(minutes.rounded())
563+
ProviderDescriptorRegistry.descriptor(for: provider).pace.resolvedResetWindowForPace(window)
584564
}
585565

586566
static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {

Sources/CodexBarCLI/CLIPayloads.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ struct ProviderPayload: Encodable {
9898
struct ProviderPacePayload: Encodable {
9999
let primary: PacePayload?
100100
let secondary: PacePayload?
101+
let tertiary: PacePayload?
101102
}
102103

103104
struct PacePayload: Encodable {

Sources/CodexBarCLI/CLIRenderer.swift

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ enum CLIRenderer {
3434
context: context,
3535
now: now,
3636
lines: &lines)
37-
self.appendTertiaryLines(snapshot: snapshot, labels: labels, context: context, now: now, lines: &lines)
37+
self.appendTertiaryLines(
38+
provider: provider,
39+
snapshot: snapshot,
40+
labels: labels,
41+
context: context,
42+
now: now,
43+
lines: &lines)
3844
self.appendMiMoBalanceLine(snapshot: snapshot, useColor: context.useColor, lines: &lines)
3945
self.appendClawRouterUsageLines(snapshot: snapshot, useColor: context.useColor, lines: &lines)
4046
self.appendSub2APIUsageLines(snapshot: snapshot, useColor: context.useColor, lines: &lines)
@@ -102,7 +108,13 @@ enum CLIRenderer {
102108
context: context,
103109
now: now,
104110
lines: &lines)
105-
self.appendTertiaryLines(snapshot: snapshot, labels: labels, context: context, now: now, lines: &lines)
111+
self.appendTertiaryLines(
112+
provider: provider,
113+
snapshot: snapshot,
114+
labels: labels,
115+
context: context,
116+
now: now,
117+
lines: &lines)
106118
self.appendMiMoBalanceLine(snapshot: snapshot, useColor: context.useColor, lines: &lines)
107119
self.appendClawRouterUsageLines(snapshot: snapshot, useColor: context.useColor, lines: &lines)
108120
self.appendSub2APIUsageLines(snapshot: snapshot, useColor: context.useColor, lines: &lines)
@@ -574,8 +586,16 @@ enum CLIRenderer {
574586
weeklyWorkDays: weeklyWorkDays,
575587
now: now)
576588
}
577-
guard primary != nil || secondary != nil else { return nil }
578-
return ProviderPacePayload(primary: primary, secondary: secondary)
589+
let tertiary = snapshot.tertiary.flatMap {
590+
self.pacePayload(
591+
provider: provider,
592+
window: $0,
593+
kind: self.paceKind(provider: provider, fallback: .weekly),
594+
weeklyWorkDays: weeklyWorkDays,
595+
now: now)
596+
}
597+
guard primary != nil || secondary != nil || tertiary != nil else { return nil }
598+
return ProviderPacePayload(primary: primary, secondary: secondary, tertiary: tertiary)
579599
}
580600

581601
static func rateLine(title: String, window: RateWindow, useColor: Bool) -> String {
@@ -765,16 +785,28 @@ enum CLIRenderer {
765785
}
766786
}
767787

788+
// swiftlint:disable:next function_parameter_count
768789
private static func appendTertiaryLines(
790+
provider: UsageProvider,
769791
snapshot: UsageSnapshot,
770792
labels: RateWindowLabels,
771793
context: RenderContext,
772794
now: Date,
773795
lines: inout [String])
774796
{
775-
guard labels.showsTertiary, let opus = snapshot.tertiary else { return }
776-
lines.append(self.rateLine(title: labels.tertiary, window: opus, useColor: context.useColor))
777-
if let reset = self.resetLine(for: opus, style: context.resetStyle, now: now) {
797+
guard labels.showsTertiary, let tertiary = snapshot.tertiary else { return }
798+
lines.append(self.rateLine(title: labels.tertiary, window: tertiary, useColor: context.useColor))
799+
if let pace = self.paceLine(
800+
provider: provider,
801+
window: tertiary,
802+
kind: self.paceKind(provider: provider, fallback: .weekly),
803+
weeklyWorkDays: context.weeklyWorkDays,
804+
useColor: context.useColor,
805+
now: now)
806+
{
807+
lines.append(pace)
808+
}
809+
if let reset = self.resetLine(for: tertiary, style: context.resetStyle, now: now) {
778810
lines.append(self.subtleLine(reset, useColor: context.useColor))
779811
}
780812
}
@@ -1098,8 +1130,9 @@ enum CLIRenderer {
10981130

10991131
/// .session mirrors the GUI's session pace (5h window, real session windows only); .weekly reads
11001132
/// weeklyProgressWorkDays from the GUI's UserDefaults (same key) and passes it to UsagePace.weekly,
1101-
/// so the baseline matches the menu bar when the setting is configured. Codex historical refinement
1102-
/// is not applied (fixed allowlist only), so it can still differ from the menu for Codex accounts.
1133+
/// so the baseline matches the menu bar when the setting is configured. Descriptor-backed reset windows
1134+
/// also use .weekly wording. Codex historical refinement is not applied, so it can still differ from the
1135+
/// menu for Codex accounts.
11031136
private enum PaceKind {
11041137
case session
11051138
case weekly
@@ -1111,7 +1144,7 @@ enum CLIRenderer {
11111144
}
11121145
}
11131146

1114-
func supports(provider: UsageProvider) -> Bool {
1147+
func supportsStandardPace(provider: UsageProvider) -> Bool {
11151148
switch self {
11161149
case .session:
11171150
provider == .codex || provider == .claude || provider == .ollama || provider == .kimi
@@ -1122,6 +1155,11 @@ enum CLIRenderer {
11221155
}
11231156
}
11241157

1158+
private struct PaceComputation {
1159+
let pace: UsagePace
1160+
let kind: PaceKind
1161+
}
1162+
11251163
private static func paceKind(provider: UsageProvider, fallback: PaceKind) -> PaceKind {
11261164
guard provider == .kimi else { return fallback }
11271165
// Kimi stores weekly in primary and its rate limit in secondary, opposite the legacy CLI slot convention.
@@ -1138,36 +1176,45 @@ enum CLIRenderer {
11381176
window: RateWindow,
11391177
kind: PaceKind,
11401178
weeklyWorkDays: Int? = nil,
1141-
now: Date) -> UsagePace?
1179+
now: Date) -> PaceComputation?
11421180
{
1143-
guard kind.supports(provider: provider) else { return nil }
1181+
let capability = ProviderDescriptorRegistry.descriptor(for: provider).pace
1182+
let paceWindow: RateWindow
1183+
let resolvedKind: PaceKind
1184+
if capability.supportsResetWindowPace(window: window, now: now) {
1185+
paceWindow = capability.resolvedResetWindowForPace(window)
1186+
resolvedKind = .weekly
1187+
} else {
1188+
guard kind.supportsStandardPace(provider: provider) else { return nil }
1189+
paceWindow = window
1190+
resolvedKind = kind
1191+
}
11441192
if provider == .kimi {
1145-
let supportsWindow = switch kind {
1193+
let supportsWindow = switch resolvedKind {
11461194
case .session:
1147-
window.windowMinutes == KimiProviderDescriptor.sessionWindowMinutes
1195+
paceWindow.windowMinutes == KimiProviderDescriptor.sessionWindowMinutes
11481196
case .weekly:
1149-
ProviderDescriptorRegistry.descriptor(for: provider).pace
1150-
.supportsResetWindowPace(window: window, now: now)
1197+
capability.supportsResetWindowPace(window: paceWindow, now: now)
11511198
}
11521199
guard supportsWindow else { return nil }
11531200
}
11541201
// Only pace a real session window here; Claude w/o 5-hour data falls a 7-day window into primary.
1155-
if case .session = kind, let minutes = window.windowMinutes, minutes > 300 {
1202+
if case .session = resolvedKind, let minutes = paceWindow.windowMinutes, minutes > 300 {
11561203
return nil
11571204
}
1158-
if provider == .ollama, window.windowMinutes == nil {
1205+
if provider == .ollama, paceWindow.windowMinutes == nil {
11591206
return nil
11601207
}
1161-
guard window.remainingPercent > 0 else { return nil }
1208+
guard paceWindow.remainingPercent > 0 else { return nil }
11621209
// workDays applies only to the weekly (10 080-min) window; UsagePace.weekly ignores it for other durations.
1163-
let workDays = kind == .weekly ? weeklyWorkDays : nil
1210+
let workDays = resolvedKind == .weekly ? weeklyWorkDays : nil
11641211
guard let pace = UsagePace.weekly(
1165-
window: window,
1212+
window: paceWindow,
11661213
now: now,
1167-
defaultWindowMinutes: kind.defaultWindowMinutes,
1214+
defaultWindowMinutes: resolvedKind.defaultWindowMinutes,
11681215
workDays: workDays) else { return nil }
11691216
guard pace.expectedUsedPercent >= Self.paceMinimumExpectedPercent else { return nil }
1170-
return pace
1217+
return PaceComputation(pace: pace, kind: resolvedKind)
11711218
}
11721219

11731220
private static func paceSummary(
@@ -1194,14 +1241,19 @@ enum CLIRenderer {
11941241
useColor: Bool,
11951242
now: Date) -> String?
11961243
{
1197-
guard let pace = self.computePace(
1244+
guard let computation = self.computePace(
11981245
provider: provider,
11991246
window: window,
12001247
kind: kind,
12011248
weeklyWorkDays: weeklyWorkDays,
12021249
now: now) else { return nil }
12031250
let label = self.label("Pace", useColor: useColor)
1204-
return "\(label): \(self.paceSummary(provider: provider, for: pace, kind: kind, now: now))"
1251+
let summary = self.paceSummary(
1252+
provider: provider,
1253+
for: computation.pace,
1254+
kind: computation.kind,
1255+
now: now)
1256+
return "\(label): \(summary)"
12051257
}
12061258

12071259
private static func pacePayload(
@@ -1211,20 +1263,24 @@ enum CLIRenderer {
12111263
weeklyWorkDays: Int? = nil,
12121264
now: Date) -> PacePayload?
12131265
{
1214-
guard let pace = self.computePace(
1266+
guard let computation = self.computePace(
12151267
provider: provider,
12161268
window: window,
12171269
kind: kind,
12181270
weeklyWorkDays: weeklyWorkDays,
12191271
now: now) else { return nil }
12201272
return PacePayload(
1221-
stage: Self.stageString(pace.stage),
1222-
deltaPercent: pace.deltaPercent.rounded(),
1223-
expectedUsedPercent: pace.expectedUsedPercent.rounded(),
1224-
willLastToReset: pace.willLastToReset,
1225-
etaSeconds: pace.etaSeconds.map { $0.rounded() },
1226-
runOutProbability: pace.runOutProbability,
1227-
summary: self.paceSummary(provider: provider, for: pace, kind: kind, now: now))
1273+
stage: Self.stageString(computation.pace.stage),
1274+
deltaPercent: computation.pace.deltaPercent.rounded(),
1275+
expectedUsedPercent: computation.pace.expectedUsedPercent.rounded(),
1276+
willLastToReset: computation.pace.willLastToReset,
1277+
etaSeconds: computation.pace.etaSeconds.map { $0.rounded() },
1278+
runOutProbability: computation.pace.runOutProbability,
1279+
summary: self.paceSummary(
1280+
provider: provider,
1281+
for: computation.pace,
1282+
kind: computation.kind,
1283+
now: now))
12281284
}
12291285

12301286
private static func stageString(_ stage: UsagePace.Stage) -> String {

Sources/CodexBarCore/Providers/ProviderDescriptor.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,29 @@ public struct ProviderPaceCapability: Sendable {
7575
public func usesInferredMonthlyDuration(window: RateWindow) -> Bool {
7676
self.inferredMonthlyDuration.matches(window: window)
7777
}
78+
79+
public func resolvedResetWindowForPace(_ window: RateWindow) -> RateWindow {
80+
guard self.usesInferredMonthlyDuration(window: window),
81+
let resetsAt = window.resetsAt,
82+
let minutes = Self.inferredMonthlyWindowMinutes(endingAt: resetsAt)
83+
else { return window }
84+
return RateWindow(
85+
usedPercent: window.usedPercent,
86+
windowMinutes: minutes,
87+
resetsAt: window.resetsAt,
88+
resetDescription: window.resetDescription,
89+
nextRegenPercent: window.nextRegenPercent,
90+
isSyntheticPlaceholder: window.isSyntheticPlaceholder)
91+
}
92+
93+
private static func inferredMonthlyWindowMinutes(endingAt resetsAt: Date) -> Int? {
94+
var calendar = Calendar(identifier: .gregorian)
95+
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? calendar.timeZone
96+
guard let startsAt = calendar.date(byAdding: .month, value: -1, to: resetsAt) else { return nil }
97+
let minutes = resetsAt.timeIntervalSince(startsAt) / 60
98+
guard minutes.isFinite, minutes > 0 else { return nil }
99+
return Int(minutes.rounded())
100+
}
78101
}
79102

80103
public struct ProviderDescriptor: Sendable {

0 commit comments

Comments
 (0)