Skip to content

Commit 3191f12

Browse files
authored
Show shorter cost comparison periods (#1816)
* feat: add cost comparison periods * fix: finish cost comparison settings port * fix: show cost comparisons in inline dashboard
1 parent a1da184 commit 3191f12

43 files changed

Lines changed: 458 additions & 10 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66
- Codex: show every available reset-credit expiry in menus and provider settings, including non-expiring credits, and summarize credits nearing expiry. Thanks @brahimhamichan!
7+
- Cost history: optionally show shorter 7, 30, and 90-day comparisons from the selected local history window (#1500). Thanks @jtl06!
78
- Codex cost history: group local usage and costs by project and worktree in menus and CLI output. Thanks @clemenspeters!
89
- Sakana AI: show best-effort pay-as-you-go credit balance and recent usage without delaying subscription quota refreshes. Thanks @ss251!
910
- Kimi: show monthly subscription usage alongside weekly and five-hour limits with a short total budget for the optional membership request. Thanks @zhiyue!

Sources/CodexBar/InlineUsageDashboardContent.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ extension UsageMenuCardView.Model {
184184
let tokenSnapshot = primaryCostHistorySnapshot(input: input),
185185
!tokenSnapshot.daily.isEmpty
186186
{
187-
return self.costHistoryInlineDashboard(provider: input.provider, snapshot: tokenSnapshot)
187+
return self.costHistoryInlineDashboard(
188+
provider: input.provider,
189+
snapshot: tokenSnapshot,
190+
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
188191
}
189192
if input.provider == .claude,
190193
let usage = input.snapshot?.claudeAdminAPIUsage
@@ -231,7 +234,10 @@ extension UsageMenuCardView.Model {
231234
let tokenSnapshot = input.tokenSnapshot,
232235
!tokenSnapshot.daily.isEmpty
233236
{
234-
return Self.costHistoryInlineDashboard(provider: input.provider, snapshot: tokenSnapshot)
237+
return Self.costHistoryInlineDashboard(
238+
provider: input.provider,
239+
snapshot: tokenSnapshot,
240+
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
235241
}
236242
return nil
237243
}
@@ -319,7 +325,8 @@ extension UsageMenuCardView.Model {
319325

320326
private static func costHistoryInlineDashboard(
321327
provider: UsageProvider,
322-
snapshot: CostUsageTokenSnapshot) -> InlineUsageDashboardModel
328+
snapshot: CostUsageTokenSnapshot,
329+
comparisonPeriodsEnabled: Bool) -> InlineUsageDashboardModel
323330
{
324331
let historyDays = max(1, min(365, snapshot.historyDays))
325332
let historyTitle = snapshot.historyLabel
@@ -354,6 +361,11 @@ extension UsageMenuCardView.Model {
354361
let usesLatestPrimary = provider == .bedrock || provider == .mistral
355362
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
356363
var details: [String] = []
364+
if comparisonPeriodsEnabled {
365+
details.append(contentsOf: snapshot.comparisonSummaries().map {
366+
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
367+
})
368+
}
357369
if let topModel = Self.topCostModel(from: snapshot.daily) {
358370
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
359371
}

Sources/CodexBar/MenuCardHeightFingerprint.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ extension UsageMenuCardView.Model.TokenUsageSection {
111111
MenuCardHeightFingerprint.join([
112112
MenuCardHeightFingerprint.field("session", self.sessionLine),
113113
MenuCardHeightFingerprint.field("month", self.monthLine),
114+
MenuCardHeightFingerprint.field("comparisons", self.comparisonLines.joined(separator: "|")),
114115
MenuCardHeightFingerprint.field("hint", self.hintLine),
115116
MenuCardHeightFingerprint.field("error", self.errorLine),
116117
MenuCardHeightFingerprint.field("errorCopy", self.errorCopyText),

Sources/CodexBar/MenuCardView+Costs.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ extension UsageMenuCardView.Model {
101101
static func tokenUsageSection(
102102
provider: UsageProvider,
103103
enabled: Bool,
104+
comparisonPeriodsEnabled: Bool,
104105
snapshot: CostUsageTokenSnapshot?,
105106
error: String?) -> TokenUsageSection?
106107
{
@@ -143,11 +144,29 @@ extension UsageMenuCardView.Model {
143144
return TokenUsageSection(
144145
sessionLine: sessionLine,
145146
monthLine: monthLine,
147+
comparisonLines: comparisonPeriodsEnabled
148+
? snapshot.comparisonSummaries().map {
149+
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
150+
}
151+
: [],
146152
hintLine: Self.tokenUsageHint(provider: provider),
147153
errorLine: err,
148154
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
149155
}
150156

157+
static func costWindowLine(summary: CostUsageWindowSummary, currencyCode: String) -> String {
158+
let label = Self.costHistoryWindowLabel(days: summary.days)
159+
let cost = summary.totalCostUSD.map {
160+
UsageFormatter.currencyString($0, currencyCode: currencyCode)
161+
} ?? ""
162+
guard let totalTokens = summary.totalTokens else { return "\(label): \(cost)" }
163+
return String(
164+
format: L("%@: %@ · %@ tokens"),
165+
label,
166+
cost,
167+
UsageFormatter.tokenCountString(totalTokens))
168+
}
169+
151170
static func tokenUsageHint(provider: UsageProvider) -> String? {
152171
switch provider {
153172
case .codex:

Sources/CodexBar/MenuCardView+ModelHelpers.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ extension UsageMenuCardView.Model {
200200
true
201201
case let (current?, candidate?):
202202
current.hintLine == candidate.hintLine &&
203-
current.errorLine == candidate.errorLine
203+
current.errorLine == candidate.errorLine &&
204+
current.comparisonLines.count == candidate.comparisonLines.count
204205
default:
205206
false
206207
}

Sources/CodexBar/MenuCardView+ModelInput.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extension UsageMenuCardView.Model {
2222
let tokenCostUsageEnabled: Bool
2323
let tokenCostInlineDashboardEnabled: Bool
2424
let tokenCostMenuSectionEnabled: Bool
25+
let costComparisonPeriodsEnabled: Bool
2526
let showOptionalCreditsAndExtraUsage: Bool
2627
let copilotBudgetExtrasEnabled: Bool
2728
let sourceLabel: String?
@@ -53,6 +54,7 @@ extension UsageMenuCardView.Model {
5354
tokenCostUsageEnabled: Bool,
5455
tokenCostInlineDashboardEnabled: Bool? = nil,
5556
tokenCostMenuSectionEnabled: Bool? = nil,
57+
costComparisonPeriodsEnabled: Bool = false,
5658
showOptionalCreditsAndExtraUsage: Bool,
5759
copilotBudgetExtrasEnabled: Bool = false,
5860
sourceLabel: String? = nil,
@@ -83,6 +85,7 @@ extension UsageMenuCardView.Model {
8385
self.tokenCostUsageEnabled = tokenCostUsageEnabled
8486
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled
8587
self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled
88+
self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled
8689
self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage
8790
self.copilotBudgetExtrasEnabled = copilotBudgetExtrasEnabled
8891
self.sourceLabel = sourceLabel

Sources/CodexBar/MenuCardView.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,26 @@ struct UsageMenuCardView: View {
8383
struct TokenUsageSection {
8484
let sessionLine: String
8585
let monthLine: String
86+
let comparisonLines: [String]
8687
let hintLine: String?
8788
let errorLine: String?
8889
let errorCopyText: String?
90+
91+
init(
92+
sessionLine: String,
93+
monthLine: String,
94+
comparisonLines: [String] = [],
95+
hintLine: String?,
96+
errorLine: String?,
97+
errorCopyText: String?)
98+
{
99+
self.sessionLine = sessionLine
100+
self.monthLine = monthLine
101+
self.comparisonLines = comparisonLines
102+
self.hintLine = hintLine
103+
self.errorLine = errorLine
104+
self.errorCopyText = errorCopyText
105+
}
89106
}
90107

91108
struct ProviderCostSection {
@@ -199,6 +216,11 @@ struct UsageMenuCardView: View {
199216
Text(tokenUsage.monthLine)
200217
.font(.footnote)
201218
.lineLimit(1)
219+
ForEach(tokenUsage.comparisonLines, id: \.self) { line in
220+
Text(line)
221+
.font(.footnote)
222+
.lineLimit(1)
223+
}
202224
if let hint = tokenUsage.hintLine, !hint.isEmpty {
203225
Text(hint)
204226
.font(.footnote)
@@ -717,6 +739,11 @@ struct UsageMenuCardCostSectionView: View {
717739
Text(tokenUsage.monthLine)
718740
.font(.caption)
719741
.lineLimit(1)
742+
ForEach(tokenUsage.comparisonLines, id: \.self) { line in
743+
Text(line)
744+
.font(.caption)
745+
.lineLimit(1)
746+
}
720747
if let hint = tokenUsage.hintLine, !hint.isEmpty {
721748
Text(hint)
722749
.font(.footnote)
@@ -832,6 +859,7 @@ extension UsageMenuCardView.Model {
832859
let tokenUsage = Self.tokenUsageSection(
833860
provider: input.provider,
834861
enabled: input.tokenCostMenuSectionEnabled,
862+
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled,
835863
snapshot: tokenUsageSnapshot,
836864
error: input.tokenError)
837865
let subtitle = Self.subtitle(

Sources/CodexBar/PreferencesDisplayPane.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ struct CostSummarySettingsSection: View {
241241
}
242242

243243
CostHistoryDaysEditor(settings: self.settings)
244+
245+
Toggle(isOn: self.$settings.costComparisonPeriodsEnabled) {
246+
SettingsRowLabel(
247+
L("cost_comparison_periods_title"),
248+
subtitle: L("cost_comparison_periods_subtitle"))
249+
}
244250
}
245251
} header: {
246252
Text(L("section_cost_summary"))

Sources/CodexBar/Resources/ar.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@
431431
"cost_history_window_title" = "نافذة التاريخ";
432432
"cost_history_window_help" = "يحدد عدد أيام سجلات الاستخدام المحلية التي تظهر في القائمة.";
433433
"cost_history_days_title" = "نافذة التاريخ: %d أيام";
434+
"cost_comparison_periods_title" = "إظهار فترات مقارنة أقصر";
435+
"cost_comparison_periods_subtitle" = "أضف إجماليات 7 و30 و90 يومًا عندما تقع ضمن نافذة التاريخ المحددة. تعيد هذه الإجماليات استخدام الفحص المحلي نفسه.";
434436
"cost_auto_refresh_info" = "تحديث تلقائي: كل ساعة · وقت الاستراحة: 10m";
435437
"refresh_cadence_title" = "وتيرة التحديث";
436438
"refresh_cadence_subtitle" = "كم مرة CodexBar استطلاعات في الخلفية.";

Sources/CodexBar/Resources/ca.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@
427427
"cost_history_window_help" = "Defineix quants dies de registres d'ús locals apareixen al menú.";
428428
"cost_history_days_title" = "Finestra d'historial: %d dies";
429429
"cost_auto_refresh_info" = "Actualització automàtica: cada hora · Temps d'espera: 10 min";
430+
"cost_comparison_periods_title" = "Mostra períodes de comparació més curts";
431+
"cost_comparison_periods_subtitle" = "Afegeix totals de 7, 30 i 90 dies quan càpiguen dins l'interval d'historial seleccionat. Aquests totals reutilitzen la mateixa exploració local.";
430432
"refresh_cadence_title" = "Freqüència d'actualització";
431433
"refresh_cadence_subtitle" = "Amb quina freqüència el CodexBar consulta els proveïdors en segon pla.";
432434
"manual_refresh_hint" = "L'actualització automàtica està desactivada; feu servir l'ordre Actualitza del menú.";

0 commit comments

Comments
 (0)