diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index e0c54f639..e1e71c2a7 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -631,6 +631,7 @@ extension UsageMenuCardView.Model { let usageBarsShowUsed: Bool let resetTimeDisplayStyle: ResetTimeDisplayStyle let tokenCostUsageEnabled: Bool + let showEstimatedCostForSubscriptions: Bool let showOptionalCreditsAndExtraUsage: Bool let sourceLabel: String? let kiloAutoMode: Bool @@ -653,6 +654,7 @@ extension UsageMenuCardView.Model { usageBarsShowUsed: Bool, resetTimeDisplayStyle: ResetTimeDisplayStyle, tokenCostUsageEnabled: Bool, + showEstimatedCostForSubscriptions: Bool = true, showOptionalCreditsAndExtraUsage: Bool, sourceLabel: String? = nil, kiloAutoMode: Bool = false, @@ -674,6 +676,7 @@ extension UsageMenuCardView.Model { self.usageBarsShowUsed = usageBarsShowUsed self.resetTimeDisplayStyle = resetTimeDisplayStyle self.tokenCostUsageEnabled = tokenCostUsageEnabled + self.showEstimatedCostForSubscriptions = showEstimatedCostForSubscriptions self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage self.sourceLabel = sourceLabel self.kiloAutoMode = kiloAutoMode @@ -702,11 +705,15 @@ extension UsageMenuCardView.Model { } else { Self.providerCostSection(provider: input.provider, cost: input.snapshot?.providerCost) } + let loginMethod = input.snapshot?.loginMethod(for: input.provider) + let isSubscription = input.provider == .claude && UsageStore.isSubscriptionPlan(loginMethod) let tokenUsage = Self.tokenUsageSection( provider: input.provider, enabled: input.tokenCostUsageEnabled, snapshot: input.tokenSnapshot, - error: input.tokenError) + error: input.tokenError, + isSubscription: isSubscription, + showEstimatedCost: input.showEstimatedCostForSubscriptions) let subtitle = Self.subtitle( snapshot: input.snapshot, isRefreshing: input.isRefreshing, @@ -1094,36 +1101,54 @@ extension UsageMenuCardView.Model { provider: UsageProvider, enabled: Bool, snapshot: CostUsageTokenSnapshot?, - error: String?) -> TokenUsageSection? + error: String?, + isSubscription: Bool = false, + showEstimatedCost: Bool = true) -> TokenUsageSection? { guard provider == .codex || provider == .claude || provider == .vertexai else { return nil } guard enabled else { return nil } guard let snapshot else { return nil } - let sessionCost = snapshot.sessionCostUSD.map { UsageFormatter.usdString($0) } ?? "—" + // For subscription users, cost display is controlled by a setting. + // When shown, a hint line clarifies these are API-equivalent estimates. + let showCost = !isSubscription || showEstimatedCost + + let sessionCost = showCost ? (snapshot.sessionCostUSD.map { UsageFormatter.usdString($0) } ?? "—") : nil let sessionTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) } let sessionLine: String = { - if let sessionTokens { + if let sessionTokens, let sessionCost { return "Today: \(sessionCost) · \(sessionTokens) tokens" + } else if let sessionTokens { + return "Today: \(sessionTokens) tokens" + } else if let sessionCost { + return "Today: \(sessionCost)" } - return "Today: \(sessionCost)" + return "Today: —" }() - let monthCost = snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—" + let monthCost = showCost ? (snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—") : nil let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +) let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil) let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) } let monthLine: String = { - if let monthTokens { + if let monthTokens, let monthCost { return "Last 30 days: \(monthCost) · \(monthTokens) tokens" + } else if let monthTokens { + return "Last 30 days: \(monthTokens) tokens" + } else if let monthCost { + return "Last 30 days: \(monthCost)" } - return "Last 30 days: \(monthCost)" + return "Last 30 days: —" }() + + let hintLine: String? = (isSubscription && showCost) + ? "Estimated at API rates — not your actual spend" + : nil let err = (error?.isEmpty ?? true) ? nil : error return TokenUsageSection( sessionLine: sessionLine, monthLine: monthLine, - hintLine: nil, + hintLine: hintLine, errorLine: err, errorCopyText: (error?.isEmpty ?? true) ? nil : error) } diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index 39a95a55f..7a06630ad 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -47,6 +47,19 @@ struct GeneralPane: View { .font(.footnote) .foregroundStyle(.tertiary) + Toggle(isOn: self.$settings.showEstimatedCostForSubscriptions) { + Text("Include cost for subscription plans") + .font(.body) + } + .toggleStyle(.checkbox) + + Text( + "Show API-equivalent cost estimates for Claude subscription users (Pro, Max, Team). These are not actual charges." + ) + .font(.footnote) + .foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + self.costStatusLine(provider: .claude) self.costStatusLine(provider: .codex) } diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 877c78da9..b42eef339 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -354,6 +354,7 @@ struct ProvidersPane: View { usageBarsShowUsed: self.settings.usageBarsShowUsed, resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle, tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider), + showEstimatedCostForSubscriptions: self.settings.showEstimatedCostForSubscriptions, showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage, hidePersonalInfo: self.settings.hidePersonalInfo, now: Date()) diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index a5d9924d0..a214a7b83 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -166,6 +166,14 @@ extension SettingsStore { } } + var showEstimatedCostForSubscriptions: Bool { + get { self.defaultsState.showEstimatedCostForSubscriptions } + set { + self.defaultsState.showEstimatedCostForSubscriptions = newValue + self.userDefaults.set(newValue, forKey: "showEstimatedCostForSubscriptions") + } + } + var hidePersonalInfo: Bool { get { self.defaultsState.hidePersonalInfo } set { diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index a45a78cf6..f17425de2 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -207,6 +207,11 @@ extension SettingsStore { let claudeOAuthKeychainPromptModeRaw = userDefaults.string(forKey: "claudeOAuthKeychainPromptMode") let claudeOAuthKeychainReadStrategyRaw = userDefaults.string(forKey: "claudeOAuthKeychainReadStrategy") let claudeWebExtrasEnabledRaw = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false + let showEstimatedCostDefault = userDefaults.object(forKey: "showEstimatedCostForSubscriptions") as? Bool + let showEstimatedCostForSubscriptions = showEstimatedCostDefault ?? true + if showEstimatedCostDefault == nil { + userDefaults.set(true, forKey: "showEstimatedCostForSubscriptions") + } let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool let showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true if creditsExtrasDefault == nil { userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage") } @@ -247,6 +252,7 @@ extension SettingsStore { claudeOAuthKeychainPromptModeRaw: claudeOAuthKeychainPromptModeRaw, claudeOAuthKeychainReadStrategyRaw: claudeOAuthKeychainReadStrategyRaw, claudeWebExtrasEnabledRaw: claudeWebExtrasEnabledRaw, + showEstimatedCostForSubscriptions: showEstimatedCostForSubscriptions, showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage, openAIWebAccessEnabled: openAIWebAccessEnabled, jetbrainsIDEBasePath: jetbrainsIDEBasePath, diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index 28295b20a..628bf12f6 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -24,6 +24,7 @@ struct SettingsDefaultsState: Sendable { var claudeOAuthKeychainPromptModeRaw: String? var claudeOAuthKeychainReadStrategyRaw: String? var claudeWebExtrasEnabledRaw: Bool + var showEstimatedCostForSubscriptions: Bool var showOptionalCreditsAndExtraUsage: Bool var openAIWebAccessEnabled: Bool var jetbrainsIDEBasePath: String diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 2e7df0192..7965d0d64 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1412,6 +1412,7 @@ extension StatusItemController { usageBarsShowUsed: self.settings.usageBarsShowUsed, resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle, tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: target), + showEstimatedCostForSubscriptions: self.settings.showEstimatedCostForSubscriptions, showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage, sourceLabel: sourceLabel, kiloAutoMode: kiloAutoMode,