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.
This commit is contained in:
Resham Joshi 2026-06-18 13:30:58 +02:00 committed by GitHub
parent 8794ddeb83
commit dbe4e94322
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 22 additions and 3 deletions

View file

@ -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 {

View file

@ -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)
}