diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a0fc06c..6f5e36bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added ### Fixed +- Menu bar: show extra-usage spend as currency text for Claude and Cursor when that metric is selected (#1107). Thanks @Yuxin-Qiao! ## 0.29.0 — 2026-05-22 diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index af2859355..510e3f079 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -667,6 +667,11 @@ extension StatusItemController { mode: self.settings.kiroMenuBarDisplayMode, showUsed: self.settings.usageBarsShowUsed) } + if self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot) == .extraUsage, + let spend = Self.extraUsageSpendDisplayText(snapshot: snapshot) + { + return spend + } let percentWindow = self.menuBarPercentWindow(for: provider, snapshot: snapshot) let mode = self.settings.menuBarDisplayMode @@ -752,6 +757,16 @@ extension StatusItemController { removingSuffix: " left") } + nonisolated static func extraUsageSpendDisplayText(snapshot: UsageSnapshot?) -> String? { + guard let cost = snapshot?.providerCost, + cost.limit > 0, + cost.used >= 0 + else { + return nil + } + return UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) + } + nonisolated static func kiroDisplayText( snapshot: UsageSnapshot?, mode: KiroMenuBarDisplayMode, diff --git a/Tests/CodexBarTests/StatusItemExtraUsageMetricTests.swift b/Tests/CodexBarTests/StatusItemExtraUsageMetricTests.swift index 34b9e3746..3ef14df71 100644 --- a/Tests/CodexBarTests/StatusItemExtraUsageMetricTests.swift +++ b/Tests/CodexBarTests/StatusItemExtraUsageMetricTests.swift @@ -38,7 +38,9 @@ struct StatusItemExtraUsageMetricTests { @Test func `menu bar extra usage preference falls back to automatic when cursor on demand budget is missing`() { - let (store, controller) = self.makeCursorController(suiteName: "StatusItemExtraUsageMetricTests-missing-budget") + let (store, controller) = self.makeController( + suiteName: "StatusItemExtraUsageMetricTests-missing-budget", + provider: .cursor) let snapshot = UsageSnapshot( primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil), secondary: RateWindow(usedPercent: 72, windowMinutes: nil, resetsAt: nil, resetDescription: nil), @@ -54,19 +56,94 @@ struct StatusItemExtraUsageMetricTests { #expect(window?.usedPercent == 72) } + @Test + func `menu bar extra usage preference shows currency spend text for cursor when provider cost exists`() { + let (store, controller) = self.makeController( + suiteName: "StatusItemExtraUsageMetricTests-cursor-spend-text", + provider: .cursor) + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + tertiary: RateWindow(usedPercent: 72, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + providerCost: ProviderCostSnapshot( + used: 12.34, + limit: 100, + currencyCode: "USD", + updatedAt: Date()), + updatedAt: Date()) + + store._setSnapshotForTesting(snapshot, provider: .cursor) + store._setErrorForTesting(nil, provider: .cursor) + + let displayText = controller.menuBarDisplayText(for: .cursor, snapshot: snapshot) + + #expect(displayText == "$12.34") + } + + @Test + func `menu bar extra usage preference shows currency spend text for claude when provider cost exists`() { + let (store, controller) = self.makeController( + suiteName: "StatusItemExtraUsageMetricTests-claude-spend-text", + provider: .claude) + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 42, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: nil, + tertiary: nil, + providerCost: ProviderCostSnapshot( + used: 88.8, + limit: 200, + currencyCode: "USD", + period: "Monthly", + updatedAt: Date()), + updatedAt: Date()) + + store._setSnapshotForTesting(snapshot, provider: .claude) + store._setErrorForTesting(nil, provider: .claude) + + let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot) + + #expect(displayText == "$88.80") + } + + @Test + func `menu bar extra usage preference falls back to existing percent text when provider cost is unavailable`() { + let (store, controller) = self.makeController( + suiteName: "StatusItemExtraUsageMetricTests-fallback-percent", + provider: .cursor) + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 72, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + tertiary: nil, + providerCost: nil, + updatedAt: Date()) + + store._setSnapshotForTesting(snapshot, provider: .cursor) + store._setErrorForTesting(nil, provider: .cursor) + + let displayText = controller.menuBarDisplayText(for: .cursor, snapshot: snapshot) + + #expect(displayText == "72%") + } + private func makeCursorController(suiteName: String) -> (UsageStore, StatusItemController) { + self.makeController(suiteName: suiteName, provider: .cursor) + } + + private func makeController(suiteName: String, provider: UsageProvider) -> (UsageStore, StatusItemController) { let settings = SettingsStore( configStore: testConfigStore(suiteName: suiteName), zaiTokenStore: NoopZaiTokenStore()) settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true - settings.selectedMenuProvider = .cursor - settings.setMenuBarMetricPreference(.extraUsage, for: .cursor) + settings.selectedMenuProvider = provider + settings.menuBarDisplayMode = .percent + settings.usageBarsShowUsed = true + settings.setMenuBarMetricPreference(.extraUsage, for: provider) let registry = ProviderRegistry.shared - if let cursorMeta = registry.metadata[.cursor] { - settings.setProviderEnabled(provider: .cursor, metadata: cursorMeta, enabled: true) + if let metadata = registry.metadata[provider] { + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: true) } let fetcher = UsageFetcher()