From dbe4e94322168538dfd64a41ccdf8cff189b0344 Mon Sep 17 00:00:00 2001 From: Resham Joshi <65915470+iamtoruk@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:30:58 +0200 Subject: [PATCH] fix(menubar): keep cost budget in USD and flag empty custom budget (#508) Two follow-ups from the budget review: - Currency consistency: the daily cost budget is defined in USD (the presets and the custom field are labeled "$"), but the "exceeded" banner ran the value through the display-currency rate, so a non-USD user saw the field and banner disagree (e.g. field "$100", banner "EUR 92"). Render the budget label in USD via a new asUSD() helper so the picker, field, and banner all agree. The over-budget comparison was already USD vs USD and is unchanged. - Empty custom cue: selecting "Custom..." and leaving the field blank stores 0, which silently disables the alert while the picker still shows "Custom...". The help text now says "Enter an amount above, or the alert stays off." in that state so it does not look armed when it isn't. --- mac/Sources/CodeBurnMenubar/AppStore.swift | 12 ++++++++++-- .../CodeBurnMenubar/Views/SettingsView.swift | 13 ++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index c2c6412..f38f1de 100644 --- a/mac/Sources/CodeBurnMenubar/AppStore.swift +++ b/mac/Sources/CodeBurnMenubar/AppStore.swift @@ -85,9 +85,11 @@ final class AppStore { return total >= activeDailyBudget } - /// The active daily-budget threshold formatted for display (currency or tokens). + /// The active daily-budget threshold formatted for display (tokens, or USD). + /// The cost budget is defined in USD (matching the "$" presets and field), so + /// it is not run through the display-currency conversion here. var dailyBudgetLabel: String { - isTokenMetric ? "\(activeDailyBudget.asCompactTokens()) tokens" : activeDailyBudget.asCurrency() + isTokenMetric ? "\(activeDailyBudget.asCompactTokens()) tokens" : activeDailyBudget.asUSD() } var isLoading: Bool { loadingCountsByKey.values.contains { $0 > 0 } } @@ -1268,6 +1270,12 @@ private let thousandsFormatter: NumberFormatter = { if n >= 1_000 { return String(format: "%.0fK", n / 1_000) } return String(format: "%.0f", n) } + + /// Formats a raw USD amount with a "$" and grouping, without applying the + /// display-currency rate. Used for the USD-denominated daily budget. + func asUSD() -> String { + "$" + (groupedDecimalFormatter.string(from: NSNumber(value: self)) ?? "\(Int(self))") + } } extension Int { diff --git a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift index 3f9733a..3197d66 100644 --- a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift +++ b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift @@ -57,6 +57,17 @@ private struct GeneralSettingsTab: View { v == v.rounded() ? String(Int(v)) : String(v) } + // Help text under the budget picker. When "Custom…" is selected but no amount + // has been entered, the budget is effectively 0 (off); call that out so the + // alert does not look armed when it isn't. + private var alertHelpText: String { + let customEmpty = store.isTokenMetric + ? (tokenCustom && store.dailyTokenBudget == 0) + : (costCustom && store.dailyBudget == 0) + if customEmpty { return "Enter an amount above, or the alert stays off." } + return "Flame icon turns yellow when today's \(store.isTokenMetric ? "tokens" : "cost") pass the daily budget." + } + var body: some View { Form { Section("Display") { @@ -162,7 +173,7 @@ private struct GeneralSettingsTab: View { } } } - Text("Flame icon turns yellow when today's \(store.isTokenMetric ? "tokens" : "cost") pass the daily budget.") + Text(alertHelpText) .font(.system(size: 11)) .foregroundStyle(.secondary) }