From 8022b63ea4533fc1fe4268dd33b77a592ff081db Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:12:13 -0700 Subject: [PATCH 1/9] refactor: derive settings provider special cases --- .../Config/CodexBarConfigMigrator.swift | 7 ++++++ .../PreferencesProviderDetailView.swift | 17 +++++++------- .../CodexBar/PreferencesProvidersPane.swift | 22 ++++++++++++------- .../CopilotProviderImplementation.swift | 5 +++++ .../Shared/ProviderImplementation.swift | 7 ++++++ .../SettingsStore+ProviderDetection.swift | 22 ++++++++++++++----- .../SettingsStore+TokenAccounts.swift | 19 +++++++++++----- .../CodexBar/SettingsStore+TokenCost.swift | 3 ++- .../Claude/ClaudeProviderDescriptor.swift | 1 + .../Codex/CodexProviderDescriptor.swift | 3 ++- .../Copilot/CopilotProviderDescriptor.swift | 4 +++- .../Cursor/CursorProviderDescriptor.swift | 3 ++- .../MiMo/MiMoProviderDescriptor.swift | 3 ++- .../Moonshot/MoonshotProviderDescriptor.swift | 2 ++ .../OpenRouterProviderDescriptor.swift | 8 ++++--- .../Providers/Poe/PoeProviderDescriptor.swift | 5 +++-- .../Providers/ProviderDescriptor.swift | 5 ++++- .../Providers/ProviderUsagePresentation.swift | 21 +++++++++++++++++- .../Providers/Zai/ZaiProviderDescriptor.swift | 3 ++- .../CodexBarCore/TokenAccountSupport.swift | 15 +++++++++++++ 20 files changed, 135 insertions(+), 40 deletions(-) diff --git a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift index 018dac1146..08a344254c 100644 --- a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift +++ b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift @@ -102,6 +102,7 @@ struct CodexBarConfigMigrator { config: inout CodexBarConfig, state: inout MigrationState) { + // Provider-specific by design: this one-shot migration names the retired per-provider secret stores. self.migrateTokenProviders( [ (.zai, stores.zaiTokenStore.loadToken), @@ -133,6 +134,7 @@ struct CodexBarConfigMigrator { config: inout CodexBarConfig, state: inout MigrationState) { + // Provider-specific by design: these are the historical UserDefaults keys shipped before unified config. let sources: [(UsageProvider, String)] = [ (.codex, "codexCookieSource"), (.claude, "claudeCookieSource"), @@ -157,6 +159,7 @@ struct CodexBarConfigMigrator { } if userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool == false { + // Provider-specific by design: the retired OpenAI web toggle controlled Codex dashboard cookies. self.updateProvider(.codex, config: &config, state: &state) { entry in guard entry.cookieSource == nil else { return false } entry.cookieSource = .off @@ -169,6 +172,7 @@ struct CodexBarConfigMigrator { config: inout CodexBarConfig, state: inout MigrationState) { + // Provider-specific by design: old Moonshot API keys stored region separately from the unified config. self.updateProvider(.moonshot, config: &config, state: &state) { entry in guard entry.sanitizedAPIKey != nil, entry.sanitizedAPIKeyRegion == nil else { return false } entry.apiKeyRegion = entry.sanitizedRegion ?? MoonshotRegion.international.rawValue @@ -214,6 +218,7 @@ struct CodexBarConfigMigrator { config: inout CodexBarConfig, state: inout MigrationState) { + // Provider-specific by design: MiniMax formerly split API token, region, and cookie across legacy stores. let token = try? stores.minimaxAPITokenStore.loadToken() let header = try? stores.minimaxCookieStore.loadCookieHeader() if token != nil || header != nil { @@ -238,6 +243,7 @@ struct CodexBarConfigMigrator { config: inout CodexBarConfig, state: inout MigrationState) { + // Provider-specific by design: Kimi's legacy cookie could live in Keychain or kimiManualCookieHeader. var token = try? stores.kimiTokenStore.loadToken() if token?.isEmpty ?? true { token = userDefaults.string(forKey: "kimiManualCookieHeader") @@ -256,6 +262,7 @@ struct CodexBarConfigMigrator { config: inout CodexBarConfig, state: inout MigrationState) { + // Provider-specific by design: OpenCode's retired store paired its cookie with opencodeWorkspaceID. let header = try? stores.opencodeCookieStore.loadCookieHeader() if header != nil { state.sawLegacySecrets = true diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index 42311b5236..d4280c230c 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -86,8 +86,9 @@ struct ProviderDetailView: View { else { return nil } - guard provider == .openrouter || provider == .mimo || provider == .moonshot || provider == .poe else { - return (label: L("Plan"), value: rawPlan) + let presentation = ProviderDescriptorRegistry.descriptor(for: provider).presentation.planRow + guard presentation.stripsBalancePrefix else { + return (label: L(presentation.label), value: rawPlan) } let prefix = "Balance:" @@ -95,13 +96,10 @@ struct ProviderDetailView: View { let valueStart = rawPlan.index(rawPlan.startIndex, offsetBy: prefix.count) let trimmedValue = rawPlan[valueStart...].trimmingCharacters(in: .whitespacesAndNewlines) if !trimmedValue.isEmpty { - return (label: L("Balance"), value: trimmedValue) + return (label: L(presentation.balancePrefixedLabel), value: trimmedValue) } } - if provider == .mimo { - return (label: L("Plan"), value: rawPlan) - } - return (label: L("Balance"), value: rawPlan) + return (label: L(presentation.label), value: rawPlan) } private var menuBarSettingsPickers: [ProviderSettingsPickerDescriptor] { @@ -431,7 +429,10 @@ struct ProviderMetricsInlineView: View { title: L("Cost"), value: tokenUsage.sessionLine) ProviderMetricInlineTextRow(title: "", value: tokenUsage.monthLine) - if self.model.provider == .codex, let hint = tokenUsage.hintLine, !hint.isEmpty { + if ProviderDescriptorRegistry.descriptor(for: self.model.provider).tokenCost.showsHintInProviderDetails, + let hint = tokenUsage.hintLine, + !hint.isEmpty + { ProviderMetricInlineTextRow(title: "", value: hint) } } diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index fb90edc9e3..c67c3967ad 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -151,6 +151,7 @@ struct ProvidersPane: View { private func triggerRefresh(for provider: UsageProvider) { Task { @MainActor in await ProviderSettingsRefreshInteraction.perform { + // Provider-specific by design: Codex account reconciliation must refresh managed profile state too. if provider == .codex { await self.store.refreshCodexAccountScopedState(allowDisabled: true) } else { @@ -222,6 +223,8 @@ struct ProvidersPane: View { } func codexAccountsSectionState(for provider: UsageProvider) -> CodexAccountsSectionState? { + // Provider-specific by design: managed Codex profiles own app-only account promotion and reauthentication + // state. guard provider == .codex else { return nil } let projection = self.settings.codexVisibleAccountProjection let degradedNotice: CodexAccountsSectionNotice? = if projection.hasUnreadableAddedAccountStore { @@ -390,6 +393,7 @@ struct ProvidersPane: View { func tokenAccountDescriptor(for provider: UsageProvider) -> ProviderSettingsTokenAccountsDescriptor? { guard let support = TokenAccountSupportCatalog.support(for: provider) else { return nil } let context = self.makeSettingsContext(provider: provider) + let implementation = ProviderCatalog.implementation(for: provider) return ProviderSettingsTokenAccountsDescriptor( id: "token-accounts-\(provider.rawValue)", title: support.title, @@ -415,8 +419,8 @@ struct ProvidersPane: View { } } }, - showsOrganizationField: provider == .claude, - showsTeamModeControls: provider == .zai, + showsOrganizationField: support.showsOrganizationField, + showsTeamModeControls: support.showsTeamModeControls, addAccount: { label, token, usageScope, organizationID, workspaceID in self.settings.addTokenAccount( provider: provider, @@ -452,13 +456,13 @@ struct ProvidersPane: View { } } }, - primaryAddActionTitle: provider == .copilot ? "Add Account" : nil, - primaryAddAction: provider == .copilot ? { - await CopilotLoginFlow.run(settings: self.settings) + primaryAddActionTitle: support.primaryAddActionTitle, + primaryAddAction: support.primaryAddActionTitle.map { _ in { + await implementation?.runTokenAccountPrimaryAction(context: context) await ProviderInteractionContext.$current.withValue(.userInitiated) { await self.store.refreshProvider(provider, allowDisabled: true) } - } : nil, + } }, openConfigFile: { self.settings.openTokenAccountsFile() }, @@ -552,8 +556,9 @@ struct ProvidersPane: View { tokenError = nil } - // Abacus and Kimi carry their long-cadence window in primary rather than secondary. - let paceWindow = provider == .abacus || provider == .kimi ? snapshot?.primary : snapshot?.secondary + let paceWindow = snapshot.flatMap { + ProviderDescriptorRegistry.descriptor(for: provider).presentation.semanticWindows(snapshot: $0).weekly + } let weeklyPace = if let codexProjection, let weekly = codexProjection.rateWindow(for: .weekly) { @@ -602,6 +607,7 @@ struct ProvidersPane: View { } func openAIWebDiagnostic(for provider: UsageProvider) -> String? { + // Provider-specific by design: the OpenAI dashboard diagnostic comes from Codex's app-only web session. guard provider == .codex else { return nil } let diagnostic = self.store.codexConsumerProjectionIfNeeded( for: provider, diff --git a/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift b/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift index 6b26386ecf..a7904b4ea8 100644 --- a/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift @@ -216,4 +216,9 @@ struct CopilotProviderImplementation: ProviderImplementation { await CopilotLoginFlow.run(settings: context.controller.settings) return true } + + @MainActor + func runTokenAccountPrimaryAction(context: ProviderSettingsContext) async { + await CopilotLoginFlow.run(settings: context.settings) + } } diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementation.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementation.swift index 65414f493a..3e6613ee91 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderImplementation.swift @@ -62,6 +62,10 @@ protocol ProviderImplementation: Sendable { @MainActor func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? + /// Optional primary action for the shared token-account editor. + @MainActor + func runTokenAccountPrimaryAction(context: ProviderSettingsContext) async + /// Optional hook to update provider settings when token accounts change. @MainActor func applyTokenAccountCookieSource(settings: SettingsStore) @@ -163,6 +167,9 @@ extension ProviderImplementation { ProviderDescriptorRegistry.descriptor(for: self.id).settingsSection.defaultContribution } + @MainActor + func runTokenAccountPrimaryAction(context _: ProviderSettingsContext) async {} + @MainActor func applyTokenAccountCookieSource(settings _: SettingsStore) {} diff --git a/Sources/CodexBar/SettingsStore+ProviderDetection.swift b/Sources/CodexBar/SettingsStore+ProviderDetection.swift index e2f35b3491..e8817a1bb0 100644 --- a/Sources/CodexBar/SettingsStore+ProviderDetection.swift +++ b/Sources/CodexBar/SettingsStore+ProviderDetection.swift @@ -13,14 +13,25 @@ enum ProviderDetectionPolicy { } static func enabledProviders(signals: Signals) -> Set { + // Provider-specific by design: first-run detection probes these four concrete CLI/app credential sources. var enabled: Set = [] - if signals.codexCLIInstalled { enabled.insert(.codex) } - if signals.claudeCLIInstalled || signals.claudeDesktopInstalled { enabled.insert(.claude) } - if signals.geminiCLIInstalled, signals.geminiConfigured { enabled.insert(.gemini) } - if signals.antigravityAvailable { enabled.insert(.antigravity) } + if signals.codexCLIInstalled { + enabled.insert(.codex) + } + if signals.claudeCLIInstalled || signals.claudeDesktopInstalled { + enabled.insert(.claude) + } + if signals.geminiCLIInstalled, signals.geminiConfigured { + enabled.insert(.gemini) + } + if signals.antigravityAvailable { + enabled.insert(.antigravity) + } // Keep the historical Codex default when no usable provider source is found. - if enabled.isEmpty { enabled.insert(.codex) } + if enabled.isEmpty { + enabled.insert(.codex) + } return enabled } } @@ -37,6 +48,7 @@ extension SettingsStore { func applyProviderDetection() async { guard !self.providerDetectionCompleted else { return } + // Provider-specific by design: detection reads each provider's installed app, CLI, or credential artifact. let codexCLIInstalled = BinaryLocator.resolveCodexBinary() != nil let claudeCLIInstalled = BinaryLocator.resolveClaudeBinary() != nil let claudeDesktopInstalled = NSWorkspace.shared.urlForApplication( diff --git a/Sources/CodexBar/SettingsStore+TokenAccounts.swift b/Sources/CodexBar/SettingsStore+TokenAccounts.swift index ac3b884b72..0c30964825 100644 --- a/Sources/CodexBar/SettingsStore+TokenAccounts.swift +++ b/Sources/CodexBar/SettingsStore+TokenAccounts.swift @@ -22,7 +22,10 @@ extension SettingsStore { /// Cursor keeps saved manual credentials when browser login switches back to Automatic, but those credentials /// stay passive until the user explicitly selects one again. func effectiveSelectedTokenAccount(for provider: UsageProvider) -> ProviderTokenAccount? { - if provider == .cursor, self.cursorCookieSource == .auto { + let support = TokenAccountSupportCatalog.support(for: provider) + if support?.selectedAccountRequiresManualCookieSource == true, + (self.providerConfig(for: provider)?.cookieSource ?? .auto) == .auto + { return nil } return self.selectedTokenAccount(for: provider) @@ -87,7 +90,7 @@ extension SettingsStore { activeIndex: accounts.count) self.updateProviderConfig(provider: provider) { entry in entry.tokenAccounts = updated - if provider == .copilot { + if TokenAccountSupportCatalog.support(for: provider)?.clearsAPIKeyOnMutation == true { entry.apiKey = nil } } @@ -115,7 +118,9 @@ extension SettingsStore { let trimmedLabel = label?.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) - if let trimmedToken, trimmedToken.isEmpty { return } + if let trimmedToken, trimmedToken.isEmpty { + return + } let existing = data.accounts[index] let resolvedIdentifier: String? @@ -165,7 +170,7 @@ extension SettingsStore { activeIndex: data.clampedActiveIndex()) self.updateProviderConfig(provider: provider) { entry in entry.tokenAccounts = updated - if provider == .copilot { + if TokenAccountSupportCatalog.support(for: provider)?.clearsAPIKeyOnMutation == true { entry.apiKey = nil } } @@ -200,7 +205,7 @@ extension SettingsStore { accounts: filtered, activeIndex: nextActiveIndex) } - if provider == .copilot { + if TokenAccountSupportCatalog.support(for: provider)?.clearsAPIKeyOnMutation == true { entry.apiKey = nil } } @@ -217,7 +222,9 @@ extension SettingsStore { } func ensureTokenAccountsLoaded() { - if self.tokenAccountsLoaded { return } + if self.tokenAccountsLoaded { + return + } self.tokenAccountsLoaded = true } diff --git a/Sources/CodexBar/SettingsStore+TokenCost.swift b/Sources/CodexBar/SettingsStore+TokenCost.swift index 2be94b6f5c..ec1bdb8f8b 100644 --- a/Sources/CodexBar/SettingsStore+TokenCost.swift +++ b/Sources/CodexBar/SettingsStore+TokenCost.swift @@ -3,7 +3,7 @@ import Foundation extension SettingsStore { func costSummaryShowsInlineDashboard(for provider: UsageProvider) -> Bool { - // DeepSeek has no cost submenu, so any enabled cost-summary style falls back to inline. + // Provider-specific by design: DeepSeek's API exposes a balance card but no token-cost submenu data. if provider == .deepseek { return self.costUsageEnabled } @@ -39,6 +39,7 @@ extension SettingsStore { homeDirectory: URL? = nil, workingDirectory: URL? = nil) -> Bool { + // Provider-specific by design: only Codex and Claude have local JSONL scanners that can auto-enable token cost. let home = homeDirectory ?? fileManager.homeDirectoryForCurrentUser func hasAnyJsonl(in root: URL) -> Bool { diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index f1e826b8e1..4d68d7cb04 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -18,6 +18,7 @@ public enum ClaudeProviderDescriptor { injection: .cookieHeader, requiresManualCookieSource: true, cookieName: "sessionKey", + showsOrganizationField: true, environmentOverride: { token in switch ClaudeCredentialRouting.resolve(tokenAccountToken: token, manualCookieHeader: nil) { case let .oauth(accessToken): diff --git a/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift index e053e6c38a..60994ae3d6 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift @@ -67,7 +67,8 @@ public enum CodexProviderDescriptor { supportsTokenCost: true, noDataMessage: self.noDataMessage, menuHintLines: [.localized("codex_api_estimate_hint")], - supportsTokenSnapshot: true), + supportsTokenSnapshot: true, + showsHintInProviderDetails: true), pace: ProviderPaceCapability( primary: .session(maximumMinutes: 300), secondary: .weekly, diff --git a/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift index c8e28b3863..84b5518cf1 100644 --- a/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift @@ -12,7 +12,9 @@ public enum CopilotProviderDescriptor { placeholder: "Paste GitHub token…", injection: .environment(key: "COPILOT_API_TOKEN"), requiresManualCookieSource: false, - cookieName: nil)) + cookieName: nil, + clearsAPIKeyOnMutation: true, + primaryAddActionTitle: "Add Account")) /// Budget imports stay Chrome-only to avoid prompting unrelated browsers. private static var browserCookieOrder: BrowserCookieImportOrder? { diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift index a25c447f36..0b4b292a67 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift @@ -9,7 +9,8 @@ public enum CursorProviderDescriptor { placeholder: "Cookie: …", injection: .cookieHeader, requiresManualCookieSource: true, - cookieName: nil)) + cookieName: nil, + selectedAccountRequiresManualCookieSource: true)) /// Active Cursor sessions often live only in Safari; Chromium profiles may carry stale tokens. private static var browserCookieOrder: BrowserCookieImportOrder? { diff --git a/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift index 935d92da43..910520bec2 100644 --- a/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift @@ -64,7 +64,8 @@ public enum MiMoProviderDescriptor { menuCard: ProviderMenuCardPresentation( showsPrimaryBalanceDescription: true, hidesPrimaryResetWithoutDate: true), - menu: ProviderMenuDescriptorPresentation(primaryDescriptionIsDetail: { _ in true })), + menu: ProviderMenuDescriptorPresentation(primaryDescriptionIsDetail: { _ in true }), + planRow: ProviderPlanRowPresentation(stripsBalancePrefix: true)), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .web], pipeline: ProviderFetchPipeline(resolveStrategies: { context in diff --git a/Sources/CodexBarCore/Providers/Moonshot/MoonshotProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Moonshot/MoonshotProviderDescriptor.swift index 77fe7c160e..ba61899aaf 100644 --- a/Sources/CodexBarCore/Providers/Moonshot/MoonshotProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Moonshot/MoonshotProviderDescriptor.swift @@ -69,6 +69,8 @@ public enum MoonshotProviderDescriptor { tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Moonshot / Kimi Open Platform cost summary is not available." }), + presentation: ProviderUsagePresentation( + planRow: ProviderPlanRowPresentation(label: "Balance", stripsBalancePrefix: true)), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .api], pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [MoonshotAPIFetchStrategy()] })), diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift index 82fb9b5566..21c6b0889d 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift @@ -64,9 +64,11 @@ public enum OpenRouterProviderDescriptor { tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "OpenRouter cost summary is not yet supported." }), - presentation: ProviderUsagePresentation(menuCard: ProviderMenuCardPresentation( - showsCreditsSection: false, - primaryDescriptionPlacement: .reset)), + presentation: ProviderUsagePresentation( + menuCard: ProviderMenuCardPresentation( + showsCreditsSection: false, + primaryDescriptionPlacement: .reset), + planRow: ProviderPlanRowPresentation(label: "Balance", stripsBalancePrefix: true)), fetchPlan: self.fetchPlan(), cli: ProviderCLIConfig( name: "openrouter", diff --git a/Sources/CodexBarCore/Providers/Poe/PoeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Poe/PoeProviderDescriptor.swift index 957bf48f54..4fa924d884 100644 --- a/Sources/CodexBarCore/Providers/Poe/PoeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Poe/PoeProviderDescriptor.swift @@ -42,8 +42,9 @@ public enum PoeProviderDescriptor { tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Poe usage history is unavailable." }), - presentation: ProviderUsagePresentation(menuCard: ProviderMenuCardPresentation( - primaryDetailKind: .poeBalance)), + presentation: ProviderUsagePresentation( + menuCard: ProviderMenuCardPresentation(primaryDetailKind: .poeBalance), + planRow: ProviderPlanRowPresentation(label: "Balance", stripsBalancePrefix: true)), fetchPlan: self.fetchPlan(), cli: ProviderCLIConfig( name: "poe", diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift index 2995a74d3e..71e855a940 100644 --- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift @@ -11,17 +11,20 @@ public struct ProviderTokenCostConfig: Sendable { public let noDataMessage: @Sendable () -> String public let menuHintLines: [ProviderTokenCostHint] public let supportsTokenSnapshot: Bool + public let showsHintInProviderDetails: Bool public init( supportsTokenCost: Bool, noDataMessage: @escaping @Sendable () -> String, menuHintLines: [ProviderTokenCostHint] = [], - supportsTokenSnapshot: Bool = false) + supportsTokenSnapshot: Bool = false, + showsHintInProviderDetails: Bool = false) { self.supportsTokenCost = supportsTokenCost self.noDataMessage = noDataMessage self.menuHintLines = menuHintLines self.supportsTokenSnapshot = supportsTokenSnapshot + self.showsHintInProviderDetails = showsHintInProviderDetails } } diff --git a/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift b/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift index bf0415ea7f..4a36a7eb57 100644 --- a/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift +++ b/Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift @@ -36,6 +36,22 @@ public struct ProviderIdentityPresentation: Sendable, Equatable { } } +public struct ProviderPlanRowPresentation: Sendable, Equatable { + public let label: String + public let balancePrefixedLabel: String + public let stripsBalancePrefix: Bool + + public init( + label: String = "Plan", + balancePrefixedLabel: String = "Balance", + stripsBalancePrefix: Bool = false) + { + self.label = label + self.balancePrefixedLabel = balancePrefixedLabel + self.stripsBalancePrefix = stripsBalancePrefix + } +} + public struct ProviderCostPresentation: Sendable, Equatable { public struct Balance: Sendable, Equatable { public let label: String @@ -377,6 +393,7 @@ public struct ProviderUsagePresentation: Sendable { public let secondaryGloballyCapsPrimary: Bool public let menuCard: ProviderMenuCardPresentation public let menu: ProviderMenuDescriptorPresentation + public let planRow: ProviderPlanRowPresentation public init( rateWindowLabeler: RateWindowLabeler? = nil, @@ -400,7 +417,8 @@ public struct ProviderUsagePresentation: Sendable { widgetRowLimitResolver: @escaping WidgetRowLimitResolver = { _, _ in nil }, secondaryGloballyCapsPrimary: Bool = false, menuCard: ProviderMenuCardPresentation = ProviderMenuCardPresentation(), - menu: ProviderMenuDescriptorPresentation = ProviderMenuDescriptorPresentation()) + menu: ProviderMenuDescriptorPresentation = ProviderMenuDescriptorPresentation(), + planRow: ProviderPlanRowPresentation = ProviderPlanRowPresentation()) { self.rateWindowLabeler = rateWindowLabeler self.identityPresenter = identityPresenter @@ -422,6 +440,7 @@ public struct ProviderUsagePresentation: Sendable { self.secondaryGloballyCapsPrimary = secondaryGloballyCapsPrimary self.menuCard = menuCard self.menu = menu + self.planRow = planRow } public func rateWindowLabels( diff --git a/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift index 4ec7146ce5..b2062936c7 100644 --- a/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift @@ -11,7 +11,8 @@ public enum ZaiProviderDescriptor { placeholder: "Paste token…", injection: .environment(key: ZaiSettingsReader.apiTokenKey), requiresManualCookieSource: false, - cookieName: nil), + cookieName: nil, + showsTeamModeControls: true), usesRegion: true, configValidator: { config in var issues = ProviderCredentialAdapter.regionValidator( diff --git a/Sources/CodexBarCore/TokenAccountSupport.swift b/Sources/CodexBarCore/TokenAccountSupport.swift index 2713e77de4..9d4cfeef8b 100644 --- a/Sources/CodexBarCore/TokenAccountSupport.swift +++ b/Sources/CodexBarCore/TokenAccountSupport.swift @@ -14,6 +14,11 @@ public struct TokenAccountSupport: Sendable { public let cookieName: String? public let environmentKeysToScrub: [String] public let minimumDelayBetweenAccountRefreshes: Duration? + public let selectedAccountRequiresManualCookieSource: Bool + public let clearsAPIKeyOnMutation: Bool + public let showsOrganizationField: Bool + public let showsTeamModeControls: Bool + public let primaryAddActionTitle: String? private let environmentOverride: @Sendable (String) -> [String: String]? private let environmentScrubber: @Sendable (inout [String: String], String) -> Void private let cookieHeaderNormalizer: @Sendable (String) -> String @@ -27,6 +32,11 @@ public struct TokenAccountSupport: Sendable { cookieName: String?, environmentKeysToScrub: [String] = [], minimumDelayBetweenAccountRefreshes: Duration? = nil, + selectedAccountRequiresManualCookieSource: Bool = false, + clearsAPIKeyOnMutation: Bool = false, + showsOrganizationField: Bool = false, + showsTeamModeControls: Bool = false, + primaryAddActionTitle: String? = nil, environmentOverride: (@Sendable (String) -> [String: String]?)? = nil, environmentScrubber: (@Sendable (inout [String: String], String) -> Void)? = nil, cookieHeaderNormalizer: (@Sendable (String) -> String)? = nil) @@ -39,6 +49,11 @@ public struct TokenAccountSupport: Sendable { self.cookieName = cookieName self.environmentKeysToScrub = environmentKeysToScrub self.minimumDelayBetweenAccountRefreshes = minimumDelayBetweenAccountRefreshes + self.selectedAccountRequiresManualCookieSource = selectedAccountRequiresManualCookieSource + self.clearsAPIKeyOnMutation = clearsAPIKeyOnMutation + self.showsOrganizationField = showsOrganizationField + self.showsTeamModeControls = showsTeamModeControls + self.primaryAddActionTitle = primaryAddActionTitle self.environmentOverride = environmentOverride ?? { token in guard case let .environment(key) = injection else { return nil } return [key: token] From 58a08c8d56cf4d7c2eb0c1e4b0a99efed0c54ed2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:14:11 -0700 Subject: [PATCH 2/9] refactor: justify usage store provider state --- Sources/CodexBar/UsageStore+Accessors.swift | 6 ++++++ Sources/CodexBar/UsageStore+BackgroundRefresh.swift | 1 + Sources/CodexBar/UsageStore+HighestUsage.swift | 2 ++ Sources/CodexBar/UsageStore+HistoricalPace.swift | 5 ++--- Sources/CodexBar/UsageStore+OpenAIWeb.swift | 1 + Sources/CodexBar/UsageStore+PlanUtilization.swift | 1 + Sources/CodexBar/UsageStore+QuotaWarnings.swift | 2 ++ Sources/CodexBar/UsageStore+Refresh.swift | 2 ++ Sources/CodexBar/UsageStore+SessionEquivalents.swift | 3 +++ Sources/CodexBar/UsageStore+SessionQuotaTransition.swift | 1 + Sources/CodexBar/UsageStore+TokenAccounts.swift | 4 +++- Sources/CodexBar/UsageStore+TokenCost.swift | 2 ++ Sources/CodexBar/UsageStore+WidgetSnapshot.swift | 1 + Sources/CodexBar/UsageStore.swift | 1 + 14 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBar/UsageStore+Accessors.swift b/Sources/CodexBar/UsageStore+Accessors.swift index d065790cd5..1af86a2b3f 100644 --- a/Sources/CodexBar/UsageStore+Accessors.swift +++ b/Sources/CodexBar/UsageStore+Accessors.swift @@ -30,14 +30,17 @@ extension UsageStore { } var codexSnapshot: UsageSnapshot? { + // Provider-specific by design: dedicated Codex consumers require the reconciled primary-account snapshot. self.snapshots[.codex] } var claudeSnapshot: UsageSnapshot? { + // Provider-specific by design: Claude swap/account consumers require the active Claude snapshot directly. self.snapshots[.claude] } func presentationSnapshot(for provider: UsageProvider) -> UsageSnapshot? { + // Provider-specific by design: DeepSeek profile transitions and Codex dashboard attachment overlay live state. if provider == .deepseek, let transition = self.deepseekProfileTransition, transition.accountID == self.settings.selectedTokenAccount(for: .deepseek)?.id @@ -116,6 +119,7 @@ extension UsageStore { } var lastCodexError: String? { + // Provider-specific by design: Codex dashboard and credits surfaces expose separate app-owned error lanes. self.errors[.codex] } @@ -132,6 +136,7 @@ extension UsageStore { } var lastClaudeError: String? { + // Provider-specific by design: Claude swap/account surfaces consume the active Claude error lane directly. self.errors[.claude] } @@ -205,6 +210,7 @@ extension UsageStore { } let account: AccountInfo + // Provider-specific by design: Codex account info must be loaded through its selected filesystem scope. if provider == .codex { let env = ProviderRegistry.makeEnvironment( base: self.environmentBase, diff --git a/Sources/CodexBar/UsageStore+BackgroundRefresh.swift b/Sources/CodexBar/UsageStore+BackgroundRefresh.swift index fa046382f8..e24138ee63 100644 --- a/Sources/CodexBar/UsageStore+BackgroundRefresh.swift +++ b/Sources/CodexBar/UsageStore+BackgroundRefresh.swift @@ -31,6 +31,7 @@ extension UsageStore { self.lastKnownResetSnapshots.removeValue(forKey: provider.instanceID) self.errors[provider.instanceID] = nil self.diagnostics[provider.instanceID] = nil + // Provider-specific by design: disabling clears each provider's app-owned transient account/session state. if provider == .deepseek { self.clearDeepSeekProfileTransition() } diff --git a/Sources/CodexBar/UsageStore+HighestUsage.swift b/Sources/CodexBar/UsageStore+HighestUsage.swift index 6a9349b579..9fd7165a05 100644 --- a/Sources/CodexBar/UsageStore+HighestUsage.swift +++ b/Sources/CodexBar/UsageStore+HighestUsage.swift @@ -44,6 +44,7 @@ extension UsageStore { now: Date) -> RateWindow? { let effectivePreference = self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot) + // Provider-specific by design: these paths depend on live Codex projection and Antigravity user policy. if provider == .antigravity, effectivePreference == .automatic, !self.settings.antigravityPrioritizeExhaustedQuotas @@ -71,6 +72,7 @@ extension UsageStore { { let effectivePreference = self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot) guard metricPercent >= 100 else { return false } + // Provider-specific by design: exclusion mirrors each provider's multi-lane resolver and optional quotas. if provider == .codex || provider == .claude, effectivePreference == .primaryAndSecondary { if provider == .codex, self.codexConsumerProjection( diff --git a/Sources/CodexBar/UsageStore+HistoricalPace.swift b/Sources/CodexBar/UsageStore+HistoricalPace.swift index 360f544be0..b5e6d819b5 100644 --- a/Sources/CodexBar/UsageStore+HistoricalPace.swift +++ b/Sources/CodexBar/UsageStore+HistoricalPace.swift @@ -10,9 +10,8 @@ extension UsageStore { guard window.remainingPercent > 0 else { return nil } let resolved: UsagePace? let workDays = self.settings.weeklyProgressWorkDays - // Codex can refine pace with historical samples because its dashboard exposes enough weekly history to build - // an account-scoped usage curve. Other providers should not need a hard-coded allowlist: if their RateWindow - // includes a reset time and window duration, the generic linear pace calculation is already defensible. + // Provider-specific by design: only Codex's dashboard yields an account-scoped daily curve for learned pace; + // other providers use the generic linear window calculation. if provider == .codex, self.settings.historicalTrackingEnabled, workDays == nil { let codexAccountKey = self.codexOwnershipContext().canonicalKey if self.codexHistoricalDatasetAccountKey == codexAccountKey, diff --git a/Sources/CodexBar/UsageStore+OpenAIWeb.swift b/Sources/CodexBar/UsageStore+OpenAIWeb.swift index 063218eada..2c30e879a2 100644 --- a/Sources/CodexBar/UsageStore+OpenAIWeb.swift +++ b/Sources/CodexBar/UsageStore+OpenAIWeb.swift @@ -71,6 +71,7 @@ extension UsageStore { } func requestOpenAIDashboardRefreshIfStale(reason: String) { + // Provider-specific by design: the OpenAI WKWebView session lifecycle attaches only to Codex consumer usage. guard self.isEnabled(.codex), self.settings.openAIWebAccessEnabled, self.settings.codexCookieSource.isEnabled diff --git a/Sources/CodexBar/UsageStore+PlanUtilization.swift b/Sources/CodexBar/UsageStore+PlanUtilization.swift index 80be8bcb11..cdbd5f6bb3 100644 --- a/Sources/CodexBar/UsageStore+PlanUtilization.swift +++ b/Sources/CodexBar/UsageStore+PlanUtilization.swift @@ -62,6 +62,7 @@ extension UsageStore { return PlanUtilizationHistorySelection(accountKey: nil, histories: providerBuckets.histories(for: nil)) } var providerBuckets = self.planUtilizationHistory[provider.instanceID] ?? PlanUtilizationHistoryBuckets() + // Provider-specific by design: Claude OAuth provenance can outrank configured token-account selection. if provider == .claude, providerBuckets.preferredAccountKey == Self.planUtilizationUnscopedPreferredKey || Self.isClaudeOAuthPlanUtilizationAccountKey(providerBuckets.preferredAccountKey) diff --git a/Sources/CodexBar/UsageStore+QuotaWarnings.swift b/Sources/CodexBar/UsageStore+QuotaWarnings.swift index 23364e02d0..bd88c71965 100644 --- a/Sources/CodexBar/UsageStore+QuotaWarnings.swift +++ b/Sources/CodexBar/UsageStore+QuotaWarnings.swift @@ -60,6 +60,8 @@ extension UsageStore { let accountContext = QuotaWarningAccountContext( discriminator: accountDiscriminator, displayName: self.quotaWarningAccountDisplayName(provider: provider, snapshot: snapshot)) + // Provider-specific by design: warning lanes follow Antigravity families, balance-only suppression, and + // provider-authored dynamic labels rather than the generic primary/secondary pair. let source: SessionQuotaWindowSource? = if provider == .antigravity { Self.hasAntigravityQuotaSummaryWindows(snapshot: snapshot) ? .antigravityQuotaSummary diff --git a/Sources/CodexBar/UsageStore+Refresh.swift b/Sources/CodexBar/UsageStore+Refresh.swift index ff12670cd3..98c6538d69 100644 --- a/Sources/CodexBar/UsageStore+Refresh.swift +++ b/Sources/CodexBar/UsageStore+Refresh.swift @@ -81,6 +81,7 @@ extension UsageStore { if let tokenAccount { return self.warningTokenAccountDiscriminator(tokenAccount) } + // Provider-specific by design: Codex owner keys and Claude OAuth observations scope warning deduplication. if provider == .codex { return context.codexSessionQuotaOwnerKey?.rawValue } @@ -122,6 +123,7 @@ extension UsageStore { } func prepareRefreshState(for provider: UsageProvider? = nil) { + // Provider-specific by design: Codex active-source correction reconciles managed profile filesystem state. guard provider == nil || provider == .codex else { return } _ = self.settings.persistResolvedCodexActiveSourceCorrectionIfNeeded() } diff --git a/Sources/CodexBar/UsageStore+SessionEquivalents.swift b/Sources/CodexBar/UsageStore+SessionEquivalents.swift index d9123a26e4..b400d36820 100644 --- a/Sources/CodexBar/UsageStore+SessionEquivalents.swift +++ b/Sources/CodexBar/UsageStore+SessionEquivalents.swift @@ -30,6 +30,7 @@ extension UsageStore { private nonisolated static let unresolvedSessionEquivalentComponentIdentity = "__unresolved__" func planUtilizationWeeklyWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? { + // Provider-specific by design: Antigravity session equivalents aggregate named model-family quota windows. if provider == .antigravity { let namedWeeklyWindows = snapshot.extraRateWindows? .filter { @@ -64,6 +65,8 @@ extension UsageStore { func sessionEquivalentWindows(provider: UsageProvider, snapshot: UsageSnapshot) -> (session: RateWindow, weekly: RateWindow, weeklyWindowID: String?, historyIdentity: String?)? { + // Provider-specific by design: Antigravity family identity and Claude's fixed session/weekly pair preserve + // established burn-history identities across refreshes. if provider == .antigravity { return Self.antigravitySessionEquivalentWindows(snapshot: snapshot) } diff --git a/Sources/CodexBar/UsageStore+SessionQuotaTransition.swift b/Sources/CodexBar/UsageStore+SessionQuotaTransition.swift index 54bf524261..21fb514142 100644 --- a/Sources/CodexBar/UsageStore+SessionQuotaTransition.swift +++ b/Sources/CodexBar/UsageStore+SessionQuotaTransition.swift @@ -18,6 +18,7 @@ extension UsageStore { let notificationsEnabled = self.settings.sessionQuotaNotificationsEnabled let hooksActive = self.hasQuotaHookRule(event: .quotaReached, provider: provider) let detectionEnabled = notificationsEnabled || hooksActive + // Provider-specific by design: Codex owner-scoped baselines reject stale observations across account switches. if provider == .codex, !detectionEnabled { self.requireFreshCodexSessionQuotaBaseline(observedAt: snapshot.updatedAt) self.sessionQuotaLogger.debug("Codex session notifications disabled; cleared notification baseline") diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index 57f7dbed4b..ccc4313620 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -88,7 +88,9 @@ extension UsageStore { snapshot: UsageSnapshot, sourceLabel: String?) { - guard provider != .cursor || self.settings.cursorCookieSource != .auto else { return } + let support = TokenAccountSupportCatalog.support(for: provider) + let cookieSource = self.settings.providerConfig(for: provider)?.cookieSource ?? .auto + guard support?.selectedAccountRequiresManualCookieSource != true || cookieSource != .auto else { return } let cached = TokenAccountUsageSnapshot( account: account, snapshot: snapshot, diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index c1505d58c9..3c9950fc1f 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -25,6 +25,7 @@ extension UsageStore { } func prepareCursorCostCookie(for provider: UsageProvider) -> CursorCostCookiePreparation { + // Provider-specific by design: Cursor's dashboard cost fetch consumes its manually selected browser cookie. guard provider == .cursor, self.settings.cursorCookieSource == .manual else { return .proceed(nil) } @@ -53,6 +54,7 @@ extension UsageStore { let fetcher = self.costUsageFetcher let timeoutSeconds = self.tokenFetchTimeout + // Provider-specific by design: the Codex ledger owns pricing refresh while Bedrock resolves AWS environment. let allowPricingRefresh = provider != .codex || !self.settings.codexLocalSessionCostLedgerEnabled let environment = provider == .bedrock ? ProviderRegistry.makeEnvironment( diff --git a/Sources/CodexBar/UsageStore+WidgetSnapshot.swift b/Sources/CodexBar/UsageStore+WidgetSnapshot.swift index 1537b3fc35..732ffeb7a7 100644 --- a/Sources/CodexBar/UsageStore+WidgetSnapshot.swift +++ b/Sources/CodexBar/UsageStore+WidgetSnapshot.swift @@ -138,6 +138,7 @@ extension UsageStore { insert(account.externalIdentifier) insert(account.id.uuidString) } + // Provider-specific by design: Claude swap subprocesses and Codex managed profiles own extra account IDs. if provider == .claude { for accountSnapshot in self.claudeSwapAccountSnapshots { insert(accountSnapshot.snapshot?.identity?.accountID) diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index 7cdef76734..aaef0b6c07 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -110,6 +110,7 @@ extension UsageStore { /// Returns true if the Claude account appears to be a subscription (Max, Pro, Ultra, Team). /// Returns false for API users or when plan cannot be determined. func isClaudeSubscription() -> Bool { + // Provider-specific by design: Claude subscription plans choose its consumer dashboard account action. Self.isSubscriptionPlan(self.loginMethod(for: .claude)) } From 208c757de71c025e16928443228436da35c37236 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:16:38 -0700 Subject: [PATCH 3/9] refactor: derive controller provider policy --- .../CodexBar/SessionQuotaNotifications.swift | 4 ++ ...tusItemController+AccountMenuDisplay.swift | 2 + .../StatusItemController+Actions.swift | 4 ++ .../StatusItemController+Animation.swift | 40 ++++++++++++++----- .../CodexBar/StatusItemController+Menu.swift | 3 ++ .../StatusItemController+MenuCardModel.swift | 10 +++-- .../StatusItemController+MenuTracking.swift | 2 + .../Mistral/MistralProviderDescriptor.swift | 3 +- .../OpenAI/OpenAIAPIProviderDescriptor.swift | 3 +- .../Providers/ProviderDescriptor.swift | 5 ++- 10 files changed, 61 insertions(+), 15 deletions(-) diff --git a/Sources/CodexBar/SessionQuotaNotifications.swift b/Sources/CodexBar/SessionQuotaNotifications.swift index 21a1f2f0c0..94ed22cfee 100644 --- a/Sources/CodexBar/SessionQuotaNotifications.swift +++ b/Sources/CodexBar/SessionQuotaNotifications.swift @@ -171,6 +171,8 @@ enum SessionQuotaTransitionReducer { state: self.baselineState(observation: observation)) } + // Provider-specific by design: Codex restore detection is owner- and reset-boundary-scoped to reject stale + // account observations after a switch. let ownerChanged = observation.provider == .codex && previous.codexOwnerKey != observation.codexOwnerKey guard previous.source == observation.source, !ownerChanged else { return SessionQuotaTransitionEvaluation( @@ -407,6 +409,8 @@ extension UsageStore { provider: UsageProvider, snapshot: UsageSnapshot) -> (window: RateWindow, source: SessionQuotaWindowSource)? { + // Provider-specific by design: MiMo/Qoder balances, Crof PAYG, Antigravity families, and Copilot chat + // fallback encode distinct session-quota payload semantics. // MiMo/Qoder balances are never session quotas. Crof is handled below so quota-backed // Crof snapshots can still participate when a real request-quota window is present. guard provider != .mimo, provider != .qoder else { return nil } diff --git a/Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift b/Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift index aa40d7eb62..57a086371d 100644 --- a/Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift +++ b/Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift @@ -7,6 +7,7 @@ enum ClaudeSwapMenuPrecedence { accountCount: Int, showSingleAccount: Bool) -> Bool { + // Provider-specific by design: claude-swap subprocess discovery owns Claude account presentation. provider == .claude && ClaudeSwapAccountProjection.shouldPresentAccounts( accountCount: accountCount, showSingleAccount: showSingleAccount) @@ -97,6 +98,7 @@ extension StatusItemController { } func codexAccountMenuDisplay(for provider: UsageProvider) -> CodexAccountMenuDisplay? { + // Provider-specific by design: managed Codex profiles use reconciled visible-account projection state. guard provider == .codex else { return nil } guard let projection = self.settings.codexVisibleAccountProjectionForMenuDisplay else { return nil } guard projection.visibleAccounts.count > 1 else { return nil } diff --git a/Sources/CodexBar/StatusItemController+Actions.swift b/Sources/CodexBar/StatusItemController+Actions.swift index 09333684fd..7044636851 100644 --- a/Sources/CodexBar/StatusItemController+Actions.swift +++ b/Sources/CodexBar/StatusItemController+Actions.swift @@ -83,6 +83,7 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return } await self.store.refreshTokenUsageNow(for: provider, force: true) guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return } + // Provider-specific by design: Codex refresh also owns OpenAI dashboard and reset-credit enrichment. if provider == .codex { await self.store.refreshCreditsNow(minimumSnapshotUpdatedAt: refreshStartedAt) guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return } @@ -324,6 +325,7 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { } @objc func openDashboard() { + // Provider-specific by design: Codex remains the historical action fallback when no provider is selected. let preferred = self.lastMenuProvider?.firstPartyProvider ?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledFirstPartyProviders().first) @@ -336,6 +338,7 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { for provider: UsageProvider, environment: [String: String] = ProcessInfo.processInfo.environment) -> URL? { + // Provider-specific by design: these dashboards depend on region, source label, scope, or subscription plan. if provider == .alibaba { return self.settings.alibabaCodingPlanAPIRegion.dashboardURL } @@ -383,6 +386,7 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { } @objc func openCreditsPurchase() { + // Provider-specific by design: codexResetCredits payload supplies the validated ChatGPT purchase URL. let preferred = self.lastMenuProvider ?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledProviders().first) let provider = preferred ?? .codex diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 991e687a7c..9102cbb9f8 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -105,7 +105,9 @@ extension StatusItemController { guard let nextWakeAt else { return Self.blinkIdleFallbackInterval } let delay = nextWakeAt.timeIntervalSince(now) - if delay <= 0 { return Self.blinkActiveTickInterval } + if delay <= 0 { + return Self.blinkActiveTickInterval + } return .seconds(delay) } @@ -217,6 +219,7 @@ extension StatusItemController { } private func randomEffect(for provider: UsageProvider) -> MotionEffect { + // Provider-specific by design: Claude's star glyph uses wiggle rather than rotational tilt. if provider == .claude { Bool.random() ? .blink : .wiggle } else { @@ -225,8 +228,12 @@ extension StatusItemController { } private func isBlinkingAllowed(at date: Date = .init()) -> Bool { - if self.settings.randomBlinkEnabled { return true } - if let until = self.blinkForceUntil, until > date { return true } + if self.settings.randomBlinkEnabled { + return true + } + if let until = self.blinkForceUntil, until > date { + return true + } self.blinkForceUntil = nil return false } @@ -238,7 +245,10 @@ extension StatusItemController { { guard let button = self.statusItem.button else { return false } if !bypassMergedMenuTrackingDeferral, - self.deferMergedIconRenderDuringMenuTrackingIfNeeded() { return true } + self.deferMergedIconRenderDuringMenuTrackingIfNeeded() + { + return true + } let style = self.store.iconStyle let showUsed = self.settings.usageBarsShowUsed @@ -694,7 +704,9 @@ extension StatusItemController { func quotaWarningFlashActive(provider: UsageProvider, now: Date = Date()) -> Bool { guard let until = self.quotaWarningFlashUntil[provider.instanceID] else { return false } - if until > now { return true } + if until > now { + return true + } self.quotaWarningFlashUntil.removeValue(forKey: provider.instanceID) self.quotaWarningFlashTasks[provider.instanceID]?.cancel() self.quotaWarningFlashTasks.removeValue(forKey: provider.instanceID) @@ -847,6 +859,7 @@ extension StatusItemController { snapshot: UsageSnapshot?, now: Date = .init()) -> String? { + // Provider-specific by design: provider payload fields and display modes supply distinct balance/spend text. let mode = self.settings.menuBarDisplayMode if provider == .openrouter, self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot) == .automatic, @@ -1017,7 +1030,9 @@ extension StatusItemController { preference: MenuBarMetricPreference) -> String? { guard let snapshot, let detail = snapshot.detailRow(label: "Balance")?.value else { return nil } - if snapshot.primary != nil, preference != .secondary { return nil } + if snapshot.primary != nil, preference != .secondary { + return nil + } return detail.components(separatedBy: " (Paid:").first } @@ -1203,6 +1218,7 @@ extension StatusItemController { snapshot: UsageSnapshot?, projection: CodexConsumerProjection?) -> (session: RateWindow?, weekly: RateWindow?)? { + // Provider-specific by design: only Codex and Claude expose the combined session-and-weekly menu metric. guard provider == .codex || provider == .claude, self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot) == .primaryAndSecondary else { return nil } @@ -1276,7 +1292,9 @@ extension StatusItemController { guard let window = self.menuBarMetricWindow(for: provider, snapshot: snapshot, now: now) else { return [] } // Outside reset-time mode the reset text is only visible once the quota is exhausted. - if mode != .resetTime, window.remainingPercent > 0 { return [] } + if mode != .resetTime, window.remainingPercent > 0 { + return [] + } return window.resetsAt.map { [$0] } ?? [] } @@ -1432,7 +1450,9 @@ extension StatusItemController { } func shouldAnimate(provider: UsageProvider, mergeIcons: Bool? = nil) -> Bool { - if self.store.debugForceAnimation { return true } + if self.store.debugForceAnimation { + return true + } let isMerged = mergeIcons ?? self.shouldMergeIcons let isVisible = isMerged ? self.isEnabled(provider) : self.isVisible(provider) @@ -1442,7 +1462,9 @@ extension StatusItemController { // Animating the fallback causes unnecessary CPU usage (battery drain). See #269, #139. let isEnabled = self.isEnabled(provider) let isFallbackOnly = !isEnabled && self.fallbackProvider == provider - if isFallbackOnly { return false } + if isFallbackOnly { + return false + } let isStale = self.store.isStale(provider: provider) let hasSatisfiedUsageFetch = self.store.hasSatisfiedUsageFetch(for: provider) diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 6e571dbde2..6e061c116b 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -987,6 +987,7 @@ extension StatusItemController { } switch selection { case .overview: + // Provider-specific by design: Codex is the persisted fallback for an empty overview. self.lastMenuProvider = (provider ?? .codex).instanceID case let .provider(provider): self.lastMenuProvider = provider @@ -1061,6 +1062,7 @@ extension StatusItemController { @discardableResult private func handleCodexVisibleAccountSelection(_ account: CodexVisibleAccount, menu: NSMenu?) -> Bool { + // Provider-specific by design: managed Codex selection rebuilds after account-scoped reconciliation. let visibleAccountID = account.id self.advanceMenuInteraction(for: menu) self.settings.selectDisplayedCodexVisibleAccount(account) @@ -1560,6 +1562,7 @@ extension StatusItemController { /// Providers that surface the live component list as a native submenu. Every other provider /// keeps the plain "Status Page" link that opens the website. Kept deliberately small: these /// are the statuspage.io/incident.io feeds we actively curate and trust to render well. + /// Provider-specific by design: these four curated status feeds expose component trees rendered by the app. static let statusComponentsSubmenuProviders: Set = [.claude, .codex, .augment, .zoommate] /// Filters `components` down to a provider's descriptor-owned named allowlist, if configured; diff --git a/Sources/CodexBar/StatusItemController+MenuCardModel.swift b/Sources/CodexBar/StatusItemController+MenuCardModel.swift index d370b5e86d..cd6cd3d788 100644 --- a/Sources/CodexBar/StatusItemController+MenuCardModel.swift +++ b/Sources/CodexBar/StatusItemController+MenuCardModel.swift @@ -22,6 +22,7 @@ extension StatusItemController { planOverride: String? = nil, subtitleOverride: String? = nil) -> UsageMenuCardView.Model? { + // Provider-specific by design: Codex is the historical card fallback when no enabled provider is available. let target = provider ?? self.store.enabledFirstPartyProvidersForDisplay().first ?? .codex let metadata = self.store.metadata(for: target) @@ -135,7 +136,8 @@ extension StatusItemController { // provider-derived snapshot sourcing) and gain members for reasons that have nothing to // do with whether this row should show, silently disabling the Cost row for those // providers too (e.g. groq's addition to the inline-dashboard set previously did this). - tokenCostMenuSectionEnabled: target != .mistral && target != .openai && + tokenCostMenuSectionEnabled: ProviderDescriptorRegistry.descriptor(for: target).tokenCost + .showsCostMenuSection && self.settings.costSummaryShowsSubmenu(for: target), costComparisonPeriodsEnabled: self.settings.costComparisonPeriodsEnabled, showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage, @@ -177,6 +179,7 @@ extension StatusItemController { provider: UsageProvider, surface: CodexConsumerProjection.Surface) -> UsageSnapshot? { + // Provider-specific by design: OpenAI dashboard cache metadata attaches only to the live Codex account. guard provider == .codex, surface == .liveCard, let snapshot, @@ -202,8 +205,9 @@ extension StatusItemController { now: Date) -> (weeklyPace: UsagePace?, sessionEquivalentForecast: SessionEquivalentForecast?) { - let paceWindow = target == .abacus || target == .kimi - ? snapshot?.primary : snapshot?.secondary + let paceWindow = snapshot.flatMap { + ProviderDescriptorRegistry.descriptor(for: target).presentation.semanticWindows(snapshot: $0).weekly + } let historySelection = self.sessionEquivalentHistorySelection( provider: target, snapshot: snapshot, diff --git a/Sources/CodexBar/StatusItemController+MenuTracking.swift b/Sources/CodexBar/StatusItemController+MenuTracking.swift index b726802a6f..076a3a3f0a 100644 --- a/Sources/CodexBar/StatusItemController+MenuTracking.swift +++ b/Sources/CodexBar/StatusItemController+MenuTracking.swift @@ -339,6 +339,8 @@ extension StatusItemController { parts.append(self.providerIdentitySignature( self.store.snapshot(for: target.instanceID)?.identity(for: target.instanceID))) + // Provider-specific by design: Codex managed profiles and Claude swap accounts contribute extra identity + // sources beyond the generic provider snapshot/account fallback. if target != .codex, self.store.metadata(for: target).usesAccountFallback { let account = self.store.accountInfo(for: target) parts.append(Self.menuIdentityField(account.email)) diff --git a/Sources/CodexBarCore/Providers/Mistral/MistralProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Mistral/MistralProviderDescriptor.swift index f585fc5dc4..e6503ea537 100644 --- a/Sources/CodexBarCore/Providers/Mistral/MistralProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Mistral/MistralProviderDescriptor.swift @@ -58,7 +58,8 @@ public enum MistralProviderDescriptor { tokenCost: ProviderTokenCostConfig( supportsTokenCost: true, noDataMessage: { "Mistral cost history needs a billing web session." }, - menuHintLines: [.literal("Reported by Mistral billing usage.")]), + menuHintLines: [.literal("Reported by Mistral billing usage.")], + showsCostMenuSection: false), presentation: ProviderUsagePresentation(menuBarWindowResolver: { context in guard context.metric == .monthlyPlan else { return .unhandled } return .resolved(context.snapshot.extraRateWindows?.first { diff --git a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift index 62484d8bd0..2032eb20b4 100644 --- a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIProviderDescriptor.swift @@ -50,7 +50,8 @@ public enum OpenAIAPIProviderDescriptor { tokenCost: ProviderTokenCostConfig( supportsTokenCost: true, noDataMessage: { "OpenAI usage needs an Admin API key for organization usage." }, - menuHintLines: [.literal("Reported by OpenAI Admin API organization usage.")]), + menuHintLines: [.literal("Reported by OpenAI Admin API organization usage.")], + showsCostMenuSection: false), presentation: ProviderUsagePresentation(menuCard: ProviderMenuCardPresentation( usageNotesResolver: { context in context.snapshot?.openAIAPIUsage.map(ProviderUsageNotesResolution.openAIAPI) ?? .unhandled diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift index 71e855a940..e9b240c4ce 100644 --- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift @@ -12,19 +12,22 @@ public struct ProviderTokenCostConfig: Sendable { public let menuHintLines: [ProviderTokenCostHint] public let supportsTokenSnapshot: Bool public let showsHintInProviderDetails: Bool + public let showsCostMenuSection: Bool public init( supportsTokenCost: Bool, noDataMessage: @escaping @Sendable () -> String, menuHintLines: [ProviderTokenCostHint] = [], supportsTokenSnapshot: Bool = false, - showsHintInProviderDetails: Bool = false) + showsHintInProviderDetails: Bool = false, + showsCostMenuSection: Bool = true) { self.supportsTokenCost = supportsTokenCost self.noDataMessage = noDataMessage self.menuHintLines = menuHintLines self.supportsTokenSnapshot = supportsTokenSnapshot self.showsHintInProviderDetails = showsHintInProviderDetails + self.showsCostMenuSection = showsCostMenuSection } } From bd628bc272338c48f73f024801a18c3ec2d4e64c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:19:11 -0700 Subject: [PATCH 4/9] refactor: derive cli provider capabilities --- Sources/CodexBarCLI/CLICardsCommand.swift | 6 +++- Sources/CodexBarCLI/CLICardsRenderer.swift | 25 ++++++++++++---- Sources/CodexBarCLI/CLIClaudeSwapCards.swift | 2 ++ Sources/CodexBarCLI/CLIConfigCommand.swift | 3 ++ Sources/CodexBarCLI/CLICostCommand.swift | 15 ++++------ Sources/CodexBarCLI/CLIHelpers.swift | 18 ++++++++--- Sources/CodexBarCLI/CLISessionsCommand.swift | 1 + .../DashboardSnapshotBuilder.swift | 30 ++++++++----------- .../Claude/ClaudeProviderDescriptor.swift | 1 + .../Codex/CodexProviderDescriptor.swift | 1 + .../Cursor/CursorProviderDescriptor.swift | 9 ++++++ .../Providers/ProviderCLIConfig.swift | 3 ++ 12 files changed, 76 insertions(+), 38 deletions(-) diff --git a/Sources/CodexBarCLI/CLICardsCommand.swift b/Sources/CodexBarCLI/CLICardsCommand.swift index 919102a1ce..ccffe4703a 100644 --- a/Sources/CodexBarCLI/CLICardsCommand.swift +++ b/Sources/CodexBarCLI/CLICardsCommand.swift @@ -97,6 +97,7 @@ extension CodexBarCLI { let resetStyle = Self.resetTimeDisplayStyleFromDefaults() let weeklyWorkDays = Self.weeklyProgressWorkDaysFromDefaults() let providerList = provider.asList + // Provider-specific by design: claude-swap cards need Claude's integration configuration and subprocess. let claudeConfig = config.providerConfig(for: .claude) let tokenSelection: TokenAccountCLISelection @@ -122,6 +123,7 @@ extension CodexBarCLI { output: output, kind: .args) } + // Provider-specific by design: --all-accounts includes reconciled Codex live and managed profiles. let supportsAllCodexAccounts = providerList[0] == .codex && tokenSelection.allAccounts && tokenSelection.label == nil @@ -196,7 +198,9 @@ extension CodexBarCLI { command: command) } }) - if result.exitCode != .success { exitCode = result.exitCode } + if result.exitCode != .success { + exitCode = result.exitCode + } cards.append(contentsOf: result.cards) failures.append(contentsOf: result.cardFailures) } diff --git a/Sources/CodexBarCLI/CLICardsRenderer.swift b/Sources/CodexBarCLI/CLICardsRenderer.swift index f98b9c9de2..62babefe81 100644 --- a/Sources/CodexBarCLI/CLICardsRenderer.swift +++ b/Sources/CodexBarCLI/CLICardsRenderer.swift @@ -184,6 +184,7 @@ enum CLICardsRenderer { : sanitizedLabel let problem = account.error.map(CLIClaudeSwapText.sanitizeDiagnostic) if let snapshot = account.snapshot { + // Provider-specific by design: claude-swap subprocess records render as Claude account cards. let base = Self.makeCard(CLICardBuildInput( provider: .claude, snapshot: snapshot, @@ -576,7 +577,9 @@ enum CLICardsRenderer { private static func truncatePlain(_ text: String, width: Int) -> String { guard width > 0 else { return "" } guard text.count > width else { return text } - if width <= 1 { return String(text.prefix(width)) } + if width <= 1 { + return String(text.prefix(width)) + } return String(text.prefix(width - 1)) + "…" } @@ -647,11 +650,21 @@ enum CLICardsRenderer { private static func normalizedSourceLabel(_ source: String) -> String { let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines) - if trimmed.isEmpty { return "auto" } - if trimmed.contains("oauth") { return "oauth" } - if trimmed.contains("web") || trimmed.contains("openai-web") { return "web" } - if trimmed.contains("api") { return "api" } - if trimmed.contains("cli") { return "cli" } + if trimmed.isEmpty { + return "auto" + } + if trimmed.contains("oauth") { + return "oauth" + } + if trimmed.contains("web") || trimmed.contains("openai-web") { + return "web" + } + if trimmed.contains("api") { + return "api" + } + if trimmed.contains("cli") { + return "cli" + } return trimmed } } diff --git a/Sources/CodexBarCLI/CLIClaudeSwapCards.swift b/Sources/CodexBarCLI/CLIClaudeSwapCards.swift index a98153ab31..cd146c0de9 100644 --- a/Sources/CodexBarCLI/CLIClaudeSwapCards.swift +++ b/Sources/CodexBarCLI/CLIClaudeSwapCards.swift @@ -94,6 +94,7 @@ enum CLIClaudeSwapCards { hasExplicitAccountSelection: Bool, sourceModeOverride: ProviderSourceMode?) -> Bool { + // Provider-specific by design: claude-swap owns multi-account state only in Claude automatic mode. provider == .claude && integrationEnabled && !hasExplicitAccountSelection @@ -148,6 +149,7 @@ enum CLIClaudeSwapCards { let diagnostic = CLIClaudeSwapText.sanitizeDiagnostic(error.localizedDescription) let message = diagnostic.isEmpty ? "claude-swap list failed." : diagnostic output.cardFailures.append(CLICardFailure( + // Provider-specific by design: a claude-swap subprocess failure is attached to the Claude card lane. provider: .claude, accountLabel: ClaudeSwapAccountProjection.sourceLabel, message: message)) diff --git a/Sources/CodexBarCLI/CLIConfigCommand.swift b/Sources/CodexBarCLI/CLIConfigCommand.swift index 0e763f8cdc..72ba85218a 100644 --- a/Sources/CodexBarCLI/CLIConfigCommand.swift +++ b/Sources/CodexBarCLI/CLIConfigCommand.swift @@ -120,6 +120,7 @@ extension CodexBarCLI { } static func unsupportedAPIKeyErrorMessage(for provider: UsageProvider, rawProvider: String) -> String { + // Provider-specific by design: Codex users are redirected to the separate OpenAI Platform provider ID. if provider == .codex { "\(rawProvider) does not support config API keys. For OpenAI Platform API keys, use '--provider openai'." } else { @@ -251,6 +252,7 @@ extension CodexBarCLI { return updated } providerConfig.apiKey = apiKey + // Provider-specific by design: legacy Moonshot config binds a newly set key to its existing/default region. if provider == .moonshot { providerConfig.apiKeyRegion = providerConfig.sanitizedRegion ?? MoonshotRegion.international.rawValue } @@ -282,6 +284,7 @@ extension CodexBarCLI { cleanedWorkspaceID != nil guard hasAccountOptions else { return nil } + // Provider-specific by design: z.ai team tokens alone accept organization, workspace, and usage-scope fields. guard provider == .zai else { throw CLIArgumentError("Token-account options are only supported for --provider zai.") } diff --git a/Sources/CodexBarCLI/CLICostCommand.swift b/Sources/CodexBarCLI/CLICostCommand.swift index fcf4c0d958..4f6b80208b 100644 --- a/Sources/CodexBarCLI/CLICostCommand.swift +++ b/Sources/CodexBarCLI/CLICostCommand.swift @@ -3,15 +3,8 @@ import Commander import Foundation extension CodexBarCLI { - private static let costSupportedProviders: Set = { - #if os(macOS) - [.claude, .codex, .cursor] - #else - // Cursor cost relies on the macOS-only dashboard fetch path; `supportsTokenSnapshot(.cursor)` - // is false elsewhere, so don't advertise Cursor cost where it can only fail. - [.claude, .codex] - #endif - }() + private static let costSupportedProviders = Set( + ProviderDescriptorRegistry.all.filter(\.cli.supportsCostCommand).map(\.id)) static func runCost(_ values: ParsedValues) async { let output = CLIOutputPreferences.from(values: values) @@ -55,6 +48,7 @@ extension CodexBarCLI { } let groupBy = Self.decodeCostGroupBy(from: values) if groupBy == .project { + // Provider-specific by design: only Codex JSONL sessions carry the local project attribution index. let unsupportedProjectProviders = providers.filter { $0 != .codex } if !unsupportedProjectProviders.isEmpty, !output.jsonOnly { let names = unsupportedProjectProviders @@ -140,6 +134,7 @@ extension CodexBarCLI { useColor: Bool) -> String { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName + // Provider-specific by design: Codex cost is explicitly an API-equivalent local-session estimate. let title = provider == .codex ? "\(name) API-equivalent estimate (not billed)" : "\(name) Cost (API-rate estimate)" @@ -252,6 +247,7 @@ extension CodexBarCLI { return CostPayload( provider: provider.rawValue, + // Provider-specific by design: Cursor cost comes from its authenticated dashboard, not local logs. source: provider == .cursor ? "web" : "local", updatedAt: snapshot?.updatedAt ?? (error == nil ? nil : Date()), currencyCode: snapshot?.currencyCode, @@ -386,6 +382,7 @@ extension CodexBarCLI { config: CodexBarConfig, providers: [UsageProvider]) throws -> ProviderSettingsSnapshot.CursorProviderSettings? { + // Provider-specific by design: Cursor cost fetches must resolve its selected dashboard-cookie account. guard providers.contains(.cursor) else { return nil } let selection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false) let context = try TokenAccountCLIContext(selection: selection, config: config, verbose: false) diff --git a/Sources/CodexBarCLI/CLIHelpers.swift b/Sources/CodexBarCLI/CLIHelpers.swift index accf597b9c..80a95ce3b8 100644 --- a/Sources/CodexBarCLI/CLIHelpers.swift +++ b/Sources/CodexBarCLI/CLIHelpers.swift @@ -29,8 +29,12 @@ extension CodexBarCLI { } return .custom(enabled) } - if enabled.count >= 3 { return .custom(enabled) } - if let first = enabled.first { return ProviderSelection(provider: first) } + if enabled.count >= 3 { + return .custom(enabled) + } + if let first = enabled.first { + return ProviderSelection(provider: first) + } return .custom([]) } @@ -60,9 +64,13 @@ extension CodexBarCLI { static func shouldUseColor(noColor: Bool, format: OutputFormat) -> Bool { guard format == .text else { return false } - if noColor { return false } + if noColor { + return false + } let env = ProcessInfo.processInfo.environment - if env["TERM"]?.lowercased() == "dumb" { return false } + if env["TERM"]?.lowercased() == "dumb" { + return false + } return isatty(STDOUT_FILENO) == 1 } @@ -105,6 +113,7 @@ extension CodexBarCLI { sourceMode: ProviderSourceMode, resolvedSourceLabel: String) -> [String] { + // Provider-specific by design: Kilo automatic mode reports when its CLI fallback won strategy selection. guard provider == .kilo, sourceMode == .auto, resolvedSourceLabel.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "cli" @@ -119,6 +128,7 @@ extension CodexBarCLI { sourceMode: ProviderSourceMode, attempts: [ProviderFetchAttempt]) -> String? { + // Provider-specific by design: Kilo exposes its ordered API-to-CLI fallback attempts in verbose output. guard provider == .kilo, sourceMode == .auto, !attempts.isEmpty else { return nil } let parts = attempts.map { attempt in let label = Self.fetchKindLabel(attempt.kind) diff --git a/Sources/CodexBarCLI/CLISessionsCommand.swift b/Sources/CodexBarCLI/CLISessionsCommand.swift index 1379865269..1d29c1a8e7 100644 --- a/Sources/CodexBarCLI/CLISessionsCommand.swift +++ b/Sources/CodexBarCLI/CLISessionsCommand.swift @@ -26,6 +26,7 @@ extension CodexBarCLI { static func sessionsForJSON(_ sessions: [AgentSession], includePiFamily: Bool) -> [AgentSession] { guard !includePiFamily else { return sessions } + // Provider-specific by design: the legacy sessions JSON contract includes only native Codex/Claude scans. return sessions.filter { $0.provider == .codex || $0.provider == .claude } } diff --git a/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift b/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift index ae0d670836..35850aed57 100644 --- a/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift +++ b/Sources/CodexBarCLI/DashboardSnapshotBuilder.swift @@ -143,6 +143,7 @@ enum DashboardSnapshotBuilder { return nil } + // Provider-specific by design: Codex plan aliases and Kilo's auto-top-up suffix require distinct cleanup. if provider == .codex { return CodexPlanFormatting.displayName(raw) ?? UsageFormatter.cleanPlanName(raw) } @@ -164,6 +165,7 @@ enum DashboardSnapshotBuilder { guard let usage else { return [] } let labels = self.rateWindowLabels(provider: provider, metadata: metadata, usage: usage) var windows: [DashboardWindowPayload] = [] + // Provider-specific by design: Amp subscription payloads model balance and orb as non-time-window kinds. let isAmpSubscription = provider == .amp && usage.secondary != nil if let primary = usage.primary { @@ -195,26 +197,18 @@ enum DashboardSnapshotBuilder { metadata: ProviderMetadata?, usage: UsageSnapshot) -> RateWindowLabels { - if provider == .factory, usage.tertiary != nil { - return RateWindowLabels(primary: "5-hour", secondary: "Weekly", tertiary: "Monthly") - } - - let primaryLabel = if provider == .amp { - AmpProviderDescriptor.primaryLabel(snapshot: usage) ?? metadata?.sessionLabel ?? "Session" - } else if provider == .crof { - CrofProviderDescriptor.primaryLabel(snapshot: usage) - } else { - metadata?.sessionLabel ?? "Session" - } - let secondaryLabel = if provider == .amp { - AmpProviderDescriptor.secondaryLabel(snapshot: usage) ?? metadata?.weeklyLabel ?? "Weekly" - } else { - metadata?.weeklyLabel ?? "Weekly" + guard let provider else { + return RateWindowLabels( + primary: metadata?.sessionLabel ?? "Session", + secondary: metadata?.weeklyLabel ?? "Weekly", + tertiary: metadata?.opusLabel ?? "Tertiary") } + let descriptor = ProviderDescriptorRegistry.descriptor(for: provider) + let labels = descriptor.presentation.rateWindowLabels(metadata: descriptor.metadata, snapshot: usage) return RateWindowLabels( - primary: primaryLabel, - secondary: secondaryLabel, - tertiary: metadata?.opusLabel ?? "Tertiary") + primary: labels.primary, + secondary: labels.secondary, + tertiary: labels.tertiary) } private static func makeWindow(kind: String, label: String, window: RateWindow) -> DashboardWindowPayload { diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index 4d68d7cb04..5a4991dcb9 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -192,6 +192,7 @@ public enum ClaudeProviderDescriptor { versionDetector: { browserDetection in ClaudeUsageFetcher(browserDetection: browserDetection).detectVersion() }, + supportsCostCommand: true, browserSupportExemption: { sourceMode, _, _ in sourceMode == .auto })) } diff --git a/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift index 60994ae3d6..4d821838dd 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift @@ -108,6 +108,7 @@ public enum CodexProviderDescriptor { name: "codex", binaryLocator: { BinaryLocator.resolveCodexBinary() }, versionDetector: { _ in ProviderVersionDetector.codexVersion() }, + supportsCostCommand: true, browserSupportExemption: { sourceMode, _, _ in sourceMode == .auto })) } diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift index 0b4b292a67..6c48cc5bcb 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift @@ -88,6 +88,7 @@ public enum CursorProviderDescriptor { cli: ProviderCLIConfig( name: "cursor", versionDetector: nil, + supportsCostCommand: self.supportsCostCommand, browserSupportExemption: { _, _, settings in #if os(Linux) // Linux uses Cursor app auth and manual cookies; browser import remains macOS-only. @@ -98,6 +99,14 @@ public enum CursorProviderDescriptor { })) } + private static var supportsCostCommand: Bool { + #if os(macOS) + true + #else + false + #endif + } + private static func menuBarWindow( context: ProviderMenuBarWindowContext) -> ProviderMenuBarWindowResolution { diff --git a/Sources/CodexBarCore/Providers/ProviderCLIConfig.swift b/Sources/CodexBarCore/Providers/ProviderCLIConfig.swift index a2cec4697f..60ff878cfe 100644 --- a/Sources/CodexBarCore/Providers/ProviderCLIConfig.swift +++ b/Sources/CodexBarCore/Providers/ProviderCLIConfig.swift @@ -10,6 +10,7 @@ public struct ProviderCLIConfig: Sendable { public let aliases: [String] public let binaryLocator: (@Sendable () -> String?)? public let versionDetector: (@Sendable (BrowserDetection) -> String?)? + public let supportsCostCommand: Bool private let browserSupportExemption: BrowserSupportExemption public init( @@ -17,12 +18,14 @@ public struct ProviderCLIConfig: Sendable { aliases: [String] = [], binaryLocator: (@Sendable () -> String?)? = nil, versionDetector: (@Sendable (BrowserDetection) -> String?)?, + supportsCostCommand: Bool = false, browserSupportExemption: @escaping BrowserSupportExemption = { _, _, _ in false }) { self.name = name self.aliases = aliases self.binaryLocator = binaryLocator self.versionDetector = versionDetector + self.supportsCostCommand = supportsCostCommand self.browserSupportExemption = browserSupportExemption } From fe4f431ad2d7ef018e9031cf6c1cbfe3cdd149fc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:22:19 -0700 Subject: [PATCH 5/9] refactor: justify scanner provider ownership --- Sources/CodexBarCore/CostUsageFetcher.swift | 3 +++ Sources/CodexBarCore/PiSessionCostScanner.swift | 2 ++ Sources/CodexBarCore/ProviderStorageFootprint.swift | 1 + .../Providers/Claude/ClaudeProviderDescriptor.swift | 4 +++- .../Providers/Cursor/CursorProviderDescriptor.swift | 4 +++- .../CodexBarCore/Providers/ProviderDescriptor.swift | 5 ++++- Sources/CodexBarCore/UsageFetcher.swift | 2 ++ Sources/CodexBarCore/UsageFormatter.swift | 10 +--------- .../Vendored/CostUsage/CostUsageCache.swift | 2 ++ .../Vendored/CostUsage/CostUsageScanner.swift | 1 + Sources/CodexBarWidget/CodexBarWidgetViews.swift | 4 ++++ 11 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Sources/CodexBarCore/CostUsageFetcher.swift b/Sources/CodexBarCore/CostUsageFetcher.swift index 1bdb7a8b23..670757726e 100644 --- a/Sources/CodexBarCore/CostUsageFetcher.swift +++ b/Sources/CodexBarCore/CostUsageFetcher.swift @@ -358,6 +358,7 @@ public struct CostUsageFetcher: Sendable { codexHomePath: String?) -> CostUsageScanner.Options { var options = override ?? CostUsageScanner.Options() + // Provider-specific by design: Codex managed profiles relocate sessions and archived_sessions roots. if provider == .codex, let codexHomePath = codexHomePath?.trimmingCharacters(in: .whitespacesAndNewlines), !codexHomePath.isEmpty @@ -512,6 +513,8 @@ public struct CostUsageFetcher: Sendable { options: LocalTokenScanOptions) async throws -> LocalTokenScanResult { try Task.checkCancellation() + // Provider-specific by design: Codex owns project/session attribution and optional Pi merge state, while + // Claude/Vertex share the transcript scanner with mutually exclusive filters. // These synchronous scans can run for minutes on large archives. The dedicated queue keeps // them off the cooperative pool and bridges task cancellation into scanner-level checks. return try await CostUsageScanExecutor.run { checkCancellation in diff --git a/Sources/CodexBarCore/PiSessionCostScanner.swift b/Sources/CodexBarCore/PiSessionCostScanner.swift index 4ecdbd3d63..3845ae5931 100644 --- a/Sources/CodexBarCore/PiSessionCostScanner.swift +++ b/Sources/CodexBarCore/PiSessionCostScanner.swift @@ -107,6 +107,7 @@ enum PiSessionCostScanner { options: Options = Options(), checkCancellation: CostUsageScanner.CancellationCheck?) throws -> CostUsageDailyReport { + // Provider-specific by design: Pi records only OpenAI Codex and Anthropic sessions with distinct pricing. guard provider == .codex || provider == .claude else { return CostUsageDailyReport(data: [], summary: nil) } @@ -711,6 +712,7 @@ enum PiSessionCostScanner { private static func normalizeModelName(_ raw: String, provider: UsageProvider) -> String? { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } + // Provider-specific by design: Pi model IDs require the vendor-specific Codex/Claude pricing normalizers. return switch provider { case .codex: CostUsagePricing.normalizeCodexModel(trimmed) diff --git a/Sources/CodexBarCore/ProviderStorageFootprint.swift b/Sources/CodexBarCore/ProviderStorageFootprint.swift index 75feb7a366..d6ba94f79d 100644 --- a/Sources/CodexBarCore/ProviderStorageFootprint.swift +++ b/Sources/CodexBarCore/ProviderStorageFootprint.swift @@ -113,6 +113,7 @@ public struct ProviderStorageRecommendation: Sendable, Equatable, Identifiable { } public static func recommendations(for footprint: ProviderStorageFootprint) -> [ProviderStorageRecommendation] { + // Provider-specific by design: Codex and Claude publish different component names and cleanup consequences. let candidates: [ProviderStorageRecommendation] = footprint.components.compactMap { component in switch footprint.provider { case .claude: diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index 5a4991dcb9..7e62b2c64e 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -128,7 +128,9 @@ public enum ClaudeProviderDescriptor { supportsTokenCost: true, noDataMessage: self.noDataMessage, menuHintLines: [.estimate], - supportsTokenSnapshot: true), + supportsTokenSnapshot: true, + estimateDisclaimer: "Estimated from local Claude logs at API rates; token totals include cache " + + "read/write tokens and may differ from Claude Code /status."), pace: ProviderPaceCapability( primary: .session(maximumMinutes: 300), secondary: .weekly, diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift index 6c48cc5bcb..fe4742a177 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift @@ -70,7 +70,9 @@ public enum CursorProviderDescriptor { supportsTokenCost: true, noDataMessage: { "No Cursor cost usage found. Sign in to Cursor in your browser or the Cursor app." }, menuHintLines: [.estimate], - supportsTokenSnapshot: self.supportsTokenSnapshot), + supportsTokenSnapshot: self.supportsTokenSnapshot, + estimateDisclaimer: "From Cursor's usage dashboard at vendor token rates; may differ from your " + + "invoice."), pace: ProviderPaceCapability(resetWindowPace: .windowDurationPresent), presentation: ProviderUsagePresentation( requestedMenuBarLaneOrders: [ diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift index e9b240c4ce..cb8b3eb5fa 100644 --- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift @@ -13,6 +13,7 @@ public struct ProviderTokenCostConfig: Sendable { public let supportsTokenSnapshot: Bool public let showsHintInProviderDetails: Bool public let showsCostMenuSection: Bool + public let estimateDisclaimer: String public init( supportsTokenCost: Bool, @@ -20,7 +21,8 @@ public struct ProviderTokenCostConfig: Sendable { menuHintLines: [ProviderTokenCostHint] = [], supportsTokenSnapshot: Bool = false, showsHintInProviderDetails: Bool = false, - showsCostMenuSection: Bool = true) + showsCostMenuSection: Bool = true, + estimateDisclaimer: String = "Estimated from local logs · may differ from your bill") { self.supportsTokenCost = supportsTokenCost self.noDataMessage = noDataMessage @@ -28,6 +30,7 @@ public struct ProviderTokenCostConfig: Sendable { self.supportsTokenSnapshot = supportsTokenSnapshot self.showsHintInProviderDetails = showsHintInProviderDetails self.showsCostMenuSection = showsCostMenuSection + self.estimateDisclaimer = estimateDisclaimer } } diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index 0124107d61..a42d4a0242 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -562,6 +562,8 @@ public enum UsageLimitsAvailability: Equatable, Sendable { account: AccountInfo? = nil, lastErrorDescription: String? = nil) -> Self { + // Provider-specific by design: Claude error text, Codex identity, and Doubao/Antigravity identities signal + // whether a successful payload actually contains subscription limits. if provider == .claude { guard snapshot == nil else { return .available } return ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(lastErrorDescription) diff --git a/Sources/CodexBarCore/UsageFormatter.swift b/Sources/CodexBarCore/UsageFormatter.swift index 0a3e1026c7..e85bf0bb91 100644 --- a/Sources/CodexBarCore/UsageFormatter.swift +++ b/Sources/CodexBarCore/UsageFormatter.swift @@ -310,15 +310,7 @@ public enum UsageFormatter { public static let costEstimateHint = "Estimated from local logs · may differ from your bill" public static func costEstimateHint(provider: UsageProvider) -> String { - switch provider { - case .claude: - "Estimated from local Claude logs at API rates; token totals include cache read/write tokens " + - "and may differ from Claude Code /status." - case .cursor: - "From Cursor's usage dashboard at vendor token rates; may differ from your invoice." - default: - self.costEstimateHint - } + ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.estimateDisclaimer } /// Formats a currency value with the specified currency code. diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift index 715f3af7e4..144852281f 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift @@ -28,6 +28,7 @@ enum CostUsageCacheIO { /// Parsing and attribution changes rotate the Codex parser producer key. /// Increment this artifact version only when the stored schema or cache layout becomes incompatible. private static func artifactVersion(for provider: UsageProvider) -> Int { + // Provider-specific by design: scanner parser/schema compatibility versions differ by cache producer. switch provider { case .codex: 11 @@ -59,6 +60,7 @@ enum CostUsageCacheIO { maxCacheBytes: Int = CostUsageCacheIO.maxCacheLoadBytes) -> CostUsageCache { let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot) + // Provider-specific by design: only Codex persistence carries bounded resume/discovery scan state. // Only Codex has bounded persistence pruning on save; other providers would be // rejected, rebuilt, and written oversized again on every refresh. let effectiveMaxBytes = provider == .codex ? maxCacheBytes : Int.max diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index 22a30f4621..3fe2ab2df2 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -1703,6 +1703,7 @@ enum CostUsageScanner { let emptyReport = CostUsageDailyReport(data: [], summary: nil) try checkCancellation?() + // Provider-specific by design: Codex JSONL and Claude/Vertex transcripts have distinct parsers and caches. switch provider { case .codex: return try self.loadCodexDaily( diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 3668319213..671afda252 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -229,6 +229,7 @@ enum CompactMetricFormatter { } static func costMetricLabel(_ label: String, provider: ProviderInstanceID) -> String { + // Provider-specific by design: old Codex widget timelines lack the API-estimate billing disclaimer. guard provider == .codex else { return "\(label) cost" } // Existing widget timelines may predate the estimate labels. Do not leave a bare // dollar value until the app next republishes it. @@ -641,6 +642,7 @@ struct WidgetUsageRow: Identifiable, Equatable { rows = defaultRows.filter { $0.percentLeft != nil } } guard let limit else { return rows } + // Provider-specific by design: Antigravity medium widgets select one constrained row per model family. if entry.provider == .antigravity, limit >= 2, rows.contains(where: { $0.id.hasPrefix("antigravity-quota-summary-") }) @@ -678,6 +680,7 @@ struct WidgetUsageRow: Identifiable, Equatable { provider: ProviderInstanceID, now: Date) -> [WidgetUsageRow] { + // Provider-specific by design: Codex weekly exhaustion suppresses its paired legacy session widget row. guard provider == .codex, let weekly = snapshots.first(where: { $0.id == "weekly" })?.window, weekly.remainingPercent <= 0, @@ -695,6 +698,7 @@ struct WidgetUsageRow: Identifiable, Equatable { for rowID: String, entry: WidgetSnapshot.ProviderEntry) -> RateWindow? { + // Provider-specific by design: old Codex timelines reconstruct session/weekly windows by duration. guard entry.provider == .codex else { return nil } let candidates = [(entry.primary, "session"), (entry.secondary, "weekly")] for (window, fallbackID) in candidates { From f8b14b49c7fc5f7cfee64de9467a3d9c1a5dd9ba Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:29:21 -0700 Subject: [PATCH 6/9] test: gate provider special case clusters --- .../PreferencesProviderDetailView.swift | 1 + .../CodexBar/PreferencesProvidersPane.swift | 1 + .../SettingsStore+TokenAccounts.swift | 1 + .../StatusItemController+Animation.swift | 5 + .../CodexBar/StatusItemController+Menu.swift | 5 + Sources/CodexBar/UsageStore+OpenAIWeb.swift | 3 + .../CodexBar/UsageStore+PlanUtilization.swift | 4 + Sources/CodexBar/UsageStore+Refresh.swift | 4 + .../CodexBar/UsageStore+TokenAccounts.swift | 4 + Sources/CodexBar/UsageStore+TokenCost.swift | 1 + Sources/CodexBar/UsageStore.swift | 6 + Sources/CodexBarCore/CostUsageFetcher.swift | 4 + Sources/CodexBarCore/UsageFetcher.swift | 4 + .../Vendored/CostUsage/CostUsageCache.swift | 1 + .../Vendored/CostUsage/CostUsageScanner.swift | 3 + .../CodexBarWidget/CodexBarWidgetViews.swift | 2 + .../ProviderArchitectureGatekeeperTests.swift | 199 ++++++++++++++++++ 17 files changed, 248 insertions(+) diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index d4280c230c..31f6b3701e 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -301,6 +301,7 @@ private struct ProviderDetailInfoRows: View { ProviderDetailInfoRow(label: L("Account"), value: self.model.email) } + // Provider-specific by design: Kiro reports an auth method as a separate identity field, not a plan. if self.provider == .kiro, let authMethod = self.store.snapshot(for: self.provider.instanceID)?.loginMethod(for: .kiro), !authMethod.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index c67c3967ad..7f1229e2ff 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -30,6 +30,7 @@ struct ProvidersPane: View { @State private var isAuthenticatingLiveCodexAccount = false init( + // Provider-specific by design: Codex is the historical settings selection when no provider is supplied. provider: UsageProvider = .codex, settings: SettingsStore, store: UsageStore, diff --git a/Sources/CodexBar/SettingsStore+TokenAccounts.swift b/Sources/CodexBar/SettingsStore+TokenAccounts.swift index 0c30964825..722fe55c1a 100644 --- a/Sources/CodexBar/SettingsStore+TokenAccounts.swift +++ b/Sources/CodexBar/SettingsStore+TokenAccounts.swift @@ -271,6 +271,7 @@ extension SettingsStore { removedAccount: ProviderTokenAccount, remainingAccounts: [ProviderTokenAccount]) { + // Provider-specific by design: removing the final Antigravity account must delete its shared OAuth cache. guard provider == .antigravity else { return } guard let removedCredentials = AntigravityOAuthCredentialsStore.credentials( fromTokenAccountValue: removedAccount.token) diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 9102cbb9f8..7bbd490c8b 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -534,6 +534,7 @@ extension StatusItemController { if let phase, self.shouldAnimate(provider: provider) { var pattern = self.animationPattern + // Provider-specific by design: Claude's star glyph cannot render the icon-only unbraid transition. if provider == .claude, pattern == .unbraid { pattern = .cylon } @@ -660,6 +661,7 @@ extension StatusItemController { primary: showUsed ? metricWindow.usedPercent : metricWindow.remainingPercent, secondary: nil) } + // Provider-specific by design: Mistral's balance/spend text replaces percentage lanes in its icon. if provider == .mistral { return (primary: nil, secondary: nil) } @@ -1037,6 +1039,7 @@ extension StatusItemController { } nonisolated static func poeBalanceDisplayText(snapshot: UsageSnapshot?) -> String? { + // Provider-specific by design: Poe stores its point balance in the login-method payload field. self.displayValue( from: snapshot?.loginMethod(for: .poe), prefix: "Balance:", @@ -1044,6 +1047,7 @@ extension StatusItemController { } nonisolated static func moonshotBalanceDisplayText(snapshot: UsageSnapshot?) -> String? { + // Provider-specific by design: Moonshot stores cash/voucher balance text in its login-method payload. self.displayValue( from: snapshot?.loginMethod(for: .moonshot), prefix: "Balance:", @@ -1332,6 +1336,7 @@ extension StatusItemController { if let projection { return projection.menuBarSelectableRateWindow(for: .weekly) } + // Provider-specific by design: Abacus publishes its weekly semantic window in the primary lane. if provider == .abacus { return snapshot?.primary } diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 6e061c116b..c881db5ee4 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -113,6 +113,7 @@ extension StatusItemController { var provider: UsageProvider? if self.shouldMergeIcons { + // Provider-specific by design: Codex is the persisted menu identity fallback when selection is empty. let resolvedProvider = self.resolvedMenuProvider() self.lastMenuProvider = (resolvedProvider ?? .codex).instanceID provider = resolvedProvider @@ -257,6 +258,7 @@ extension StatusItemController { } else { switcherSelection?.provider ?? provider } + // Provider-specific by design: Codex remains the empty merged-menu selection fallback. let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex let rawCodexAccountDisplay = isOverviewSelected ? nil : self.codexAccountMenuDisplay(for: currentProvider) let codexAccountDisplay = isOverviewSelected @@ -678,6 +680,7 @@ extension StatusItemController { return false } + // Provider-specific by design: Kilo organization scopes render as stacked account-like cards. if context.currentProvider == .kilo, self.store.kiloScopeSnapshots.count > 1 { let cards = self.store.kiloScopeSnapshots.compactMap { scope in self.menuCardModel( @@ -1474,6 +1477,7 @@ extension StatusItemController { if webItems.hasUsageBreakdown { return self.makeUsageBreakdownSubmenu(width: width) } + // Provider-specific by design: OpenAI and Mistral attach cost history to their provider usage row. if provider == .openai { return self.makeOpenAIAPIUsageSubmenu(provider: provider, width: width) } @@ -1533,6 +1537,7 @@ extension StatusItemController { } private func hasOpenAIAPIUsageSubmenu(provider: UsageProvider) -> Bool { + // Provider-specific by design: OpenAI Admin API daily data gates its native usage submenu. provider == .openai && self.tokenSnapshotForCostHistorySubmenu(provider: provider)?.daily.isEmpty == false } diff --git a/Sources/CodexBar/UsageStore+OpenAIWeb.swift b/Sources/CodexBar/UsageStore+OpenAIWeb.swift index 2c30e879a2..f277a7e291 100644 --- a/Sources/CodexBar/UsageStore+OpenAIWeb.swift +++ b/Sources/CodexBar/UsageStore+OpenAIWeb.swift @@ -256,6 +256,7 @@ extension UsageStore { self.lastOpenAIDashboardError = nil self.openAIDashboardRequiresLogin = false + // Provider-specific by design: an authorized OpenAI dashboard attaches metadata/backfill to Codex usage. if let currentUsage = self.snapshots[.codex] { self.snapshots[.codex] = currentUsage.withSubscriptionMetadata( expiresAt: dashboard.subscriptionExpiresAt, @@ -904,6 +905,7 @@ extension UsageStore { } if allowCurrentSnapshotFallback, + // Provider-specific by design: OpenAI web attachment falls back to the current Codex owner email. let snapshotEmail = self.snapshots[.codex]?.accountEmail(for: .codex)? .trimmingCharacters(in: .whitespacesAndNewlines), !snapshotEmail.isEmpty @@ -1480,6 +1482,7 @@ extension UsageStore { } func syncOpenAIWebState() { + // Provider-specific by design: the OpenAI WKWebView lifecycle is enabled only for Codex consumer access. guard self.isEnabled(.codex), self.settings.openAIWebAccessEnabled, self.settings.codexCookieSource.isEnabled diff --git a/Sources/CodexBar/UsageStore+PlanUtilization.swift b/Sources/CodexBar/UsageStore+PlanUtilization.swift index cdbd5f6bb3..4cf7541ea0 100644 --- a/Sources/CodexBar/UsageStore+PlanUtilization.swift +++ b/Sources/CodexBar/UsageStore+PlanUtilization.swift @@ -470,6 +470,7 @@ extension UsageStore { context: LimitResetDetectionContext, samples: [PlanUtilizationSeriesSample]) { + // Provider-specific by design: Codex reset celebration confirmation uses owner-scoped session observations. let sessionObservation: LimitResetObservation? = if context.provider == .codex { samples.last(where: { $0.name == .session }).map { LimitResetObservation( @@ -808,6 +809,7 @@ extension UsageStore { .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() if let normalizedEmail, !normalizedEmail.isEmpty { + // Provider-specific by design: Codex hashes email ownership; Claude also binds organization and plan. if provider == .codex { return CodexHistoryOwnership.canonicalEmailHashKey(for: normalizedEmail) } @@ -1091,6 +1093,7 @@ extension UsageStore { shouldAdoptUnscopedHistory: Bool = true, providerBuckets: inout PlanUtilizationHistoryBuckets) -> String? { + // Provider-specific by design: Codex reconciliation and Claude OAuth use distinct persisted owner migrations. if provider == .codex { return self.resolveCodexPlanUtilizationAccountKey( snapshot: snapshot, @@ -1248,6 +1251,7 @@ extension UsageStore { for rawKey in legacyRawKeysToRemove { providerBuckets.accounts.removeValue(forKey: rawKey) } + // Provider-specific by design: legacy Codex email/workspace buckets merge only after ambiguity checks. let mergedHistory = Self.mergedPlanUtilizationHistories(provider: .codex, histories: historiesToMerge) providerBuckets.setHistories(mergedHistory, for: canonicalKey) return canonicalKey diff --git a/Sources/CodexBar/UsageStore+Refresh.swift b/Sources/CodexBar/UsageStore+Refresh.swift index 98c6538d69..9a8f49ef1b 100644 --- a/Sources/CodexBar/UsageStore+Refresh.swift +++ b/Sources/CodexBar/UsageStore+Refresh.swift @@ -297,6 +297,7 @@ extension UsageStore { guard matches.count == 1 else { return nil } return matches[0] }() + // Provider-specific by design: Codex account refresh hydrates only a uniquely matching reconciled owner. if self.snapshots[.codex] == nil, let hydratedPrior, let hydratedSnapshot = hydratedPrior.snapshot @@ -654,6 +655,7 @@ extension UsageStore { context: ProviderRefreshOutcomeContext) async { let rawScoped = result.usage.scoped(to: provider) + // Provider-specific by design: Codex results are discarded when managed-account ownership changes mid-fetch. if provider == .codex, let codexExpectedGuard = context.codexExpectedGuard, !self.shouldApplyCodexUsageResult(expectedGuard: codexExpectedGuard, usage: rawScoped) @@ -1121,6 +1123,7 @@ extension UsageStore { beforeFetch: ClaudeRefreshAuthState?, afterFetchFingerprintToken: String?) -> Bool { + // Provider-specific by design: Claude credential fingerprints invalidate results produced by an old OAuth key. provider == .claude && afterFetchFingerprintToken != beforeFetch?.fingerprintToken } @@ -1287,6 +1290,7 @@ extension UsageStore { } private func clearClaudeCredentialDerivedStateForCredentialSwap() { + // Provider-specific by design: Claude credential swaps invalidate OAuth, swap, widget, quota, and token state. self.widgetUsagePreservationBlockedProviders.insert(.claude) self.snapshots.removeValue(forKey: .claude) self.lastKnownResetSnapshots.removeValue(forKey: .claude) diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index ccc4313620..8e6e2a4820 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -280,6 +280,7 @@ extension UsageStore { } } + // Provider-specific by design: Codex multi-account results reconcile against the post-fetch visible projection. let currentProjection = self.freshCodexVisibleAccountProjectionForAccountRefresh( requireLiveManagedAuthFor: managedAccountIDsWithReadableAuthAtStart) guard self.isCurrentProviderRefreshGeneration(.codex, generation: generation) else { return } @@ -483,6 +484,7 @@ extension UsageStore { _ snapshot: UsageSnapshot?, account: CodexVisibleAccount) -> UsageSnapshot? { + // Provider-specific by design: Codex managed profiles relabel fetched identity from reconciled workspace data. guard let snapshot else { return nil } let existing = snapshot.identity(for: .codex) return snapshot.withIdentity(ProviderIdentitySnapshot( @@ -657,6 +659,7 @@ extension UsageStore { _ outcome: ProviderFetchOutcome, account: CodexVisibleAccount) -> Bool { + // Provider-specific by design: Codex account refresh rejects successful payloads for a different email owner. guard case let .success(result) = outcome.result else { return true } guard let resultEmail = CodexIdentityResolver.normalizeEmail( result.usage.scoped(to: .codex).accountEmail(for: .codex)) @@ -1251,6 +1254,7 @@ extension UsageStore { // produces visually duplicate cards with no useful data. return ResolvedAccountOutcome(snapshot: nil, usage: nil, freshUsage: nil) } + // Provider-specific by design: Claude OAuth rate limits preserve a matching prior OAuth account snapshot. if provider == .claude, ClaudeUsageError.isClaudeOAuthUsageRateLimit(error), let priorSnapshot, diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index 3c9950fc1f..24cf19d654 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -200,6 +200,7 @@ extension UsageStore { @discardableResult func hydrateCachedTokenSnapshots(now: Date = Date()) -> Task? { + // Provider-specific by design: only the Codex local ledger hydrates a cached snapshot before the first scan. guard self.settings.isCostUsageEffectivelyEnabled(for: .codex) else { return nil } guard self.settings.enabledProvidersOrdered(metadataByProvider: self.providerMetadata).contains(.codex) else { return nil diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index aaef0b6c07..14bc241118 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -314,6 +314,7 @@ final class UsageStore { @ObservationIgnored let pluginApprovalStore = ProviderPluginApprovalStore() @ObservationIgnored let sessionQuotaNotifier: any SessionQuotaNotifying @ObservationIgnored let sessionQuotaLogger = CodexBarLog.logger(LogCategories.sessionQuota) + // Provider-specific by design: OpenAI web and Augment runtime diagnostics have dedicated app-owned log streams. @ObservationIgnored let openAIWebLogger = CodexBarLog.logger(LogCategories.provider(.openai, scope: "web")) @ObservationIgnored private let tokenCostLogger = CodexBarLog.logger(LogCategories.tokenCost) @ObservationIgnored let augmentLogger = CodexBarLog.logger(LogCategories.provider(.augment)) @@ -548,6 +549,7 @@ final class UsageStore { if let provider = enabled.first?.firstPartyProvider { return self.style(for: provider) } + // Provider-specific by design: Codex is the historical empty-enabled-set icon fallback. return .codex } @@ -789,6 +791,7 @@ final class UsageStore { } if enrichmentMode == .forcedForeground, self.openAIDashboardRequiresLogin { + // Provider-specific by design: failed OpenAI attachment retries Codex usage before credits enrichment. await self.refreshProvider(.codex) await self.refreshCreditsNow(minimumSnapshotUpdatedAt: refreshStartedAt) } @@ -957,6 +960,7 @@ final class UsageStore { extension UsageStore { func debugDumpClaude() async { + // Provider-specific by design: Claude's debug command owns a raw CLI/web probe artifact and error lane. let fetcher = ClaudeUsageFetcher( browserDetection: self.browserDetection, keepCLISessionsAlive: self.settings.debugKeepCLISessionsAlive) @@ -1420,6 +1424,7 @@ extension UsageStore { return } + // Provider-specific by design: Cursor cost shares the dashboard-cookie source policy with status fetching. // Cursor cost honors the same cookie policy as status: when the user set the cookie source // to Off, skip the network fetch entirely (mirrors CursorProviderDescriptor.checkStatus). if provider == .cursor, self.settings.cursorCookieSource == .off { @@ -1560,6 +1565,7 @@ extension UsageStore { } private func resetTokenUsageState(for provider: UsageProvider) { + // Provider-specific by design: resetting Codex token state also cancels its two ledger catch-up workflows. if provider == .codex { self.cancelCodexCostCatchUp() self.cancelSpendDashboardCodexCostCatchUp() diff --git a/Sources/CodexBarCore/CostUsageFetcher.swift b/Sources/CodexBarCore/CostUsageFetcher.swift index 670757726e..ae6de0bfab 100644 --- a/Sources/CodexBarCore/CostUsageFetcher.swift +++ b/Sources/CodexBarCore/CostUsageFetcher.swift @@ -257,6 +257,7 @@ public struct CostUsageFetcher: Sendable { package func codexScanCatchUpStatus( codexHomePath: String? = nil) async -> CodexScanCatchUpStatus { + // Provider-specific by design: Codex exposes bounded background catch-up for its incremental JSONL scanner. let options = Self.resolvedScannerOptions( self.scannerOptionsOverride(), provider: .codex, @@ -411,6 +412,7 @@ public struct CostUsageFetcher: Sendable { // Rolling window is inclusive, so a 30-day display starts 29 days before `now`. let since = options.calendar.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now let scopedCodexHomePath = codexHomePath?.trimmingCharacters(in: .whitespacesAndNewlines) + // Provider-specific by design: scoped Codex homes exclude ambient Pi sessions from managed-profile totals. let shouldMergePiUsage = provider != .codex || scopedCodexHomePath?.isEmpty != false await Self.refreshPricingIfAllowed( options: PricingRefreshOptions( @@ -1121,6 +1123,7 @@ public struct CostUsageFetcher: Sendable { bypassScannerDebounce: Bool, configuredDuration: TimeInterval?) -> TimeInterval? { + // Provider-specific by design: only Codex refresh uses a bounded initial scan before background catch-up. guard provider == .codex, bypassScannerDebounce, configuredDuration == nil @@ -1375,6 +1378,7 @@ extension CostUsageFetcher { historyDays: Int, cursorCookieHeaderOverride: String?) async throws -> CostUsageTokenSnapshot? { + // Provider-specific by design: Bedrock uses AWS billing while Cursor uses its macOS dashboard session. let since = Calendar.current.date(byAdding: .day, value: -(historyDays - 1), to: now) ?? now if provider == .bedrock { let daily = try await Self.loadBedrockDailyReport( diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index a42d4a0242..d8269e77f4 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -402,6 +402,7 @@ public struct UsageSnapshot: Codable, Sendable { guard Self.identitiesMatch(self.identity, cached.identity) else { return self } // Amp's percentage-based daily quota supersedes the legacy rolling-replenishment cadence. Do not attach // that older exact reset to the new daily window; other providers retain the shared backfill behavior. + // Provider-specific by design: Amp daily quotas must not inherit its obsolete rolling-reset cadence. let cachedPrimary: RateWindow? = if self.identity?.providerID == .amp, self.primary?.resetDescription == "resets daily" { @@ -832,6 +833,7 @@ enum RPCWireError: Error, LocalizedError { /// RPC helper used on background tasks; safe because we confine it to the owning task. private final class CodexRPCClient: @unchecked Sendable { + // Provider-specific by design: Codex RPC owns its dedicated subprocess log category. private static let log = CodexBarLog.logger(LogCategories.provider(.codex, scope: "rpc")) private let process = Process() private let stdinPipe = Pipe() @@ -1145,6 +1147,7 @@ public struct UsageFetcher: Sendable { let limits = limitsResponse.rateLimits let account = try? await rpc.fetchAccount() let rateLimitsPlan = Self.normalizedCodexAccountField(limits.planType) + // Provider-specific by design: Codex app-server responses construct Codex reconciled identity. let identity = ProviderIdentitySnapshot( providerID: .codex, accountEmail: account?.account.flatMap { details in @@ -1365,6 +1368,7 @@ public struct UsageFetcher: Sendable { } private static func recoverUsageFromRPCError(_ error: Error) -> UsageSnapshot? { + // Provider-specific by design: Codex RPC error bodies can still carry authoritative rate-limit payloads. guard let body = self.decodeRateLimitsErrorBody(from: error) else { return nil } let identity = ProviderIdentitySnapshot( providerID: .codex, diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift index 144852281f..f5d9abdf09 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift @@ -765,6 +765,7 @@ enum CostUsageCacheIO { provider: UsageProvider, parserHash: String = CodexParserHash.value) -> String? { + // Provider-specific by design: only the Codex incremental parser persists a producer hash. guard provider == .codex else { return nil } return "\(provider.rawValue):cu:p\(parserHash)" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index 3fe2ab2df2..992c5fc2b1 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -728,6 +728,7 @@ enum CostUsageScanner { private let homeCodexWorktreesPrefix: String init(homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) { + // Provider-specific by design: Codex worktree sessions canonicalize to their source project path. self.homeCodexWorktreesPrefix = homeDirectory .appendingPathComponent(".codex/worktrees", isDirectory: true) .standardizedFileURL @@ -1776,6 +1777,7 @@ enum CostUsageScanner { // MARK: - Codex private static func defaultCodexSessionsRoot(options: Options) -> URL { + // Provider-specific by design: Codex session discovery honors CODEX_HOME before ~/.codex. if let override = options.codexSessionsRoot { return override } @@ -4458,6 +4460,7 @@ enum CostUsageScanner { } private static func saveCodexCache(_ cache: CostUsageCache, options: Options, range: CostUsageDayRange) { + // Provider-specific by design: Codex scans persist resume and report-window metadata. CostUsageCacheIO.save( provider: .codex, cache: cache, diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 671afda252..205d242ed5 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -727,6 +727,7 @@ struct WidgetUsageRow: Identifiable, Equatable { } private static func antigravityQuotaFamily(for row: WidgetUsageRow) -> AntigravityQuotaFamily? { + // Provider-specific by design: Antigravity IDs/titles classify Gemini versus third-party quota families. guard row.id.hasPrefix("antigravity-quota-summary-") else { return nil } let id = row.id.lowercased() if id.contains("gemini") { @@ -939,6 +940,7 @@ struct WidgetBalanceLine: Equatable { enum WidgetBalanceFormatter { static func extraUsageCost(for entry: WidgetSnapshot.ProviderEntry) -> ProviderCostSnapshot? { + // Provider-specific by design: Devin encodes its extra-usage balance as a named provider-cost period. guard entry.provider == .devin, let cost = entry.providerCost, cost.period == "Extra usage balance" diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index eb9327f1b5..9b24ec24ed 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -211,6 +211,205 @@ struct ProviderArchitectureGatekeeperTests { ]) } + @Test + func `cross provider case clusters are derived or specifically justified`() throws { + let root = try Self.repoRoot() + let sources = root.appending(path: "Sources", directoryHint: .isDirectory) + let providerCases = UsageProvider.allCases.map(\.rawValue) + let enumerator = try #require(FileManager.default.enumerator( + at: sources, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles])) + var failures: [String] = [] + + for case let url as URL in enumerator where url.pathExtension == "swift" { + let relativePath = url.path.replacingOccurrences(of: root.path + "/", with: "") + guard !relativePath.contains("/Providers/") else { continue } + let source = try String(contentsOf: url, encoding: .utf8) + let lines = source.components(separatedBy: .newlines) + let hitLines = lines.indices.filter { index in + let trimmed = lines[index].trimmingCharacters(in: .whitespaces) + guard !trimmed.hasPrefix("//") else { return false } + return providerCases.contains { Self.containsProviderCase($0, in: lines[index]) } + } + guard !hitLines.isEmpty else { continue } + + if Self.auditedCrossProviderFiles.contains(relativePath) { + for cluster in Self.providerCaseClusters(hitLines) { + let markerStart = max(0, cluster.lowerBound - Self.providerCaseMarkerWindow) + let hasMarker = lines[markerStart...cluster.lowerBound] + .contains { $0.contains("Provider-specific by design:") } + if !hasMarker { + failures.append( + "\(relativePath):\(cluster.lowerBound + 1) has an unjustified UsageProvider case cluster; " + + "derive it or add '// Provider-specific by design: ' immediately " + + "before the cluster.") + } + } + continue + } + + let isProviderOwnedFile = Self.providerOwnedFilenameTokens.contains { token in + url.deletingPathExtension().lastPathComponent.contains(token) + } + let isAllowlisted = Self.genericDispatchAllowlist.contains(relativePath) + let hasExistingJustification = source.contains("Provider-specific by design:") + if !isProviderOwnedFile, !isAllowlisted, !hasExistingJustification { + failures.append( + "\(relativePath):\(hitLines[0] + 1) is a new cross-provider case-dispatch file; " + + "add it to the audited inventory, derive the dispatch, or justify the provider owner.") + } + } + + #expect(failures.isEmpty, Comment(rawValue: failures.joined(separator: "\n"))) + } + + private static let providerCaseMarkerWindow = 120 + private static let providerCaseClusterGap = 120 + + private static let auditedCrossProviderFiles: Set = [ + "Sources/CodexBar/Config/CodexBarConfigMigrator.swift", + "Sources/CodexBar/PreferencesProviderDetailView.swift", + "Sources/CodexBar/PreferencesProvidersPane.swift", + "Sources/CodexBar/SessionQuotaNotifications.swift", + "Sources/CodexBar/SettingsStore+ProviderDetection.swift", + "Sources/CodexBar/SettingsStore+TokenAccounts.swift", + "Sources/CodexBar/SettingsStore+TokenCost.swift", + "Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift", + "Sources/CodexBar/StatusItemController+Actions.swift", + "Sources/CodexBar/StatusItemController+Animation.swift", + "Sources/CodexBar/StatusItemController+Menu.swift", + "Sources/CodexBar/StatusItemController+MenuCardModel.swift", + "Sources/CodexBar/StatusItemController+MenuTracking.swift", + "Sources/CodexBar/UsageStore+Accessors.swift", + "Sources/CodexBar/UsageStore+BackgroundRefresh.swift", + "Sources/CodexBar/UsageStore+HighestUsage.swift", + "Sources/CodexBar/UsageStore+HistoricalPace.swift", + "Sources/CodexBar/UsageStore+OpenAIWeb.swift", + "Sources/CodexBar/UsageStore+PlanUtilization.swift", + "Sources/CodexBar/UsageStore+QuotaWarnings.swift", + "Sources/CodexBar/UsageStore+Refresh.swift", + "Sources/CodexBar/UsageStore+SessionEquivalents.swift", + "Sources/CodexBar/UsageStore+SessionQuotaTransition.swift", + "Sources/CodexBar/UsageStore+TokenAccounts.swift", + "Sources/CodexBar/UsageStore+TokenCost.swift", + "Sources/CodexBar/UsageStore+WidgetSnapshot.swift", + "Sources/CodexBar/UsageStore.swift", + "Sources/CodexBarCLI/CLICardsCommand.swift", + "Sources/CodexBarCLI/CLICardsRenderer.swift", + "Sources/CodexBarCLI/CLIClaudeSwapCards.swift", + "Sources/CodexBarCLI/CLIConfigCommand.swift", + "Sources/CodexBarCLI/CLICostCommand.swift", + "Sources/CodexBarCLI/CLIHelpers.swift", + "Sources/CodexBarCLI/CLISessionsCommand.swift", + "Sources/CodexBarCLI/DashboardSnapshotBuilder.swift", + "Sources/CodexBarCore/CostUsageFetcher.swift", + "Sources/CodexBarCore/PiSessionCostScanner.swift", + "Sources/CodexBarCore/ProviderStorageFootprint.swift", + "Sources/CodexBarCore/UsageFetcher.swift", + "Sources/CodexBarCore/UsageFormatter.swift", + "Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift", + "Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift", + "Sources/CodexBarWidget/BurnDownWidgetProvider.swift", + "Sources/CodexBarWidget/CodexBarWidgetViews.swift", + ] + + private static let genericDispatchAllowlist: Set = [ + "Sources/CodexBar/CostHistoryChartMenuView.swift", + "Sources/CodexBar/HistoricalUsagePace.swift", + "Sources/CodexBar/IconRenderer.swift", + "Sources/CodexBar/InlineUsageDashboardContent.swift", + "Sources/CodexBar/MenuBarLayout.swift", + "Sources/CodexBar/MenuBarLayoutEditor.swift", + "Sources/CodexBar/MenuBarMetricWindowResolver.swift", + "Sources/CodexBar/MenuCardView+Costs.swift", + "Sources/CodexBar/MenuCardView+ModelHelpers.swift", + "Sources/CodexBar/MenuCardView.swift", + "Sources/CodexBar/MenuDescriptor.swift", + "Sources/CodexBar/MenuOpenRefreshPlan.swift", + "Sources/CodexBar/PredictivePaceWarnings.swift", + "Sources/CodexBar/PreferencesMenuPane.swift", + "Sources/CodexBar/PreferencesSpendDashboardPane.swift", + "Sources/CodexBar/ProviderRegistry.swift", + "Sources/CodexBar/PreferencesProvidersPane+Testing.swift", + "Sources/CodexBar/SettingsStore+MenuObservation.swift", + "Sources/CodexBar/SettingsStore+MenuPreferences.swift", + "Sources/CodexBar/SettingsStore.swift", + "Sources/CodexBar/ShareStatsPayload.swift", + "Sources/CodexBar/SpendDashboardController.swift", + "Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift", + "Sources/CodexBar/SpendDashboardModel.swift", + "Sources/CodexBar/StatusItemController+CompactAccountMenu.swift", + "Sources/CodexBar/StatusItemController+CostMenuCard.swift", + "Sources/CodexBar/StatusItemController+CountdownRefresh.swift", + "Sources/CodexBar/StatusItemController+HostedSubmenus.swift", + "Sources/CodexBar/StatusItemController+MemoryPressure.swift", + "Sources/CodexBar/StatusItemController+MenuBarLayout.swift", + "Sources/CodexBar/StatusItemController+MenuSwitcherWarmup.swift", + "Sources/CodexBar/StatusItemController+MenuTypes.swift", + "Sources/CodexBar/StatusItemController+MenuViewportRestore.swift", + "Sources/CodexBar/StatusItemController+OverviewSubmenus.swift", + "Sources/CodexBar/StatusItemController+ProviderNavigation.swift", + "Sources/CodexBar/StatusItemController+SwitcherMetrics.swift", + "Sources/CodexBar/StatusItemController.swift", + "Sources/CodexBar/UsageStore+APIKeyDebug.swift", + "Sources/CodexBar/UsageStore+LimitResetCelebration.swift", + "Sources/CodexBar/UsageStore+LimitResetIdentity.swift", + "Sources/CodexBar/UsageStore+ProviderStorage.swift", + "Sources/CodexBar/UsageStore+RefreshEnrichment.swift", + "Sources/CodexBar/UsageStore+TokenAccountLabels.swift", + "Sources/CodexBarCore/AgentSession.swift", + "Sources/CodexBarCore/LocalAgentSessionScanner.swift", + "Sources/CodexBarCore/Logging/LogCategories.swift", + "Sources/CodexBarCore/PathEnvironment.swift", + "Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift", + "Sources/CodexBarCore/SessionWindowFocuser.swift", + "Sources/CodexBarCore/UsageSnapshot+SwitcherWeeklyWindow.swift", + "Sources/CodexBarCore/Vendored/CostUsage/ModelsDevPricing.swift", + "Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift", + "Sources/CodexBarCLI/CLIHelp.swift", + ] + + private static let providerOwnedFilenameTokens = [ + "Codex", "Claude", "Cursor", "Gemini", "Antigravity", "Copilot", "Zai", "MiniMax", "Kimi", "Kilo", + "Kiro", "Vertex", "Augment", "Moonshot", "Amp", "Synthetic", "OpenRouter", "ElevenLabs", "Warp", + "Windsurf", "Perplexity", "Mimo", "Doubao", "Sakana", "Abacus", "Mistral", "DeepSeek", "DeepInfra", + "Crof", "Venice", "CommandCode", "Qoder", "Bedrock", "Grok", "Groq", "Deepgram", "Poe", + "ClawRouter", "Sub2API", "OpenAI", "Alibaba", "StepFun", "Wayfinder", "ZoomMate", "Notion", + ] + + private static func providerCaseClusters(_ hitLines: [Int]) -> [ClosedRange] { + guard let first = hitLines.first else { return [] } + var clusters: [ClosedRange] = [] + var start = first + var end = first + for line in hitLines.dropFirst() { + if line - end > self.providerCaseClusterGap { + clusters.append(start...end) + start = line + } + end = line + } + clusters.append(start...end) + return clusters + } + + private static func containsProviderCase(_ rawValue: String, in line: String) -> Bool { + let needle = ".\(rawValue)" + var searchStart = line.startIndex + while let range = line.range(of: needle, range: searchStart.. Bool { + character == "_" || character.isLetter || character.isNumber + } + private static func repoRoot() throws -> URL { var directory = URL(filePath: #filePath).deletingLastPathComponent() for _ in 0..<12 { From 2994084e703e69b501fe2de03c7d3a9971ff4005 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:30:16 -0700 Subject: [PATCH 7/9] build: refresh codex parser hash --- Sources/CodexBarCore/Generated/CodexParserHash.generated.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 97d361de19..f81d7b2f83 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "6c0f1fa950e63467" + static let value = "37aedd661c4272a8" } From 62a05af64a180e262ba6c785036929d6e4ded67d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:31:47 -0700 Subject: [PATCH 8/9] style: satisfy special case gate lint --- Sources/CodexBar/StatusItemController+Animation.swift | 1 + Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 7bbd490c8b..1c134cfcf6 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -239,6 +239,7 @@ extension StatusItemController { } @discardableResult + // swiftlint:disable:next function_body_length func applyIcon( phase: Double?, bypassMergedMenuTrackingDeferral: Bool = false) -> Bool diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 9b24ec24ed..91f57e9d34 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -241,7 +241,7 @@ struct ProviderArchitectureGatekeeperTests { .contains { $0.contains("Provider-specific by design:") } if !hasMarker { failures.append( - "\(relativePath):\(cluster.lowerBound + 1) has an unjustified UsageProvider case cluster; " + + "\(relativePath):\(cluster.lowerBound + 1) has an unjustified provider case cluster; " + "derive it or add '// Provider-specific by design: ' immediately " + "before the cluster.") } From 9a2a077286c1cb5a629a6b4c8ac3fed9fcbb533c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 5 Aug 2026 16:32:31 -0700 Subject: [PATCH 9/9] style: accept cli cards command size --- Sources/CodexBarCLI/CLICardsCommand.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CodexBarCLI/CLICardsCommand.swift b/Sources/CodexBarCLI/CLICardsCommand.swift index ccffe4703a..4754048c1f 100644 --- a/Sources/CodexBarCLI/CLICardsCommand.swift +++ b/Sources/CodexBarCLI/CLICardsCommand.swift @@ -66,6 +66,7 @@ struct CardsOptions: CommanderParsable { } extension CodexBarCLI { + // swiftlint:disable:next function_body_length static func runCards(_ values: ParsedValues) async { let output = CLIOutputPreferences.from(values: values) let config = Self.loadConfig(output: output)