diff --git a/CHANGELOG.md b/CHANGELOG.md index 91014933c0..d467254ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Usage bars: render values rounded to 0% or 100% as fully empty or full. Thanks @Zihao-Qi! - Codex web: keep cookie-import deadlines responsive when browser cookie work blocks the shared worker pool. - z.ai: open the usage dashboard for the configured global or China API region. Thanks @renbaoshuo! +- Usage dashboards: tint inline history bars with each provider's branding color. Thanks @elijahfriedman! - Codex pace: extrapolate historically exhausted weeks for run-out forecasts and avoid contradictory reset headlines. Thanks @Yuxin-Qiao! - Localization: correct the German in-progress refresh label. Thanks @ChrisLauinger77! - Localization: correct misleading literal German UI translations. Thanks @madebyjulz! diff --git a/Sources/CodexBar/InlineUsageDashboardContent.swift b/Sources/CodexBar/InlineUsageDashboardContent.swift index cc3f5ff557..762007bdc0 100644 --- a/Sources/CodexBar/InlineUsageDashboardContent.swift +++ b/Sources/CodexBar/InlineUsageDashboardContent.swift @@ -27,6 +27,9 @@ struct InlineUsageDashboardModel: Equatable { let kpis: [KPI] let points: [Point] let detailLines: [String] + /// Provider branding color used to fill the mini usage bars. When nil the bars fall back to a + /// neutral palette derived from `valueStyle`. + var barColor: Color? } extension UsageMenuCardView.Model { @@ -141,6 +144,19 @@ extension UsageMenuCardView.Model { } static func inlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? { + guard var model = self.resolveInlineUsageDashboard(input: input) else { return nil } + model.barColor = Self.inlineDashboardBarColor(for: input.provider) + return model + } + + /// Provider branding color for the inline usage bars, matching the provider's switcher tab and + /// detailed cost-history chart. + static func inlineDashboardBarColor(for provider: UsageProvider) -> Color { + let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color + return Color(red: color.red, green: color.green, blue: color.blue) + } + + private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? { if self.usesProviderCostHistoryAsPrimaryDashboard(input.provider), let tokenSnapshot = primaryCostHistorySnapshot(input: input), !tokenSnapshot.daily.isEmpty @@ -757,13 +773,20 @@ struct InlineUsageDashboardContent: View { if self.isHighlighted { return Color.white.opacity(0.55 + ratio * 0.35) } + return self.baseColor.opacity(0.42 + ratio * 0.58) + } + + private var baseColor: Color { + if let barColor = self.model.barColor { + return barColor + } switch self.model.valueStyle { case .currencyUSD, .currency: - return Color(red: 0.81, green: 0.56, blue: 0.24).opacity(0.42 + ratio * 0.58) + return Color(red: 0.81, green: 0.56, blue: 0.24) case .tokens: - return Color(red: 0.48, green: 0.41, blue: 0.86).opacity(0.42 + ratio * 0.58) + return Color(red: 0.48, green: 0.41, blue: 0.86) case .points: - return Color(red: 0.16, green: 0.62, blue: 0.36).opacity(0.42 + ratio * 0.58) + return Color(red: 0.16, green: 0.62, blue: 0.36) } } } diff --git a/Tests/CodexBarTests/InlineUsageDashboardBarColorTests.swift b/Tests/CodexBarTests/InlineUsageDashboardBarColorTests.swift new file mode 100644 index 0000000000..78b34b2c14 --- /dev/null +++ b/Tests/CodexBarTests/InlineUsageDashboardBarColorTests.swift @@ -0,0 +1,88 @@ +import CodexBarCore +import Foundation +import SwiftUI +import Testing +@testable import CodexBar + +struct InlineUsageDashboardBarColorTests { + /// The inline usage bars must be tinted with each provider's branding color (the same color + /// used by the switcher tab and the detailed cost-history chart) rather than a fixed palette. + @Test + func `bar color matches branding for every provider`() { + for provider in UsageProvider.allCases { + let branding = ProviderDescriptorRegistry.descriptor(for: provider).branding.color + let expected = Color(red: branding.red, green: branding.green, blue: branding.blue) + #expect( + UsageMenuCardView.Model.inlineDashboardBarColor(for: provider) == expected, + "inline bar color did not match branding for \(provider.rawValue)") + } + } + + /// The resolved dashboard model must actually carry the provider's branding color, and two + /// providers with different branding must end up with different bar colors. + @Test + func `resolved dashboard carries provider branding color`() throws { + let now = Date(timeIntervalSince1970: 1_700_179_200) + let daily = [ + CostUsageDailyReport.Entry( + date: "2023-11-14", + inputTokens: 100, + outputTokens: 50, + totalTokens: 150, + costUSD: 0.12, + modelsUsed: ["gpt-5"], + modelBreakdowns: nil), + CostUsageDailyReport.Entry( + date: "2023-11-15", + inputTokens: 200, + outputTokens: 75, + totalTokens: 275, + costUSD: 0.25, + modelsUsed: ["gpt-5"], + modelBreakdowns: nil), + ] + + func makeModel(provider: UsageProvider) throws -> UsageMenuCardView.Model { + let metadata = try #require(ProviderDefaults.metadata[provider]) + let tokenSnapshot = CostUsageTokenSnapshot( + sessionTokens: 275, + sessionCostUSD: 0.25, + last30DaysTokens: 425, + last30DaysCostUSD: 0.37, + historyDays: 30, + daily: daily, + updatedAt: now) + return UsageMenuCardView.Model.make(.init( + provider: provider, + metadata: metadata, + snapshot: UsageSnapshot( + primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: nil, + updatedAt: now), + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: tokenSnapshot, + tokenError: nil, + account: AccountInfo(email: nil, plan: nil), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: true, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + } + + let codex = try makeModel(provider: .codex) + let claude = try makeModel(provider: .claude) + + #expect(codex.inlineUsageDashboard?.barColor + == UsageMenuCardView.Model.inlineDashboardBarColor(for: .codex)) + #expect(claude.inlineUsageDashboard?.barColor + == UsageMenuCardView.Model.inlineDashboardBarColor(for: .claude)) + #expect(codex.inlineUsageDashboard?.barColor != claude.inlineUsageDashboard?.barColor) + } +}