Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Added
- Cost history: optionally show shorter 7, 30, and 90-day comparisons derived from the selected local history window (#1500). Thanks @jtl06!

### Fixed
- Claude web usage: bound stale requests so Auto can reach CLI fallback instead of hanging indefinitely.
- Claude history: keep OAuth utilization separate across account switches while preserving continuity through token refreshes.
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/MenuCardHeightFingerprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ extension UsageMenuCardView.Model.TokenUsageSection {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("session", self.sessionLine),
MenuCardHeightFingerprint.field("month", self.monthLine),
MenuCardHeightFingerprint.field("comparisons", self.comparisonLines.joined(separator: "|")),
MenuCardHeightFingerprint.field("hint", self.hintLine),
MenuCardHeightFingerprint.field("error", self.errorLine),
MenuCardHeightFingerprint.field("errorCopy", self.errorCopyText),
Expand Down
19 changes: 19 additions & 0 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ extension UsageMenuCardView.Model {
static func tokenUsageSection(
provider: UsageProvider,
enabled: Bool,
comparisonPeriodsEnabled: Bool,
snapshot: CostUsageTokenSnapshot?,
error: String?) -> TokenUsageSection?
{
Expand Down Expand Up @@ -134,11 +135,29 @@ extension UsageMenuCardView.Model {
return TokenUsageSection(
sessionLine: sessionLine,
monthLine: monthLine,
comparisonLines: comparisonPeriodsEnabled
? snapshot.comparisonSummaries().map {
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
}
: [],
hintLine: Self.tokenUsageHint(provider: provider),
errorLine: err,
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
}

private static func costWindowLine(summary: CostUsageWindowSummary, currencyCode: String) -> String {
let label = Self.costHistoryWindowLabel(days: summary.days)
let cost = summary.totalCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: currencyCode)
} ?? "—"
guard let totalTokens = summary.totalTokens else { return "\(label): \(cost)" }
return String(
format: L("%@: %@ · %@ tokens"),
label,
cost,
UsageFormatter.tokenCountString(totalTokens))
}

static func tokenUsageHint(provider: UsageProvider) -> String? {
switch provider {
case .codex:
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ extension UsageMenuCardView.Model {
true
case let (current?, candidate?):
current.hintLine == candidate.hintLine &&
current.errorLine == candidate.errorLine
current.errorLine == candidate.errorLine &&
current.comparisonLines.count == candidate.comparisonLines.count
default:
false
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ extension UsageMenuCardView.Model {
let tokenCostUsageEnabled: Bool
let tokenCostInlineDashboardEnabled: Bool
let tokenCostMenuSectionEnabled: Bool
let costComparisonPeriodsEnabled: Bool
let showOptionalCreditsAndExtraUsage: Bool
let copilotBudgetExtrasEnabled: Bool
let sourceLabel: String?
Expand Down Expand Up @@ -51,6 +52,7 @@ extension UsageMenuCardView.Model {
tokenCostUsageEnabled: Bool,
tokenCostInlineDashboardEnabled: Bool? = nil,
tokenCostMenuSectionEnabled: Bool? = nil,
costComparisonPeriodsEnabled: Bool = false,
showOptionalCreditsAndExtraUsage: Bool,
copilotBudgetExtrasEnabled: Bool = false,
sourceLabel: String? = nil,
Expand Down Expand Up @@ -80,6 +82,7 @@ extension UsageMenuCardView.Model {
self.tokenCostUsageEnabled = tokenCostUsageEnabled
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled
self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled
self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled
self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage
self.copilotBudgetExtrasEnabled = copilotBudgetExtrasEnabled
self.sourceLabel = sourceLabel
Expand Down
28 changes: 28 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,26 @@ struct UsageMenuCardView: View {
struct TokenUsageSection {
let sessionLine: String
let monthLine: String
let comparisonLines: [String]
let hintLine: String?
let errorLine: String?
let errorCopyText: String?

init(
sessionLine: String,
monthLine: String,
comparisonLines: [String] = [],
hintLine: String?,
errorLine: String?,
errorCopyText: String?)
{
self.sessionLine = sessionLine
self.monthLine = monthLine
self.comparisonLines = comparisonLines
self.hintLine = hintLine
self.errorLine = errorLine
self.errorCopyText = errorCopyText
}
}

struct ProviderCostSection {
Expand Down Expand Up @@ -200,6 +217,11 @@ struct UsageMenuCardView: View {
Text(tokenUsage.monthLine)
.font(.footnote)
.lineLimit(1)
ForEach(tokenUsage.comparisonLines, id: \.self) { line in
Text(line)
.font(.footnote)
.lineLimit(1)
}
if let hint = tokenUsage.hintLine, !hint.isEmpty {
Text(hint)
.font(.footnote)
Expand Down Expand Up @@ -720,6 +742,11 @@ struct UsageMenuCardCostSectionView: View {
Text(tokenUsage.monthLine)
.font(.caption)
.lineLimit(1)
ForEach(tokenUsage.comparisonLines, id: \.self) { line in
Text(line)
.font(.caption)
.lineLimit(1)
}
if let hint = tokenUsage.hintLine, !hint.isEmpty {
Text(hint)
.font(.footnote)
Expand Down Expand Up @@ -831,6 +858,7 @@ extension UsageMenuCardView.Model {
let tokenUsage = Self.tokenUsageSection(
provider: input.provider,
enabled: input.tokenCostMenuSectionEnabled,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled,
snapshot: tokenUsageSnapshot,
error: input.tokenError)
let subtitle = Self.subtitle(
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBar/PreferencesGeneralPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ struct GeneralPane: View {

CostHistoryDaysEditor(settings: self.settings)

PreferenceToggleRow(
title: L("cost_comparison_periods_title"),
subtitle: L("cost_comparison_periods_subtitle"),
binding: self.$settings.costComparisonPeriodsEnabled)

Text(L("cost_auto_refresh_info"))
.font(.footnote)
.foregroundStyle(.tertiary)
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@
"cost_history_window_title" = "نافذة التاريخ";
"cost_history_window_help" = "يحدد عدد أيام سجلات الاستخدام المحلية التي تظهر في القائمة.";
"cost_history_days_title" = "نافذة التاريخ: %d أيام";
"cost_comparison_periods_title" = "إظهار فترات مقارنة أقصر";
"cost_comparison_periods_subtitle" = "أضف إجماليات 7 و30 و90 يومًا عندما تقع ضمن نافذة التاريخ المحددة. تعيد هذه الإجماليات استخدام الفحص المحلي نفسه.";
"cost_auto_refresh_info" = "تحديث تلقائي: كل ساعة · وقت الاستراحة: 10m";
"refresh_cadence_title" = "وتيرة التحديث";
"refresh_cadence_subtitle" = "كم مرة CodexBar استطلاعات في الخلفية.";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@
"cost_history_window_title" = "History window";
"cost_history_window_help" = "Sets how many days of local usage logs appear in the menu.";
"cost_history_days_title" = "History window: %d days";
"cost_comparison_periods_title" = "Show shorter comparison periods";
"cost_comparison_periods_subtitle" = "Add 7, 30, and 90-day totals when they fit inside the selected history window. These totals reuse the same local scan.";
"cost_auto_refresh_info" = "Auto-refresh: hourly · Timeout: 10m";
"refresh_cadence_title" = "Refresh cadence";
"refresh_cadence_subtitle" = "How often CodexBar polls providers in the background.";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/fa.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@
"cost_history_window_title" = "پنجره تاریخچه";
"cost_history_window_help" = "تعیین می‌کند چند روز از گزارش‌های استفاده محلی در منو نشان داده شود.";
"cost_history_days_title" = "پنجره تاریخچه: %d روز";
"cost_comparison_periods_title" = "نمایش دوره‌های مقایسه کوتاه‌تر";
"cost_comparison_periods_subtitle" = "وقتی در پنجره تاریخچه انتخاب‌شده جا می‌گیرند، مجموع‌های ۷، ۳۰ و ۹۰ روزه را اضافه کنید. این مجموع‌ها از همان اسکن محلی استفاده می‌کنند.";
"cost_auto_refresh_info" = "تازه سازی خودکار: ساعتی · زمان استراحت: 10m";
"refresh_cadence_title" = "کادانس تازه سازی";
"refresh_cadence_subtitle" = "چند وقت یکبار CodexBar ارائه دهندگان نظرسنجی در پس زمینه انجام می دهند.";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/id.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@
"cost_history_window_title" = "Jendela riwayat";
"cost_history_window_help" = "Menentukan berapa hari log penggunaan lokal yang ditampilkan di menu.";
"cost_history_days_title" = "Jendela riwayat: %d hari";
"cost_comparison_periods_title" = "Tampilkan periode perbandingan yang lebih singkat";
"cost_comparison_periods_subtitle" = "Tambahkan total 7, 30, dan 90 hari jika termasuk dalam rentang riwayat yang dipilih. Total ini menggunakan kembali pemindaian lokal yang sama.";
"cost_auto_refresh_info" = "Penyegaran otomatis: per jam · Batas waktu: 10m";
"refresh_cadence_title" = "Frekuensi penyegaran";
"refresh_cadence_subtitle" = "Seberapa sering CodexBar memeriksa penyedia di latar belakang.";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@
"cost_history_window_title" = "Finestra storica";
"cost_history_window_help" = "Imposta quanti giorni di log di utilizzo locali mostrare nel menu.";
"cost_history_days_title" = "Finestra storica: %d giorni";
"cost_comparison_periods_title" = "Mostra periodi di confronto più brevi";
"cost_comparison_periods_subtitle" = "Aggiungi i totali di 7, 30 e 90 giorni quando rientrano nell'intervallo di cronologia selezionato. Questi totali riutilizzano la stessa scansione locale.";
"cost_auto_refresh_info" = "Aggiornamento automatico: ogni ora · Timeout: 10 min";
"refresh_cadence_title" = "Frequenza aggiornamento";
"refresh_cadence_subtitle" = "Con quale frequenza CodexBar interroga i provider in background.";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@
"cost_history_window_title" = "Zakres historii";
"cost_history_window_help" = "Ustawia, ile dni lokalnych dzienników użycia pokazać w menu.";
"cost_history_days_title" = "Zakres historii: %d dni";
"cost_comparison_periods_title" = "Pokaż krótsze okresy porównawcze";
"cost_comparison_periods_subtitle" = "Dodaj sumy z 7, 30 i 90 dni, gdy mieszczą się w wybranym zakresie historii. Sumy te wykorzystują to samo skanowanie lokalne.";
"cost_auto_refresh_info" = "Auto-odświeżanie: co godzinę · Limit czasu: 10 min";
"refresh_cadence_title" = "Częstotliwość odświeżania";
"refresh_cadence_subtitle" = "Jak często CodexBar odpyta dostawców w tle.";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/th.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@
"cost_history_window_title" = "กรอบเวลาประวัติ";
"cost_history_window_help" = "กำหนดจำนวนวันของบันทึกการใช้งานในเครื่องที่จะแสดงในเมนู";
"cost_history_days_title" = "กรอบเวลาประวัติ: %d วัน";
"cost_comparison_periods_title" = "แสดงช่วงเปรียบเทียบที่สั้นกว่า";
"cost_comparison_periods_subtitle" = "เพิ่มยอดรวม 7, 30 และ 90 วันเมื่ออยู่ภายในกรอบเวลาประวัติที่เลือก โดยใช้การสแกนในเครื่องเดียวกัน";
"cost_auto_refresh_info" = "รีเฟรชอัตโนมัติ: รายชั่วโมง · หมดเวลา: 10m";
"refresh_cadence_title" = "จังหวะการรีเฟรช";
"refresh_cadence_subtitle" = "ความถี่ในการ CodexBar ผู้ให้บริการโพลในเบื้องหลัง";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/tr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@
"cost_history_window_title" = "Geçmiş penceresi";
"cost_history_window_help" = "Menüde kaç günlük yerel kullanım günlüğünün gösterileceğini belirler.";
"cost_history_days_title" = "Geçmiş penceresi: %d gün";
"cost_comparison_periods_title" = "Daha kısa karşılaştırma dönemlerini göster";
"cost_comparison_periods_subtitle" = "Seçilen geçmiş aralığına sığdığında 7, 30 ve 90 günlük toplamları ekler. Bu toplamlar aynı yerel taramayı yeniden kullanır.";
"cost_auto_refresh_info" = "Otomatik yenileme: saatlik · Zaman aşımı: 10 dk";
"refresh_cadence_title" = "Yenileme sıklığı";
"refresh_cadence_subtitle" = "CodexBar'ın arka planda sağlayıcıları ne sıklıkla sorgulayacağı.";
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,14 @@ extension SettingsStore {
}
}

var costComparisonPeriodsEnabled: Bool {
get { self.defaultsState.costComparisonPeriodsEnabled }
set {
self.defaultsState.costComparisonPeriodsEnabled = newValue
self.userDefaults.set(newValue, forKey: "costComparisonPeriodsEnabled")
}
}

var costSummaryDisplayStyleRaw: String {
get { self.defaultsState.costSummaryDisplayStyleRaw }
set {
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension SettingsStore {
_ = self.copilotIconSecondaryWindowIDRaw
_ = self.costUsageEnabled
_ = self.costUsageHistoryDays
_ = self.costComparisonPeriodsEnabled
_ = self.costSummaryDisplayStyle
_ = self.appLanguage
_ = self.hidePersonalInfo
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,8 @@ extension SettingsStore {
let costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? false
let rawCostUsageHistoryDays = userDefaults.object(forKey: "tokenCostUsageHistoryDays") as? Int ?? 30
let costUsageHistoryDays = max(1, min(365, rawCostUsageHistoryDays))
let costComparisonPeriodsEnabled = userDefaults.object(
forKey: "costComparisonPeriodsEnabled") as? Bool ?? false
let costSummaryDisplayStyleRaw = Self.loadCostSummaryDisplayStyleRaw(
userDefaults: userDefaults,
costUsageEnabled: costUsageEnabled)
Expand Down Expand Up @@ -488,6 +490,7 @@ extension SettingsStore {
copilotIconSecondaryWindowIDRaw: copilotIconSecondaryWindowIDRaw,
costUsageEnabled: costUsageEnabled,
costUsageHistoryDays: costUsageHistoryDays,
costComparisonPeriodsEnabled: costComparisonPeriodsEnabled,
costSummaryDisplayStyleRaw: costSummaryDisplayStyleRaw,
hidePersonalInfo: hidePersonalInfo,
randomBlinkEnabled: randomBlinkEnabled,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct SettingsDefaultsState {
var copilotIconSecondaryWindowIDRaw: String
var costUsageEnabled: Bool
var costUsageHistoryDays: Int
var costComparisonPeriodsEnabled: Bool
var costSummaryDisplayStyleRaw: String
var hidePersonalInfo: Bool
var randomBlinkEnabled: Bool
Expand Down
13 changes: 7 additions & 6 deletions Sources/CodexBar/StatusItemController+CostMenuCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,28 @@ extension StatusItemController {
}

static func costMenuTooltipLines(tokenUsage: UsageMenuCardView.Model.TokenUsageSection?) -> [String] {
[
let lines = [
tokenUsage?.sessionLine,
tokenUsage?.monthLine,
tokenUsage?.hintLine,
tokenUsage?.errorLine,
]
.compactMap(\.self)
.filter { !$0.isEmpty }
+ (tokenUsage?.comparisonLines ?? [])
+ [tokenUsage?.hintLine, tokenUsage?.errorLine].compactMap(\.self)
return lines.filter { !$0.isEmpty }
}

static func costMenuVisibleDetailLines(
tokenUsage: UsageMenuCardView.Model.TokenUsageSection?,
hasSubmenu: Bool) -> [String]
{
guard !hasSubmenu else { return [] }
let primaryLines = [
let primaryLines = ([
tokenUsage?.sessionLine,
tokenUsage?.monthLine,
tokenUsage?.errorLine,
]
.compactMap(\.self)
+ (tokenUsage?.comparisonLines ?? [])
+ [tokenUsage?.errorLine].compactMap(\.self))
.filter { !$0.isEmpty }
guard primaryLines.isEmpty else { return primaryLines }
return [tokenUsage?.hintLine]
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/StatusItemController+MenuCardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ extension StatusItemController {
tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: target),
tokenCostMenuSectionEnabled: !UsageStore.tokenCostRequiresProviderSnapshot(target) &&
self.settings.costSummaryShowsSubmenu(for: target),
costComparisonPeriodsEnabled: self.settings.costComparisonPeriodsEnabled,
showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage,
copilotBudgetExtrasEnabled: self.settings.copilotBudgetExtrasEnabled,
sourceLabel: sourceLabel,
Expand Down
Loading
Loading