Skip to content
Merged
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
6 changes: 5 additions & 1 deletion Scripts/check-site-locales.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ assertEqual(
localeCatalog.filter((locale) => locale.direction === "rtl").map((locale) => locale.code),
["ar", "fa"],
"RTL locale catalog");
const appCatalogCodes = [...appLanguageSource.matchAll(/case \w+ = "([^"]+)"/g)]
const appLanguageEnumBody = appLanguageSource.match(
/enum AppLanguage:[^{]+\{([\s\S]*?)\n\}/,
)?.[1];
assert(appLanguageEnumBody, "could not locate AppLanguage cases");
const appCatalogCodes = [...appLanguageEnumBody.matchAll(/case \w+ = "([^"]+)"/g)]
.map((match) => match[1])
.filter(Boolean)
.map((code) => ({ "zh-Hans": "zh-CN", "zh-Hant": "zh-TW", ja: "ja-JP" })[code] ?? code);
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/CodexbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
guard let settings = self?.settings else { return }
AdaptiveActivityConsentPresenter.presentIfNeeded(settings: settings)
AppNotifications.shared.requestAuthorizationOnStartup()
// A persisted non-USD choice opts into the daily exchange-rate refresh. The service
// returns before networking for the default USD setting and Auto.
guard CurrencyExchange.requiresLiveRates(
preferredCurrencyCode: settings.preferredCurrencyCode)
else { return }
await CurrencyExchange.shared.fetchLatestRatesIfNeeded(
preferredCurrencyCode: settings.preferredCurrencyCode)
}
KeyboardShortcuts.onKeyUp(for: .openMenu) { [weak self] in
// KeyboardShortcuts dispatches both normal and menu-tracking hotkeys on the main event loop.
Expand Down
134 changes: 99 additions & 35 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,22 @@ extension UsageMenuCardView.Model {
return self.costHistoryInlineDashboard(
provider: input.provider,
snapshot: tokenSnapshot,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled,
preferredCurrencyCode: input.preferredCurrencyCode)
}
if input.provider == .claude,
let usage = input.snapshot?.claudeAdminAPIUsage
{
return Self.claudeAdminAPIInlineDashboard(usage)
return Self.claudeAdminAPIInlineDashboard(
usage,
preferredCurrencyCode: input.preferredCurrencyCode)
}
if input.provider == .openrouter,
let usage = input.snapshot?.openRouterUsage
{
return Self.openRouterInlineDashboard(usage)
return Self.openRouterInlineDashboard(
usage,
preferredCurrencyCode: input.preferredCurrencyCode)
}
if input.provider == .zai,
let modelUsage = input.snapshot?.zaiUsage?.modelUsage
Expand Down Expand Up @@ -269,7 +274,8 @@ extension UsageMenuCardView.Model {
return Self.costHistoryInlineDashboard(
provider: input.provider,
snapshot: tokenSnapshot,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled,
preferredCurrencyCode: input.preferredCurrencyCode)
}
return nil
}
Expand Down Expand Up @@ -414,8 +420,26 @@ extension UsageMenuCardView.Model {
private static func costHistoryInlineDashboard(
provider: UsageProvider,
snapshot: CostUsageTokenSnapshot,
comparisonPeriodsEnabled: Bool) -> InlineUsageDashboardModel
comparisonPeriodsEnabled: Bool,
preferredCurrencyCode: String) -> InlineUsageDashboardModel
{
let displayCurrencyCode = UsageFormatter.convertedCost(
0,
preferredCurrency: preferredCurrencyCode,
providerCurrency: snapshot.currencyCode).currencyCode
func convertedValue(_ value: Double) -> Double {
UsageFormatter.convertedCost(
value,
preferredCurrency: preferredCurrencyCode,
providerCurrency: snapshot.currencyCode).value
}
func convertedString(_ value: Double) -> String {
UsageFormatter.convertedCostString(
value,
preferredCurrency: preferredCurrencyCode,
providerCurrency: snapshot.currencyCode)
}

let historyDays = max(1, min(365, snapshot.historyDays))
let defaultHistoryTitle = snapshot.historyLabel
?? (historyDays == 1
Expand Down Expand Up @@ -454,16 +478,23 @@ extension UsageMenuCardView.Model {
return InlineUsageDashboardModel.Point(
id: entry.date,
label: Self.shortDayLabel(entry.date),
value: cost,
accessibilityValue: "\(entry.date): \(Self.costString(cost, currencyCode: snapshot.currencyCode))")
value: convertedValue(cost),
accessibilityValue: "\(entry.date): \(convertedString(cost))")
}
let latest = CostUsageTokenSnapshot.latestEntry(in: snapshot.daily)
let usesLatestPrimary = provider == .bedrock || provider == .mistral
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
var details: [String] = []
if comparisonPeriodsEnabled {
details.append(contentsOf: snapshot.comparisonSummaries().map {
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
let label = Self.costHistoryWindowLabel(days: $0.days)
let cost = $0.totalCostUSD.map(convertedString) ?? "—"
guard let totalTokens = $0.totalTokens else { return "\(label): \(cost)" }
return String(
format: L("%@: %@ · %@ tokens"),
label,
cost,
UsageFormatter.tokenCountString(totalTokens))
})
}
if let topModel = Self.topCostModel(from: snapshot.daily) {
Expand Down Expand Up @@ -494,12 +525,12 @@ extension UsageMenuCardView.Model {
var kpis = [
InlineUsageDashboardModel.KPI(
title: usesLatestPrimary ? L("Latest") : L("Today"),
value: primaryCostUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—",
value: primaryCostUSD.map(convertedString) ?? "—",
emphasis: true),
.init(
title: historyTitle,
value: snapshot.last30DaysCostUSD
.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—",
.map(convertedString) ?? "—",
emphasis: false),
]
let tokenHistoryKPI = InlineUsageDashboardModel.KPI(
Expand All @@ -518,17 +549,17 @@ extension UsageMenuCardView.Model {
kpis.insert(
.init(
title: "Cursor-metered",
value: Self.costString(meteredCostUSD, currencyCode: snapshot.currencyCode),
value: convertedString(meteredCostUSD),
emphasis: true),
at: 0)
}
var model = InlineUsageDashboardModel(
accessibilityLabel: accessibilityLabel,
valueStyle: Self.costValueStyle(currencyCode: snapshot.currencyCode),
valueStyle: Self.costValueStyle(currencyCode: displayCurrencyCode),
kpis: kpis,
points: points,
detailLines: details)
model.currencyCode = snapshot.currencyCode
model.currencyCode = displayCurrencyCode
return model
}

Expand All @@ -553,18 +584,36 @@ extension UsageMenuCardView.Model {
]
}

fileprivate static func claudeAdminAPIInlineDashboard(_ usage: ClaudeAdminAPIUsageSnapshot)
fileprivate static func claudeAdminAPIInlineDashboard(
_ usage: ClaudeAdminAPIUsageSnapshot,
preferredCurrencyCode: String = "auto")
-> InlineUsageDashboardModel
{
let displayCurrencyCode = UsageFormatter.convertedCost(
0,
preferredCurrency: preferredCurrencyCode,
providerCurrency: "USD").currencyCode
func convertedValue(_ value: Double) -> Double {
UsageFormatter.convertedCost(
value,
preferredCurrency: preferredCurrencyCode,
providerCurrency: "USD").value
}
func convertedString(_ value: Double) -> String {
UsageFormatter.convertedCostString(
value,
preferredCurrency: preferredCurrencyCode,
providerCurrency: "USD")
}
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: $0.costUSD,
accessibilityValue: "\($0.day): \(UsageFormatter.usdString($0.costUSD))")
value: convertedValue($0.costUSD),
accessibilityValue: "\($0.day): \(convertedString($0.costUSD))")
}
var details = [
"30d: \(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))",
Expand All @@ -575,13 +624,13 @@ extension UsageMenuCardView.Model {
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("Claude Admin API 30 day spend trend"),
valueStyle: .currencyUSD,
valueStyle: Self.costValueStyle(currencyCode: displayCurrencyCode),
kpis: [
.init(title: L("Today"), value: UsageFormatter.usdString(today.costUSD), emphasis: true),
.init(title: L("7d spend"), value: UsageFormatter.usdString(last7.costUSD), emphasis: false),
.init(title: L("Today"), value: convertedString(today.costUSD), emphasis: true),
.init(title: L("7d spend"), value: convertedString(last7.costUSD), emphasis: false),
.init(
title: L("30d spend"),
value: UsageFormatter.usdString(last30.costUSD),
value: convertedString(last30.costUSD),
emphasis: false),
.init(
title: L("Today tokens"),
Expand All @@ -590,23 +639,42 @@ extension UsageMenuCardView.Model {
],
points: points,
detailLines: details)
model.currencyCode = "USD"
model.currencyCode = displayCurrencyCode
return model
}

private static func openRouterInlineDashboard(_ usage: OpenRouterUsageSnapshot) -> InlineUsageDashboardModel? {
private static func openRouterInlineDashboard(
_ usage: OpenRouterUsageSnapshot,
preferredCurrencyCode: String) -> InlineUsageDashboardModel?
{
let displayCurrencyCode = UsageFormatter.convertedCost(
0,
preferredCurrency: preferredCurrencyCode,
providerCurrency: "USD").currencyCode
func convertedValue(_ value: Double) -> Double {
UsageFormatter.convertedCost(
value,
preferredCurrency: preferredCurrencyCode,
providerCurrency: "USD").value
}
func convertedString(_ value: Double) -> String {
UsageFormatter.convertedCostString(
value,
preferredCurrency: preferredCurrencyCode,
providerCurrency: "USD")
}
let periodValues: [(String, String, Double?)] = [
("day", L("Today"), usage.keyUsageDaily),
("week", L("Week"), usage.keyUsageWeekly),
("month", L("Month"), usage.keyUsageMonthly),
]
let points = periodValues.compactMap { id, label, value -> InlineUsageDashboardModel.Point? in
guard let value else { return nil }
let formattedValue = Self.openRouterCurrencyString(value)
let formattedValue = convertedString(value)
return InlineUsageDashboardModel.Point(
id: id,
label: label,
value: value,
value: convertedValue(value),
accessibilityValue: String(format: L("%@: %@"), label, formattedValue))
}
guard !points.isEmpty else { return nil }
Expand All @@ -620,7 +688,7 @@ extension UsageMenuCardView.Model {
details.append(String(
format: L("%@: %@"),
L("Key remaining"),
Self.openRouterCurrencyString(remaining)))
convertedString(remaining)))
}
case .noLimitConfigured:
details.append(L("No limit set for the API key"))
Expand All @@ -629,25 +697,25 @@ extension UsageMenuCardView.Model {
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("OpenRouter API key spend trend"),
valueStyle: .currencyUSD,
valueStyle: Self.costValueStyle(currencyCode: displayCurrencyCode),
kpis: [
.init(title: L("Balance"), value: Self.openRouterCurrencyString(usage.balance), emphasis: true),
.init(title: L("Balance"), value: convertedString(usage.balance), emphasis: true),
.init(
title: L("Today"),
value: usage.keyUsageDaily.map(Self.openRouterCurrencyString) ?? "—",
value: usage.keyUsageDaily.map(convertedString) ?? "—",
emphasis: false),
.init(
title: L("Week"),
value: usage.keyUsageWeekly.map(Self.openRouterCurrencyString) ?? "—",
value: usage.keyUsageWeekly.map(convertedString) ?? "—",
emphasis: false),
.init(
title: L("Month"),
value: usage.keyUsageMonthly.map(Self.openRouterCurrencyString) ?? "—",
value: usage.keyUsageMonthly.map(convertedString) ?? "—",
emphasis: false),
],
points: points,
detailLines: details)
model.currencyCode = "USD"
model.currencyCode = displayCurrencyCode
return model
}

Expand Down Expand Up @@ -810,10 +878,6 @@ extension UsageMenuCardView.Model {
}?.key
}

private static func openRouterCurrencyString(_ value: Double) -> String {
String(format: "$%.2f", value)
}

private static func minimaxCashString(_ value: Double) -> String {
String(format: "%.2f", max(0, value))
}
Expand Down
Loading