diff --git a/CHANGELOG.md b/CHANGELOG.md index b400c7cc45..d77b382503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Widgets: opt-in display of Claude model-scoped weekly quotas (for example Fable) projected from the shared usage snapshot, off by default (#2645). Thanks @alfredjbclaw! ### Fixed +- Menu: apply the cost summary display style to every provider's menu card, so Submenu only hides inline cost rows for z.ai and other providers (#2976). Thanks @ar0nbg! - Codex: keep CLI-owned `auth.json` read-only during usage refresh, delegate stale native credentials to CLI recovery, and fail closed for stale external OAuth files (#2944). Thanks @Yuxin-Qiao! - Usage & Spend: keep safely priced Codex totals visible after completed history scans when request-tier uncertainty leaves some days unpriced (#2948). Thanks @Atopoz for the report! - Vertex AI: match Cloud Monitoring quota usage without a `limit_name` to its unambiguous same-metric, same-location limit, restoring quota percentages (#2958). Thanks @MachApple! diff --git a/Sources/CodexBar/InlineUsageDashboardContent.swift b/Sources/CodexBar/InlineUsageDashboardContent.swift index 2eb617e4ed..4769af109a 100644 --- a/Sources/CodexBar/InlineUsageDashboardContent.swift +++ b/Sources/CodexBar/InlineUsageDashboardContent.swift @@ -41,7 +41,7 @@ extension UsageMenuCardView.Model { switch menuCard.usageNotes(context: ProviderUsageNotesContext( snapshot: input.snapshot, isRefreshing: input.isRefreshing, - tokenCostInlineDashboardEnabled: input.tokenCostInlineDashboardEnabled, + costSummaryInlineEnabled: input.costSummaryInlineEnabled, showOptionalUsage: input.showOptionalCreditsAndExtraUsage)) { case let .openAIAPI(usage): @@ -94,6 +94,7 @@ extension UsageMenuCardView.Model { private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? { let menuCard = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard if menuCard.usesProviderCostHistoryAsPrimaryDashboard, + input.costSummaryInlineEnabled, let tokenSnapshot = primaryCostHistorySnapshot(input: input), !tokenSnapshot.daily.isEmpty { @@ -104,7 +105,7 @@ extension UsageMenuCardView.Model { preferredCurrencyCode: input.preferredCurrencyCode) } if menuCard.supportsInlineTokenCostDashboard, - input.tokenCostInlineDashboardEnabled, + input.costSummaryInlineEnabled, let tokenSnapshot = input.tokenSnapshot, !tokenSnapshot.daily.isEmpty || tokenSnapshot.meteredCostUSD != nil { diff --git a/Sources/CodexBar/MenuCardView+Costs.swift b/Sources/CodexBar/MenuCardView+Costs.swift index fde4d99a44..e90cf9d442 100644 --- a/Sources/CodexBar/MenuCardView+Costs.swift +++ b/Sources/CodexBar/MenuCardView+Costs.swift @@ -90,6 +90,25 @@ extension UsageMenuCardView.Model.ProviderCostSection { } extension UsageMenuCardView.Model { + static func providerCostFollowsSummaryStyle( + cost: ProviderCostSnapshot?, + style: ProviderCostMenuCardStyle, + isClaudeAdminAPI: Bool) -> Bool + { + // Provider-specific by design: Claude Admin API spend is a summary, while Claude extra-usage balance is not. + switch style { + case .apiSpend, .payAsYouGoSpend: + true + case .claude: + isClaudeAdminAPI + case .clawRouter: + (cost?.limit ?? 0) <= 0 + case .generic, .hidden, .extraUsageBalance, .zenBalance, .pointsBalance, .prepaidCredits, + .payAsYouGoBalance: + false + } + } + static func isRequiredOpenCodeZenBalance(_ snapshot: UsageSnapshot?) -> Bool { snapshot?.primary == nil && snapshot?.secondary == nil && diff --git a/Sources/CodexBar/MenuCardView+ModelInput.swift b/Sources/CodexBar/MenuCardView+ModelInput.swift index 754047d8d6..12210aa1b5 100644 --- a/Sources/CodexBar/MenuCardView+ModelInput.swift +++ b/Sources/CodexBar/MenuCardView+ModelInput.swift @@ -24,7 +24,7 @@ extension UsageMenuCardView.Model { let tokenCostUsageEnabled: Bool let tokenCostIsRefreshing: Bool let codexLocalSessionCostLedgerEnabled: Bool - let tokenCostInlineDashboardEnabled: Bool + let costSummaryInlineEnabled: Bool let tokenCostMenuSectionEnabled: Bool let costComparisonPeriodsEnabled: Bool let showOptionalCreditsAndExtraUsage: Bool @@ -69,7 +69,7 @@ extension UsageMenuCardView.Model { tokenCostUsageEnabled: Bool, tokenCostIsRefreshing: Bool = false, codexLocalSessionCostLedgerEnabled: Bool = false, - tokenCostInlineDashboardEnabled: Bool? = nil, + costSummaryInlineEnabled: Bool? = nil, tokenCostMenuSectionEnabled: Bool? = nil, costComparisonPeriodsEnabled: Bool = false, showOptionalCreditsAndExtraUsage: Bool, @@ -111,7 +111,7 @@ extension UsageMenuCardView.Model { self.tokenCostUsageEnabled = tokenCostUsageEnabled self.tokenCostIsRefreshing = tokenCostIsRefreshing self.codexLocalSessionCostLedgerEnabled = codexLocalSessionCostLedgerEnabled - self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled + self.costSummaryInlineEnabled = costSummaryInlineEnabled ?? tokenCostUsageEnabled self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index e67897aa7b..effcb26551 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -975,7 +975,13 @@ extension UsageMenuCardView.Model { let providerCostStyle = input.snapshot.map { presentation.cost(snapshot: $0).menuCardStyle } ?? .generic - let providerCost: ProviderCostSection? = if !showsProviderCost { + let providerCostFollowsSummaryStyle = Self.providerCostFollowsSummaryStyle( + cost: input.snapshot?.providerCost, + style: providerCostStyle, + isClaudeAdminAPI: isClaudeAdminAPI) + let providerCost: ProviderCostSection? = if !showsProviderCost || + (providerCostFollowsSummaryStyle && !input.costSummaryInlineEnabled) + { nil } else { Self.providerCostSection( @@ -1031,8 +1037,13 @@ extension UsageMenuCardView.Model { private static func visibleProviderDetails(input: Input) -> [ProviderDetailSection] { var details = input.snapshot?.details ?? [] + let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.optionalDetails + if !input.costSummaryInlineEnabled, !policy.costSummaryTitles.isEmpty { + details.removeAll { section in + section.title.map(policy.costSummaryTitles.contains) == true + } + } if !input.showOptionalCreditsAndExtraUsage { - let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.optionalDetails if policy.hidesAllWithoutOptionalUsage { details = [] } else if !policy.hiddenTitlesWithoutOptionalUsage.isEmpty { diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 8b9891b1ae..138b2e5a12 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -588,9 +588,9 @@ struct ProvidersPane: View { resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle, tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider), codexLocalSessionCostLedgerEnabled: self.settings.codexLocalSessionCostLedgerEnabled, - tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: provider), // Display style only controls the main menu. Provider details always expose // available cost data in their Usage section. + costSummaryInlineEnabled: true, tokenCostMenuSectionEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider), showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage, claudeDailyRoutinesUsageVisible: self.settings.claudeDailyRoutinesUsageVisible, diff --git a/Sources/CodexBar/SettingsStore+TokenCost.swift b/Sources/CodexBar/SettingsStore+TokenCost.swift index ec1bdb8f8b..d57fd06ddc 100644 --- a/Sources/CodexBar/SettingsStore+TokenCost.swift +++ b/Sources/CodexBar/SettingsStore+TokenCost.swift @@ -2,12 +2,11 @@ import CodexBarCore import Foundation extension SettingsStore { - func costSummaryShowsInlineDashboard(for provider: UsageProvider) -> Bool { - // Provider-specific by design: DeepSeek's API exposes a balance card but no token-cost submenu data. - if provider == .deepseek { - return self.costUsageEnabled - } - return self.isCostUsageEffectivelyEnabled(for: provider) && + func costSummaryShowsInline(for provider: UsageProvider) -> Bool { + // Provider-specific by design: Codex's local ledger can enable its summary without the global scanner. + let isEnabled = self.costUsageEnabled || + (provider == .codex && self.codexLocalSessionCostLedgerEnabled) + return isEnabled && self.costSummaryDisplayStyle.showsInlineSummary } diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index be4f44c07c..cacc1426e4 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1483,13 +1483,13 @@ extension StatusItemController { return self.makeUsageBreakdownSubmenu(width: width) } // Provider-specific by design: OpenAI and Mistral attach cost history to their provider usage row. - if provider == .openai { + if provider == .openai, self.settings.costSummaryShowsSubmenu(for: provider) { return self.makeOpenAIAPIUsageSubmenu(provider: provider, width: width) } - // Mistral's top usage pane has no rate-limit bars of its own, so its cost history always hangs - // off this row. Providers whose cards render an inline cost dashboard also gain cost history - // on the top card when the Cost Summary style permits it; Both still keeps the dedicated Cost row. - if provider == .mistral { + // Mistral's top usage pane has no rate-limit bars of its own, so its cost history hangs off this row + // when the Cost Summary style permits it. Other inline cost dashboards follow the same submenu policy; + // Both still keeps the dedicated Cost row. + if provider == .mistral, self.settings.costSummaryShowsSubmenu(for: provider) { return self.makeCostHistorySubmenu(provider: provider, width: width) } if hasInlineCostDashboard, self.settings.costSummaryShowsSubmenu(for: provider) { diff --git a/Sources/CodexBar/StatusItemController+MenuCardModel.swift b/Sources/CodexBar/StatusItemController+MenuCardModel.swift index ad726ec8ca..1f50122e8b 100644 --- a/Sources/CodexBar/StatusItemController+MenuCardModel.swift +++ b/Sources/CodexBar/StatusItemController+MenuCardModel.swift @@ -128,7 +128,7 @@ extension StatusItemController { tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: target), tokenCostIsRefreshing: self.store.tokenCostRefreshIsActive(for: target), codexLocalSessionCostLedgerEnabled: self.settings.codexLocalSessionCostLedgerEnabled, - tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: target), + costSummaryInlineEnabled: self.settings.costSummaryShowsInline(for: target), // openai/mistral's cost history always surfaces via the inline dashboard or a // dedicated top-pane submenu (see `makeUsageSubmenu`), so they skip the generic // "Cost" row. This must stay an explicit provider check rather than reusing diff --git a/Sources/CodexBar/StatusItemController+OverviewSubmenus.swift b/Sources/CodexBar/StatusItemController+OverviewSubmenus.swift index b8e66f6dad..1587d2113d 100644 --- a/Sources/CodexBar/StatusItemController+OverviewSubmenus.swift +++ b/Sources/CodexBar/StatusItemController+OverviewSubmenus.swift @@ -8,15 +8,16 @@ extension StatusItemController { width: CGFloat) -> NSMenu? { if provider == .openai, + self.settings.costSummaryShowsSubmenu(for: provider), let submenu = self.makeOpenAIAPIUsageSubmenu(provider: provider, width: width) { return submenu } - // Mistral's top usage pane has no rate-limit bars of its own, so its Overview row always - // prioritizes cost history too. Other `tokenCostRequiresProviderSnapshot` providers (e.g. - // opencodego) show real rate-limit bars and should fall through to the settings-gated - // check below, same as Codex/Claude (see StatusItemController+Menu.swift's makeUsageSubmenu). + // Mistral's top usage pane has no rate-limit bars of its own, so its Overview row prioritizes + // cost history when the display style permits it. Other `tokenCostRequiresProviderSnapshot` + // providers (e.g. opencodego) show real rate-limit bars and fall through to the generic check. if provider == .mistral, + self.settings.costSummaryShowsSubmenu(for: provider), let submenu = self.makeCostHistorySubmenu(provider: provider, width: width) { return submenu diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index 18c33c2792..a8c2b394e0 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -201,7 +201,9 @@ public enum ClaudeProviderDescriptor { costVisibilityResolver: { context in context.showOptionalUsage || context.snapshot?.loginMethod(for: .claude) == "Admin API" }, - supportsInlineTokenCostDashboard: true)), + supportsInlineTokenCostDashboard: true), + optionalDetails: ProviderOptionalDetailsPresentation( + costSummaryTitles: ["Usage summary", "Cost items"])), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .api, .web, .cli, .oauth], pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)), diff --git a/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift b/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift index 04bb65e52c..29a41b5521 100644 --- a/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift @@ -120,7 +120,7 @@ public enum DeepSeekProviderDescriptor { return .localized(["Select a DeepSeek Chrome profile in Settings."]) } } - guard context.tokenCostInlineDashboardEnabled, context.showOptionalUsage else { + guard context.costSummaryInlineEnabled, context.showOptionalUsage else { return .unhandled } guard context.snapshot?.details.isEmpty == false else { @@ -137,7 +137,9 @@ public enum DeepSeekProviderDescriptor { showsPrimaryBalanceDescription: true, hidesPrimaryResetWithoutDate: true, movePrimaryDetailToStatus: { _ in true }), - menu: ProviderMenuDescriptorPresentation(primaryDescriptionIsDetail: { _ in true })), + menu: ProviderMenuDescriptorPresentation(primaryDescriptionIsDetail: { _ in true }), + optionalDetails: ProviderOptionalDetailsPresentation( + costSummaryTitles: ["Detailed usage"])), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .api, .web], pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)), diff --git a/Sources/CodexBarCore/Providers/Groq/GroqProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Groq/GroqProviderDescriptor.swift index 66ee13393f..4cd584d09e 100644 --- a/Sources/CodexBarCore/Providers/Groq/GroqProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Groq/GroqProviderDescriptor.swift @@ -51,6 +51,9 @@ public enum GroqProviderDescriptor { noDataMessage: { "Sign in at console.groq.com to show Groq spend and token usage." }, showsRequestHistory: false, hintPlacement: .hidden), + presentation: ProviderUsagePresentation( + optionalDetails: ProviderOptionalDetailsPresentation( + costSummaryTitles: ["Usage summary"])), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .web, .api], pipeline: ProviderFetchPipeline(resolveStrategies: { context in diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift index 4dd08ebec6..f8b144f7a4 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift @@ -108,7 +108,8 @@ public enum MiniMaxProviderDescriptor { context.snapshot.tertiary)) }, optionalDetails: ProviderOptionalDetailsPresentation( - hiddenTitlesWithoutOptionalUsage: ["Billing history"])), + hiddenTitlesWithoutOptionalUsage: ["Billing history"], + costSummaryTitles: ["Billing history"])), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .web, .api], pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)), diff --git a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift index 64aafbe87c..6f242892a7 100644 --- a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift @@ -71,7 +71,9 @@ public enum OpenAIAPIProviderDescriptor { return projected } return snapshot == nil ? tokenSnapshot : nil - })), + }), + optionalDetails: ProviderOptionalDetailsPresentation( + costSummaryTitles: ["Usage summary"])), fetchPlan: self.fetchPlan(), cli: ProviderCLIConfig( name: "openai", diff --git a/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift b/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift index 64a5047ad0..fba4bdc0e1 100644 --- a/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift +++ b/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift @@ -98,13 +98,16 @@ public struct ProviderCostPresentation: Sendable, Equatable { public struct ProviderOptionalDetailsPresentation: Sendable, Equatable { public let hidesAllWithoutOptionalUsage: Bool public let hiddenTitlesWithoutOptionalUsage: Set + public let costSummaryTitles: Set public init( hidesAllWithoutOptionalUsage: Bool = false, - hiddenTitlesWithoutOptionalUsage: Set = []) + hiddenTitlesWithoutOptionalUsage: Set = [], + costSummaryTitles: Set = []) { self.hidesAllWithoutOptionalUsage = hidesAllWithoutOptionalUsage self.hiddenTitlesWithoutOptionalUsage = hiddenTitlesWithoutOptionalUsage + self.costSummaryTitles = costSummaryTitles } } @@ -208,18 +211,18 @@ public enum ProviderWidgetFamily: Sendable { public struct ProviderUsageNotesContext: Sendable { public let snapshot: UsageSnapshot? public let isRefreshing: Bool - public let tokenCostInlineDashboardEnabled: Bool + public let costSummaryInlineEnabled: Bool public let showOptionalUsage: Bool public init( snapshot: UsageSnapshot?, isRefreshing: Bool, - tokenCostInlineDashboardEnabled: Bool, + costSummaryInlineEnabled: Bool, showOptionalUsage: Bool) { self.snapshot = snapshot self.isRefreshing = isRefreshing - self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled + self.costSummaryInlineEnabled = costSummaryInlineEnabled self.showOptionalUsage = showOptionalUsage } } diff --git a/Sources/CodexBarCore/Providers/XAI/XAIProviderDescriptor.swift b/Sources/CodexBarCore/Providers/XAI/XAIProviderDescriptor.swift index 019e0a052f..d01ae70a65 100644 --- a/Sources/CodexBarCore/Providers/XAI/XAIProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/XAI/XAIProviderDescriptor.swift @@ -53,7 +53,9 @@ public enum XAIProviderDescriptor { let showsFallback = snapshot.providerCost?.period != "Prepaid credits" let style: ProviderCostMenuCardStyle = showsFallback ? .generic : .prepaidCredits return ProviderCostPresentation(showsGenericFallback: showsFallback, menuCardStyle: style) - }), + }, + optionalDetails: ProviderOptionalDetailsPresentation( + costSummaryTitles: ["Billing summary"])), fetchPlan: self.fetchPlan(), cli: ProviderCLIConfig( name: "xai", diff --git a/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift index 2a9c886eed..cd50e09bc5 100644 --- a/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift @@ -113,7 +113,9 @@ public enum ZaiProviderDescriptor { context.snapshot.primary, context.snapshot.secondary)) }, - primaryBindingQuotaLanes: [.secondary]), + primaryBindingQuotaLanes: [.secondary], + optionalDetails: ProviderOptionalDetailsPresentation( + costSummaryTitles: ["Hourly tokens", "Daily tokens"])), fetchPlan: self.fetchPlan(), cli: ProviderCLIConfig( name: "zai", diff --git a/Tests/CodexBarTests/AiAndProviderTests.swift b/Tests/CodexBarTests/AiAndProviderTests.swift index 5fe70ee869..4737740f05 100644 --- a/Tests/CodexBarTests/AiAndProviderTests.swift +++ b/Tests/CodexBarTests/AiAndProviderTests.swift @@ -321,6 +321,7 @@ struct AiAndProviderTests { usageBarsShowUsed: true, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/ClaudeAdminAPIInlineDashboardModelTests.swift b/Tests/CodexBarTests/ClaudeAdminAPIInlineDashboardModelTests.swift index 12ec2bbc81..4a91602df9 100644 --- a/Tests/CodexBarTests/ClaudeAdminAPIInlineDashboardModelTests.swift +++ b/Tests/CodexBarTests/ClaudeAdminAPIInlineDashboardModelTests.swift @@ -52,6 +52,7 @@ struct ClaudeAdminAPIInlineDashboardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/ClaudeMenuCardCostTests.swift b/Tests/CodexBarTests/ClaudeMenuCardCostTests.swift index 26a2794e12..7c34ba6fbe 100644 --- a/Tests/CodexBarTests/ClaudeMenuCardCostTests.swift +++ b/Tests/CodexBarTests/ClaudeMenuCardCostTests.swift @@ -132,6 +132,7 @@ struct ClaudeMenuCardCostTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: false, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/GroqMenuCardModelTests.swift b/Tests/CodexBarTests/GroqMenuCardModelTests.swift index 8c4488369e..b0d2eb3319 100644 --- a/Tests/CodexBarTests/GroqMenuCardModelTests.swift +++ b/Tests/CodexBarTests/GroqMenuCardModelTests.swift @@ -5,7 +5,7 @@ import Testing extension StatusMenuTests { @Test - func `groq cost data stays reachable via inline dashboard regardless of cost row gating`() throws { + func `groq cost details follow submenu only display style`() throws { StatusItemController.menuCardRenderingEnabled = true StatusItemController.setMenuRefreshEnabledForTesting(false) defer { self.disableMenuCardsForTesting() } @@ -51,16 +51,9 @@ extension StatusMenuTests { statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } - // Groq's descriptor sets `tokenCost.supportsTokenCost = false`, so the generic Cost - // row/submenu is unreachable regardless of display style or `tokenCostMenuSectionEnabled` - // (that guard runs first in `tokenUsageSection`) — Groq relies solely on the inline - // dashboard for its cost data, same as openai/mistral. This locks in that Groq's absence - // from the "Cost" row is unaffected by which provider set gates that row, so a future - // predicate change there can't silently break Groq the way it silently broke when this - // row's gate briefly reused `usesProviderCostHistoryAsPrimaryDashboard`. let model = try #require(controller.menuCardModel(for: .groq)) #expect(model.tokenUsage == nil) #expect(model.inlineUsageDashboard == nil) - #expect(model.providerDetails.first?.chart?.title == "Daily spend") + #expect(model.providerDetails.isEmpty) } } diff --git a/Tests/CodexBarTests/LiteLLMMenuCardModelTests.swift b/Tests/CodexBarTests/LiteLLMMenuCardModelTests.swift index 668ee31c16..ee4e656eee 100644 --- a/Tests/CodexBarTests/LiteLLMMenuCardModelTests.swift +++ b/Tests/CodexBarTests/LiteLLMMenuCardModelTests.swift @@ -171,6 +171,7 @@ struct LiteLLMMenuCardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/MenuCardModelTests.swift b/Tests/CodexBarTests/MenuCardModelTests.swift index eed2fc2e12..150c827d74 100644 --- a/Tests/CodexBarTests/MenuCardModelTests.swift +++ b/Tests/CodexBarTests/MenuCardModelTests.swift @@ -308,7 +308,7 @@ struct ProviderInlineDashboardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, - tokenCostInlineDashboardEnabled: true, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) @@ -421,6 +421,7 @@ struct ProviderInlineDashboardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/MiniMaxMenuCardBillingTests.swift b/Tests/CodexBarTests/MiniMaxMenuCardBillingTests.swift index ef898c9933..f49300c005 100644 --- a/Tests/CodexBarTests/MiniMaxMenuCardBillingTests.swift +++ b/Tests/CodexBarTests/MiniMaxMenuCardBillingTests.swift @@ -59,6 +59,7 @@ struct MiniMaxMenuCardBillingTests { usageBarsShowUsed: true, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) @@ -84,6 +85,7 @@ struct MiniMaxMenuCardBillingTests { usageBarsShowUsed: true, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: false, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/MistralMenuCardModelTests.swift b/Tests/CodexBarTests/MistralMenuCardModelTests.swift index 5726839e69..88f47180e8 100644 --- a/Tests/CodexBarTests/MistralMenuCardModelTests.swift +++ b/Tests/CodexBarTests/MistralMenuCardModelTests.swift @@ -43,6 +43,7 @@ struct MistralMenuCardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) @@ -104,6 +105,7 @@ struct MistralMenuCardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) @@ -157,6 +159,7 @@ struct MistralMenuCardModelTests { usageBarsShowUsed: true, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/OpenAIAPIMenuCardModelTests.swift b/Tests/CodexBarTests/OpenAIAPIMenuCardModelTests.swift index 5d235db6fe..20781edbe7 100644 --- a/Tests/CodexBarTests/OpenAIAPIMenuCardModelTests.swift +++ b/Tests/CodexBarTests/OpenAIAPIMenuCardModelTests.swift @@ -52,7 +52,7 @@ struct OpenAIAPIMenuCardModelTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, - tokenCostInlineDashboardEnabled: true, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/OpenAIAPIStatusMenuTests.swift b/Tests/CodexBarTests/OpenAIAPIStatusMenuTests.swift index 86ed7c165a..b01bfe5fef 100644 --- a/Tests/CodexBarTests/OpenAIAPIStatusMenuTests.swift +++ b/Tests/CodexBarTests/OpenAIAPIStatusMenuTests.swift @@ -5,7 +5,7 @@ import Testing extension StatusMenuTests { @Test - func `open AI API primary dashboard ignores optional cost summary toggle`() throws { + func `open AI API primary dashboard follows cost summary toggle`() throws { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false @@ -50,7 +50,7 @@ extension StatusMenuTests { defer { controller.releaseStatusItemsForTesting() } let model = try #require(controller.menuCardModel(for: .openai)) - #expect(model.inlineUsageDashboard != nil) + #expect(model.inlineUsageDashboard == nil) #expect(model.tokenUsage == nil) } @@ -155,7 +155,7 @@ extension StatusMenuTests { } @Test - func `mistral native billing submenus ignore optional local cost preferences`() throws { + func `mistral native billing follows cost summary preferences`() throws { StatusItemController.menuCardRenderingEnabled = true StatusItemController.setMenuRefreshEnabledForTesting(false) defer { self.disableMenuCardsForTesting() } @@ -207,14 +207,14 @@ extension StatusMenuTests { defer { controller.releaseStatusItemsForTesting() } let model = try #require(controller.menuCardModel(for: .mistral)) - #expect(model.inlineUsageDashboard != nil) + #expect(model.inlineUsageDashboard == nil) #expect(model.tokenUsage == nil) - #expect(controller.makeOverviewRowSubmenu(provider: .mistral, model: model, width: 320) != nil) + #expect(controller.makeOverviewRowSubmenu(provider: .mistral, model: model, width: 320) == nil) let menu = controller.makeMenu(for: .mistral) controller.menuWillOpen(menu) let usageItem = menu.items.first { ($0.representedObject as? String) == "menuCardUsage" } - #expect(usageItem?.submenu != nil) + #expect(usageItem?.submenu == nil) settings.costUsageEnabled = true settings.costSummaryDisplayStyle = .both diff --git a/Tests/CodexBarTests/OpenCodeGoMenuCardModelTests.swift b/Tests/CodexBarTests/OpenCodeGoMenuCardModelTests.swift index c211d03899..7f83357f5c 100644 --- a/Tests/CodexBarTests/OpenCodeGoMenuCardModelTests.swift +++ b/Tests/CodexBarTests/OpenCodeGoMenuCardModelTests.swift @@ -185,7 +185,7 @@ struct OpenCodeGoMenuCardModelTests { @Test func `inline dashboard falls back to inline chart when cost row is unavailable`() throws { // "Inline only" cost display style: tokenCostMenuSectionEnabled is false (no Cost row), - // but tokenCostInlineDashboardEnabled is true. OpenCode Go should behave like + // but costSummaryInlineEnabled is true. OpenCode Go should behave like // Codex/Claude/Cursor here and still surface its cost history via the inline chart. let now = Date() let snapshot = UsageSnapshot( @@ -229,7 +229,7 @@ struct OpenCodeGoMenuCardModelTests { usageBarsShowUsed: true, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: true, - tokenCostInlineDashboardEnabled: true, + costSummaryInlineEnabled: true, tokenCostMenuSectionEnabled: false, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, @@ -285,7 +285,7 @@ struct OpenCodeGoMenuCardModelTests { usageBarsShowUsed: true, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: true, - tokenCostInlineDashboardEnabled: true, + costSummaryInlineEnabled: true, tokenCostMenuSectionEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, diff --git a/Tests/CodexBarTests/OpenCodeMenuCardCostTests.swift b/Tests/CodexBarTests/OpenCodeMenuCardCostTests.swift index 30a4fa3c3e..4fd37cddb2 100644 --- a/Tests/CodexBarTests/OpenCodeMenuCardCostTests.swift +++ b/Tests/CodexBarTests/OpenCodeMenuCardCostTests.swift @@ -25,6 +25,7 @@ struct OpenCodeMenuCardCostTests { usageBarsShowUsed: false, resetTimeDisplayStyle: .countdown, tokenCostUsageEnabled: false, + costSummaryInlineEnabled: true, showOptionalCreditsAndExtraUsage: true, hidePersonalInfo: false, now: now)) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 437597b8dd..7fd363cb10 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -884,13 +884,13 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact provider-owned construct passes a fixed identity to shared infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SettingsStore+MenuObservation.swift", - line: 97, + line: 98, anchor: "_ = self[providerConfig: .synthetic, field: .apiKey]", expectedProviderIDs: ["synthetic"], reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), SuppressedProviderReference( path: "Sources/CodexBar/SettingsStore+MenuObservation.swift", - line: 116, + line: 117, anchor: "_ = self[providerConfig: .warp, field: .apiKey]", expectedProviderIDs: ["warp"], reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), @@ -1263,13 +1263,13 @@ struct ProviderArchitectureGatekeeperTests { reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 285, + line: 286, anchor: "return self.tokenAccountSnapshotCacheKey(provider: .claude, account: account)", expectedProviderIDs: ["claude"], reason: "Claude widget quota ownership uses the selected Claude account's isolated snapshot key."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 289, + line: 290, anchor: "provider: .claude,", expectedProviderIDs: ["claude"], reason: "Claude widget quota ownership uses the selected Claude account's isolated snapshot key."), @@ -1745,7 +1745,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/InlineUsageDashboardContent.swift", - line: 260, + line: 261, anchor: "if provider == .cursor, let meteredCostUSD = snapshot.meteredCostUSD {", expectedProviderIDs: ["cursor"], expectedReferenceCount: 1, @@ -1785,7 +1785,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+Costs.swift", - line: 175, + line: 194, anchor: "let sessionLabel = if provider == .bedrock || provider == .mistral {", expectedProviderIDs: ["bedrock", "mistral"], expectedReferenceCount: 2, @@ -1793,7 +1793,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+Costs.swift", - line: 198, + line: 217, anchor: "} else if provider == .mistral,", expectedProviderIDs: ["mistral"], expectedReferenceCount: 1, @@ -1801,7 +1801,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+Costs.swift", - line: 452, + line: 471, anchor: "if style == .claude {", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -1934,7 +1934,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1044, + line: 1055, anchor: "if input.provider == .sub2api {", expectedProviderIDs: ["sub2api"], expectedReferenceCount: 1, @@ -1942,7 +1942,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The sub2api menu card localizes and groups provider-owned usage detail rows for display."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1111, + line: 1122, anchor: "if provider == .kiro,", expectedProviderIDs: ["kilo", "kiro"], expectedReferenceCount: 2, @@ -1950,7 +1950,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1134, + line: 1145, anchor: "if provider == .minimax {", expectedProviderIDs: ["codex", "minimax"], expectedReferenceCount: 2, @@ -1958,7 +1958,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1169, + line: 1180, anchor: "guard let loginMethod = snapshot?.loginMethod(for: .kilo) else {", expectedProviderIDs: ["kilo"], expectedReferenceCount: 1, @@ -1966,7 +1966,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1253, + line: 1264, anchor: "if input.provider == .antigravity {", expectedProviderIDs: ["antigravity", "mistral"], expectedReferenceCount: 2, @@ -1974,7 +1974,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1273, + line: 1284, anchor: "if input.provider == .codex, let codexProjection = input.codexProjection {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -1982,7 +1982,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1289, + line: 1300, anchor: "if input.provider != .codex, let weekly = snapshot.secondary {", expectedProviderIDs: ["alibaba", "alibabatokenplan", "codex", "perplexity", "sub2api"], expectedReferenceCount: 5, @@ -1996,7 +1996,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1328, + line: 1339, anchor: "if input.provider == .kilo || input.provider == .kimi,", expectedProviderIDs: ["kilo", "kimi"], expectedReferenceCount: 2, @@ -2004,7 +2004,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1424, + line: 1435, anchor: "var paceDetail = if input.provider == .kimi {", expectedProviderIDs: ["kimi"], expectedReferenceCount: 1, @@ -2012,7 +2012,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1440, + line: 1451, anchor: "if input.provider == .warp,", expectedProviderIDs: ["chutes", "kilo", "kiro", "litellm", "sub2api", "warp"], expectedReferenceCount: 6, @@ -2020,7 +2020,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1473, + line: 1484, anchor: "if input.provider == .alibaba || input.provider == .alibabatokenplan,", expectedProviderIDs: ["alibaba", "alibabatokenplan", "copilot", "crof", "manus", "perplexity", "zenmux"], expectedReferenceCount: 8, @@ -2037,7 +2037,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1514, + line: 1525, anchor: "if input.provider == .synthetic,", expectedProviderIDs: ["synthetic"], expectedReferenceCount: 1, @@ -2281,7 +2281,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/SettingsStore.swift", - line: 1069, + line: 1073, anchor: "if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {", expectedProviderIDs: ["factory", "minimax", "zai"], expectedReferenceCount: 8, @@ -3136,7 +3136,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 214, + line: 215, anchor: "(provider == .claude && (storedTokenSnapshot != nil || preservedClaudeUsage != nil))", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3144,7 +3144,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 234, + line: 235, anchor: "if provider == .codex, let snapshot {", expectedProviderIDs: ["claude", "codex", "devin"], expectedReferenceCount: 3, @@ -3152,7 +3152,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 284, + line: 285, anchor: "if let account = self.settings.effectiveSelectedTokenAccount(for: .claude) {", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3160,7 +3160,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 299, + line: 301, anchor: "guard let entry, entry.provider == .claude else { return nil }", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3168,7 +3168,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 338, + line: 347, anchor: "let sessionLabel = if provider == .bedrock || provider == .mistral {", expectedProviderIDs: ["bedrock", "codex", "mistral"], expectedReferenceCount: 4, @@ -3176,7 +3176,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 368, + line: 377, anchor: "if provider == .codex {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -3184,7 +3184,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 387, + line: 396, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3192,7 +3192,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 400, + line: 409, anchor: "if provider == .antigravity,", expectedProviderIDs: ["alibabatokenplan", "amp", "antigravity", "crof", "cursor", "doubao", "grok"], expectedReferenceCount: 8, @@ -3209,7 +3209,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 445, + line: 454, anchor: "let secondaryTitle = if provider == .amp {", expectedProviderIDs: ["alibabatokenplan", "amp"], expectedReferenceCount: 2, @@ -3217,7 +3217,15 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", - line: 470, + line: 479, + anchor: "if provider == .claude, self.settings.claudeModelScopedWeeklyUsageVisible {", + expectedProviderIDs: ["claude"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["claude@0"], + reason: "Claude's opt-in widget projection adds provider-owned model-scoped weekly quota rows."), + AllowedProviderConstruct( + path: "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", + line: 493, anchor: "if provider == .kimi {", expectedProviderIDs: ["kimi"], expectedReferenceCount: 1, diff --git a/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift b/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift index e3d63741ed..4782a31924 100644 --- a/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift +++ b/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift @@ -1035,7 +1035,7 @@ extension ProviderSettingsDescriptorTests { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-deepseek-account-usage") fixture.settings.showOptionalCreditsAndExtraUsage = true fixture.settings.costSummaryOption = .inlineSummary - #expect(fixture.settings.costSummaryShowsInlineDashboard(for: .deepseek)) + #expect(fixture.settings.costSummaryShowsInline(for: .deepseek)) fixture.settings.addTokenAccount(provider: .deepseek, label: "Personal", token: "token-1") fixture.settings.addTokenAccount(provider: .deepseek, label: "Work", token: "token-2") let accounts = fixture.settings.tokenAccounts(for: .deepseek) @@ -1051,15 +1051,15 @@ extension ProviderSettingsDescriptorTests { settings: fixture.settings, override: TokenAccountOverride(provider: .deepseek, account: inactive))) fixture.settings.costSummaryOption = .costSubmenu - #expect(fixture.settings.costSummaryShowsInlineDashboard(for: .deepseek)) + #expect(!fixture.settings.costSummaryShowsInline(for: .deepseek)) #expect(ProviderTokenAccountSelection.shouldIncludeOptionalUsage( provider: .deepseek, settings: fixture.settings, override: TokenAccountOverride(provider: .deepseek, account: active))) fixture.settings.costSummaryOption = .both - #expect(fixture.settings.costSummaryShowsInlineDashboard(for: .deepseek)) + #expect(fixture.settings.costSummaryShowsInline(for: .deepseek)) fixture.settings.costSummaryOption = .off - #expect(!fixture.settings.costSummaryShowsInlineDashboard(for: .deepseek)) + #expect(!fixture.settings.costSummaryShowsInline(for: .deepseek)) #expect(!ProviderTokenAccountSelection.shouldIncludeOptionalUsage( provider: .deepseek, settings: fixture.settings, diff --git a/Tests/CodexBarTests/ZaiMenuCardTests.swift b/Tests/CodexBarTests/ZaiMenuCardTests.swift index 77aedb3d3b..b33ecbb7ae 100644 --- a/Tests/CodexBarTests/ZaiMenuCardTests.swift +++ b/Tests/CodexBarTests/ZaiMenuCardTests.swift @@ -4,6 +4,22 @@ import Testing @testable import CodexBar struct ZaiMenuCardTests { + @MainActor + @Test + func `submenu only hides zai inline cost summary details`() throws { + let model = try Self.costSummaryModel(style: .costSubmenu) + + #expect(model.providerDetails.map(\.title) == ["Quota details"]) + } + + @MainActor + @Test + func `inline style keeps zai inline cost summary details`() throws { + let model = try Self.costSummaryModel(style: .inlineSummary) + + #expect(model.providerDetails.map(\.title) == ["Quota details", "Hourly tokens", "Daily tokens"]) + } + @Test func `zai metrics titles are 5-hour weekly and MCP when session token limit present`() throws { let now = Date() @@ -61,4 +77,65 @@ struct ZaiMenuCardTests { #expect(mcp.value == "50% used") #expect(mcp.secondaryValue == "100 limit · 50 remaining") } + + @MainActor + private static func costSummaryModel(style: CostSummaryDisplayStyle) throws -> UsageMenuCardView.Model { + let settings = testSettingsStore(suiteName: "ZaiMenuCardTests-cost-summary-\(style.rawValue)") + settings.costUsageEnabled = true + settings.costSummaryDisplayStyle = style + let now = Date() + let details = try [ + ProviderDetailSection( + title: "Quota details", + rows: [.init(label: "Token quota", value: "3% used")]), + ProviderDetailSection( + title: "Hourly tokens", + rows: [.init(label: "GLM-5.3", value: "18002346")], + chart: .init( + kind: .bars, + title: "Hourly tokens", + unit: "tokens", + points: [.init(label: "2026-08-16 10:00", value: 18_002_346)])), + ProviderDetailSection( + title: "Daily tokens", + rows: [.init(label: "GLM-5.3", value: "20883920")], + chart: .init( + kind: .bars, + title: "Daily tokens", + unit: "tokens", + points: [.init(label: "2026-08-16", value: 20_883_920)])), + ] + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 3, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: nil, + details: details, + updatedAt: now, + identity: ProviderIdentitySnapshot( + providerID: .zai, + accountEmail: nil, + accountOrganization: nil, + loginMethod: "Pro")) + let metadata = try #require(ProviderDefaults.metadata[.zai]) + + return UsageMenuCardView.Model.make(.init( + provider: .zai, + metadata: metadata, + snapshot: snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: nil, plan: nil), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + costSummaryInlineEnabled: settings.costSummaryShowsInline(for: .zai), + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + } }