import AppKitimport CodexBarCoreimport Observationimport ServiceManagement enum RefreshFrequency: String, CaseIterable, Identifiable { case manual case oneMinute case twoMinutes case fiveMinutes case fifteenMinutes case thirtyMinutes var id: String { self.rawValue } var seconds: TimeInterval? { switch self { case .manual: nil case .oneMinute: 60 case .twoMinutes: 120 case .fiveMinutes: 300 case .fifteenMinutes: 900 case .thirtyMinutes: 1800 } } var label: String { switch self { case .manual: L("refresh_manual") case .oneMinute: L("refresh_1min") case .twoMinutes: L("refresh_2min") case .fiveMinutes: L("refresh_5min") case .fifteenMinutes: L("refresh_15min") case .thirtyMinutes: L("refresh_30min") } }} enum MenuBarMetricPreference: String, CaseIterable, Identifiable { case automatic case primary case secondary case tertiary case extraUsage case average case monthlyPlan var id: String { self.rawValue } var label: String { switch self { case .automatic: L("metric_pref_automatic") case .primary: L("metric_pref_primary") case .secondary: L("metric_pref_secondary") case .tertiary: L("metric_pref_tertiary") case .extraUsage: L("metric_pref_extra_usage") case .average: L("metric_pref_average") case .monthlyPlan: L("metric_mistral_monthly_plan") } }} enum KiroMenuBarDisplayMode: String, CaseIterable, Identifiable { case automatic case hidden case creditsLeft case percentLeft case creditsAndPercent case usedAndTotal case overageCreditsWhenExhausted case overageCostWhenExhausted case overageCreditsAndCostWhenExhausted var id: String { self.rawValue } var label: String { switch self { case .automatic: "Automatic" case .hidden: "Hidden" case .creditsLeft: "Credits left" case .percentLeft: "Percent left" case .creditsAndPercent: "Credits + percent" case .usedAndTotal: "Used / total" case .overageCreditsWhenExhausted: "Overage credits at zero" case .overageCostWhenExhausted: "Overage cost at zero" case .overageCreditsAndCostWhenExhausted: "Overage credits + cost at zero" } }} enum MultiAccountMenuLayout: String, CaseIterable, Identifiable { case segmented case stacked var id: String { self.rawValue } var label: String { switch self { case .segmented: L("multi_account_layout_segmented") case .stacked: L("multi_account_layout_stacked") } }} struct CachedCodexAccountReconciliationSnapshot { let activeSource: CodexActiveSource let loadedAt: Date let snapshot: CodexAccountReconciliationSnapshot} struct CachedCodexAccountMenuProjection: Equatable { let activeSource: CodexActiveSource let loadedAt: Date let projection: CodexVisibleAccountProjection} enum CodexAccountMenuProjectionRevalidationResult: Equatable { case skipped case discarded case unchanged case updated} @MainActor@Observablefinal class SettingsStore { static let sharedDefaults = AppGroupSupport.sharedDefaults() static let mergedOverviewProviderLimit = 3 static let productionCodexAccountReconciliationSnapshotCacheInterval: TimeInterval = 2 static let isRunningTests: Bool = { let env = ProcessInfo.processInfo.environment if env["XCTestConfigurationFilePath"] != nil { return true } if env["TESTING_LIBRARY_VERSION"] != nil { return true } if env["SWIFT_TESTING"] != nil { return true } return NSClassFromString("XCTestCase") != nil }() #if DEBUG static var codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting: TimeInterval? #endif @ObservationIgnored let userDefaults: UserDefaults @ObservationIgnored let configStore: CodexBarConfigStore @ObservationIgnored let antigravityOAuthCredentialsStore: AntigravityOAuthCredentialsStore @ObservationIgnored var config: CodexBarConfig @ObservationIgnored var configPersistTask: Task<Void, Never>? @ObservationIgnored var configLoading = false @ObservationIgnored var tokenAccountsLoaded = false @ObservationIgnored var cachedCodexAccountReconciliationSnapshot: CachedCodexAccountReconciliationSnapshot? @ObservationIgnored var cachedCodexAccountMenuProjection: CachedCodexAccountMenuProjection? @ObservationIgnored var codexAccountReconciliationGeneration: UInt = 0 #if DEBUG @ObservationIgnored var _test_codexAccountSnapshotLoader: (@Sendable (CodexActiveSource) -> CodexAccountReconciliationSnapshot)? #endif @ObservationIgnored var mergedMenuLastSelectedWasOverviewStorage = false @ObservationIgnored var selectedMenuProviderRawStorage: String? var defaultsState: SettingsDefaultsState var configRevision: Int = 0 var providerOrder: [UsageProvider] = [] var providerEnablement: [UsageProvider: Bool] = [:] static func shouldBridgeSharedDefaults(for userDefaults: UserDefaults) -> Bool { if !self.isRunningTests { return true } if userDefaults === UserDefaults.standard { return true } if let shared = sharedDefaults, userDefaults === shared { return true } return false } init( userDefaults: UserDefaults = .standard, configStore: CodexBarConfigStore = CodexBarConfigStore(), zaiTokenStore: any ZaiTokenStoring = KeychainZaiTokenStore(), syntheticTokenStore: any SyntheticTokenStoring = KeychainSyntheticTokenStore(), codexCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "codex-cookie", promptKind: .codexCookie), claudeCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "claude-cookie", promptKind: .claudeCookie), cursorCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "cursor-cookie", promptKind: .cursorCookie), opencodeCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "opencode-cookie", promptKind: .opencodeCookie), factoryCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "factory-cookie", promptKind: .factoryCookie), minimaxCookieStore: any MiniMaxCookieStoring = KeychainMiniMaxCookieStore(), minimaxAPITokenStore: any MiniMaxAPITokenStoring = KeychainMiniMaxAPITokenStore(), kimiTokenStore: any KimiTokenStoring = KeychainKimiTokenStore(), kimiK2TokenStore: any KimiK2TokenStoring = KeychainKimiK2TokenStore(), augmentCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "augment-cookie", promptKind: .augmentCookie), ampCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "amp-cookie", promptKind: .ampCookie), copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore(), tokenAccountStore: any ProviderTokenAccountStoring = FileTokenAccountStore(), antigravityOAuthCredentialsStore: AntigravityOAuthCredentialsStore = AntigravityOAuthCredentialsStore(), performInitialProviderDetection: Bool = !SettingsStore.isRunningTests) { let appGroupID = AppGroupSupport.currentGroupID() let appGroupMigration: AppGroupSupport.MigrationResult if Self.isRunningTests { appGroupMigration = AppGroupSupport.migrateLegacyDataIfNeeded(standardDefaults: userDefaults) } else { Self.scheduleAppGroupMigration() appGroupMigration = AppGroupSupport.MigrationResult(status: .targetUnavailable) } let sharedDefaultsAvailable = Self.sharedDefaults != nil if !Self.isRunningTests { CodexBarLog.logger(LogCategories.settings).info( "App group resolved", metadata: [ "groupID": appGroupID, "sharedDefaultsAvailable": sharedDefaultsAvailable ? "1" : "0", "migrationStatus": appGroupMigration.status.rawValue, "migratedSnapshot": appGroupMigration.copiedSnapshot ? "1" : "0", "migratedDefaults": "\(appGroupMigration.copiedDefaults)", ]) } if userDefaults.object(forKey: "openAIWebAccessEnabled") == nil, let legacyOpenAIWebAccess = userDefaults.object(forKey: "openAIWebAccess") as? Bool { userDefaults.set(legacyOpenAIWebAccess, forKey: "openAIWebAccessEnabled") } let hasStoredOpenAIWebAccessPreference = userDefaults.object(forKey: "openAIWebAccessEnabled") != nil let hadExistingConfig = (try? configStore.load()) != nil let legacyStores = CodexBarConfigMigrator.LegacyStores( zaiTokenStore: zaiTokenStore, syntheticTokenStore: syntheticTokenStore, codexCookieStore: codexCookieStore, claudeCookieStore: claudeCookieStore, cursorCookieStore: cursorCookieStore, opencodeCookieStore: opencodeCookieStore, factoryCookieStore: factoryCookieStore, minimaxCookieStore: minimaxCookieStore, minimaxAPITokenStore: minimaxAPITokenStore, kimiTokenStore: kimiTokenStore, kimiK2TokenStore: kimiK2TokenStore, augmentCookieStore: augmentCookieStore, ampCookieStore: ampCookieStore, copilotTokenStore: copilotTokenStore, tokenAccountStore: tokenAccountStore) let config = CodexBarConfigMigrator.loadOrMigrate( configStore: configStore, userDefaults: userDefaults, stores: legacyStores) self.userDefaults = userDefaults self.configStore = configStore self.antigravityOAuthCredentialsStore = antigravityOAuthCredentialsStore self.config = config self.configLoading = true let defaultsState = Self.loadDefaultsState(userDefaults: userDefaults) self.defaultsState = defaultsState self.mergedMenuLastSelectedWasOverviewStorage = defaultsState.mergedMenuLastSelectedWasOverview self.selectedMenuProviderRawStorage = defaultsState.selectedMenuProviderRaw self.updateProviderState(config: config) self.configLoading = false CodexBarLog.setFileLoggingEnabled(self.debugFileLoggingEnabled) userDefaults.removeObject(forKey: "showCodexUsage") userDefaults.removeObject(forKey: "showClaudeUsage") LaunchAtLoginManager.setEnabled(self.launchAtLogin) if performInitialProviderDetection { self.runInitialProviderDetectionIfNeeded() } self.ensureAlibabaProviderAutoEnabledIfNeeded() self.applyTokenCostDefaultIfNeeded() if self.claudeUsageDataSource != .cli { if Self.isRunningTests { self.claudeWebExtrasEnabled = false } else { self.defaultsState.claudeWebExtrasEnabledRaw = false } } let resolvedOpenAIWebAccessEnabled = if hasStoredOpenAIWebAccessPreference { self.defaultsState.openAIWebAccessEnabled } else { Self.inferredInitialOpenAIWebAccessEnabled( config: config, hadExistingConfig: hadExistingConfig) } if Self.isRunningTests { self.openAIWebAccessEnabled = resolvedOpenAIWebAccessEnabled } else { self.defaultsState.openAIWebAccessEnabled = resolvedOpenAIWebAccessEnabled } KeychainAccessGate.isDisabled = self.debugDisableKeychainAccess }} extension SettingsStore { private static func scheduleAppGroupMigration() { Task.detached(priority: .utility) { let result = AppGroupSupport.migrateLegacyDataIfNeeded() CodexBarLog.logger(LogCategories.settings).info( "App group migration completed", metadata: [ "migrationStatus": result.status.rawValue, "migratedSnapshot": result.copiedSnapshot ? "1" : "0", "migratedDefaults": "\(result.copiedDefaults)", ]) } } private static func inferredInitialOpenAIWebAccessEnabled( config: CodexBarConfig, hadExistingConfig: Bool) -> Bool { guard let codex = config.providerConfig(for: .codex) else { return false } if let cookieSource = codex.cookieSource { return cookieSource.isEnabled } if codex.sanitizedCookieHeader != nil { return true } return hadExistingConfig } private static func loadDefaultsState(userDefaults: UserDefaults) -> SettingsDefaultsState { let refreshDefault = userDefaults.string(forKey: "refreshFrequency") .flatMap(RefreshFrequency.init(rawValue:)) let refreshFrequency = refreshDefault ?? .fiveMinutes if Self.isRunningTests, refreshDefault == nil { userDefaults.set(refreshFrequency.rawValue, forKey: "refreshFrequency") } let launchAtLogin = userDefaults.object(forKey: "launchAtLogin") as? Bool ?? false let debugMenuEnabled = userDefaults.object(forKey: "debugMenuEnabled") as? Bool ?? false let debugDisableKeychainAccess = Self.loadDebugDisableKeychainAccess(userDefaults: userDefaults) let debugFileLoggingEnabled = userDefaults.object(forKey: "debugFileLoggingEnabled") as? Bool ?? false let debugLogLevelRaw = userDefaults.string(forKey: "debugLogLevel") ?? CodexBarLog.Level.verbose.rawValue if Self.isRunningTests, userDefaults.string(forKey: "debugLogLevel") == nil { userDefaults.set(debugLogLevelRaw, forKey: "debugLogLevel") } let debugLoadingPatternRaw = userDefaults.string(forKey: "debugLoadingPattern") let debugKeepCLISessionsAlive = userDefaults.object(forKey: "debugKeepCLISessionsAlive") as? Bool ?? false let statusChecksEnabled = userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true let sessionQuotaDefault = userDefaults.object(forKey: "sessionQuotaNotificationsEnabled") as? Bool let sessionQuotaNotificationsEnabled = sessionQuotaDefault ?? true if Self.isRunningTests, sessionQuotaDefault == nil { userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled") } let quotaWarnings = Self.loadQuotaWarningDefaults(userDefaults: userDefaults) let quotaWarningMarkersVisibleDefault = userDefaults.object(forKey: "quotaWarningMarkersVisible") as? Bool let quotaWarningMarkersVisible = quotaWarningMarkersVisibleDefault ?? true if Self.isRunningTests, quotaWarningMarkersVisibleDefault == nil { userDefaults.set(true, forKey: "quotaWarningMarkersVisible") } let weeklyProgressWorkDays = userDefaults.object(forKey: "weeklyProgressWorkDays") as? Int let usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false let resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false let providerChangelogLinksEnabled = userDefaults.object( forKey: "providerChangelogLinksEnabled") as? Bool ?? false let menuBarShowsBrandIconWithPercent = userDefaults.object( forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false let menuBarHidesCritters = userDefaults.object(forKey: "menuBarHidesCritters") as? Bool ?? false let menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode") ?? MenuBarDisplayMode.percent.rawValue let kiroMenuBarDisplayModeRaw = userDefaults.string(forKey: "kiroMenuBarDisplayMode") ?? KiroMenuBarDisplayMode.automatic.rawValue let historicalTrackingEnabled = userDefaults.object(forKey: "historicalTrackingEnabled") as? Bool ?? false let multiAccountMenuLayoutRaw = Self.loadMultiAccountMenuLayoutRaw(userDefaults: userDefaults) let resolvedPreferences = Self.loadMenuBarMetricPreferences(userDefaults: userDefaults) let copilotBudgetExtrasEnabled = userDefaults.object(forKey: "copilotBudgetExtrasEnabled") as? Bool ?? false let copilotIconSecondaryWindowIDRaw = Self.loadCopilotIconSecondaryWindowIDRaw(userDefaults: userDefaults) let costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? false let rawCostUsageHistoryDays = userDefaults.object(forKey: "tokenCostUsageHistoryDays") as? Int ?? 30 let costUsageHistoryDays = max(1, min(365, rawCostUsageHistoryDays)) let hidePersonalInfo = userDefaults.object(forKey: "hidePersonalInfo") as? Bool ?? false let randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false let confettiOnWeeklyLimitResetsEnabled = userDefaults.object( forKey: "confettiOnWeeklyLimitResetsEnabled") as? Bool ?? false let menuBarShowsHighestUsage = userDefaults.object(forKey: "menuBarShowsHighestUsage") as? Bool ?? false let claudeOAuthKeychainPromptModeRaw = userDefaults.string(forKey: "claudeOAuthKeychainPromptMode") let claudeOAuthKeychainReadStrategyRaw = userDefaults.string(forKey: "claudeOAuthKeychainReadStrategy") let claudeWebExtrasEnabledRaw = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool let showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true if Self.isRunningTests, creditsExtrasDefault == nil { userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage") } let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool let openAIWebAccessEnabled = openAIWebAccessDefault ?? false if Self.isRunningTests, openAIWebAccessDefault == nil { userDefaults.set(false, forKey: "openAIWebAccessEnabled") } let openAIWebBatterySaverDefault = userDefaults.object(forKey: "openAIWebBatterySaverEnabled") as? Bool let openAIWebBatterySaverEnabled = openAIWebBatterySaverDefault ?? false if Self.isRunningTests, openAIWebBatterySaverDefault == nil { userDefaults.set(false, forKey: "openAIWebBatterySaverEnabled") } let providerStorageFootprintsDefault = userDefaults.object(forKey: "providerStorageFootprintsEnabled") as? Bool let providerStorageFootprintsEnabled = providerStorageFootprintsDefault ?? false if Self.isRunningTests, providerStorageFootprintsDefault == nil { userDefaults.set(false, forKey: "providerStorageFootprintsEnabled") } let jetbrainsIDEBasePath = userDefaults.string(forKey: "jetbrainsIDEBasePath") ?? "" let mergeIcons = userDefaults.object(forKey: "mergeIcons") as? Bool ?? true let switcherShowsIcons = userDefaults.object(forKey: "switcherShowsIcons") as? Bool ?? true let mergedMenuLastSelectedWasOverview = userDefaults.object( forKey: "mergedMenuLastSelectedWasOverview") as? Bool ?? false let mergedOverviewSelectedProvidersRaw = userDefaults.array( forKey: "mergedOverviewSelectedProviders") as? [String] ?? [] let selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider") let providerDetectionCompleted = userDefaults.object(forKey: "providerDetectionCompleted") as? Bool ?? false let providersSortedAlphabetically = userDefaults.object( forKey: "providersSortedAlphabetically") as? Bool ?? false let appLanguageRaw = userDefaults.string(forKey: "appLanguage") return SettingsDefaultsState( refreshFrequency: refreshFrequency, launchAtLogin: launchAtLogin, debugMenuEnabled: debugMenuEnabled, debugDisableKeychainAccess: debugDisableKeychainAccess, debugFileLoggingEnabled: debugFileLoggingEnabled, debugLogLevelRaw: debugLogLevelRaw, debugLoadingPatternRaw: debugLoadingPatternRaw, debugKeepCLISessionsAlive: debugKeepCLISessionsAlive, statusChecksEnabled: statusChecksEnabled, sessionQuotaNotificationsEnabled: sessionQuotaNotificationsEnabled, quotaWarningNotificationsEnabled: quotaWarnings.notificationsEnabled, quotaWarningThresholdsRaw: quotaWarnings.thresholdsRaw, quotaWarningSessionThresholdsRaw: quotaWarnings.sessionThresholdsRaw, quotaWarningWeeklyThresholdsRaw: quotaWarnings.weeklyThresholdsRaw, quotaWarningSessionEnabled: quotaWarnings.sessionEnabled, quotaWarningWeeklyEnabled: quotaWarnings.weeklyEnabled, quotaWarningSoundEnabled: quotaWarnings.soundEnabled, quotaWarningMarkersVisible: quotaWarningMarkersVisible, weeklyProgressWorkDays: weeklyProgressWorkDays, usageBarsShowUsed: usageBarsShowUsed, resetTimesShowAbsolute: resetTimesShowAbsolute, providerChangelogLinksEnabled: providerChangelogLinksEnabled, menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent, menuBarHidesCritters: menuBarHidesCritters, menuBarDisplayModeRaw: menuBarDisplayModeRaw, kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw, historicalTrackingEnabled: historicalTrackingEnabled, multiAccountMenuLayoutRaw: multiAccountMenuLayoutRaw, menuBarMetricPreferencesRaw: resolvedPreferences, copilotBudgetExtrasEnabled: copilotBudgetExtrasEnabled, copilotIconSecondaryWindowIDRaw: copilotIconSecondaryWindowIDRaw, costUsageEnabled: costUsageEnabled, costUsageHistoryDays: costUsageHistoryDays, hidePersonalInfo: hidePersonalInfo, randomBlinkEnabled: randomBlinkEnabled, confettiOnWeeklyLimitResetsEnabled: confettiOnWeeklyLimitResetsEnabled, menuBarShowsHighestUsage: menuBarShowsHighestUsage, claudeOAuthKeychainPromptModeRaw: claudeOAuthKeychainPromptModeRaw, claudeOAuthKeychainReadStrategyRaw: claudeOAuthKeychainReadStrategyRaw, claudeWebExtrasEnabledRaw: claudeWebExtrasEnabledRaw, showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage, openAIWebAccessEnabled: openAIWebAccessEnabled, openAIWebBatterySaverEnabled: openAIWebBatterySaverEnabled, providerStorageFootprintsEnabled: providerStorageFootprintsEnabled, jetbrainsIDEBasePath: jetbrainsIDEBasePath, mergeIcons: mergeIcons, switcherShowsIcons: switcherShowsIcons, mergedMenuLastSelectedWasOverview: mergedMenuLastSelectedWasOverview, mergedOverviewSelectedProvidersRaw: mergedOverviewSelectedProvidersRaw, selectedMenuProviderRaw: selectedMenuProviderRaw, providerDetectionCompleted: providerDetectionCompleted, providersSortedAlphabetically: providersSortedAlphabetically, appLanguageRaw: appLanguageRaw, terminalAppRaw: userDefaults.string(forKey: "terminalApp")) } private static func loadMenuBarMetricPreferences(userDefaults: UserDefaults) -> [String: String] { let storedPreferences = userDefaults.dictionary(forKey: "menuBarMetricPreferences") as? [String: String] ?? [:] let preferences: [String: String] = if !storedPreferences.isEmpty { storedPreferences } else if let menuBarMetricRaw = userDefaults.string(forKey: "menuBarMetricPreference"), let legacyPreference = MenuBarMetricPreference(rawValue: menuBarMetricRaw) { Dictionary( uniqueKeysWithValues: UsageProvider.allCases.map { ($0.rawValue, legacyPreference.rawValue) }) } else { [:] } let migrationKey = "antigravityTwoPoolMetricPreferenceMigrated" guard !userDefaults.bool(forKey: migrationKey) else { return preferences } // Tagged builds through v0.35 used primary=Claude, secondary=Gemini Pro, // and tertiary=Gemini Flash. Remap those meanings once to the two-pool schema. var migrated = preferences switch MenuBarMetricPreference(rawValue: migrated[UsageProvider.antigravity.rawValue] ?? "") { case .primary: migrated[UsageProvider.antigravity.rawValue] = MenuBarMetricPreference.secondary.rawValue case .secondary: migrated[UsageProvider.antigravity.rawValue] = MenuBarMetricPreference.primary.rawValue case .tertiary: migrated[UsageProvider.antigravity.rawValue] = MenuBarMetricPreference.primary.rawValueLines 1–500 of 693. Load more
import AppKit import CodexBarCore import Observation import ServiceManagement enum RefreshFrequency: String, CaseIterable, Identifiable { case manual case oneMinute case twoMinutes case fiveMinutes case fifteenMinutes case thirtyMinutes var id: String { self.rawValue } var seconds: TimeInterval? { switch self { case .manual: nil case .oneMinute: 60 case .twoMinutes: 120 case .fiveMinutes: 300 case .fifteenMinutes: 900 case .thirtyMinutes: 1800 } } var label: String { switch self { case .manual: L("refresh_manual") case .oneMinute: L("refresh_1min") case .twoMinutes: L("refresh_2min") case .fiveMinutes: L("refresh_5min") case .fifteenMinutes: L("refresh_15min") case .thirtyMinutes: L("refresh_30min") } } } enum MenuBarMetricPreference: String, CaseIterable, Identifiable { case automatic case primary case secondary case tertiary case extraUsage case average case monthlyPlan var id: String { self.rawValue } var label: String { switch self { case .automatic: L("metric_pref_automatic") case .primary: L("metric_pref_primary") case .secondary: L("metric_pref_secondary") case .tertiary: L("metric_pref_tertiary") case .extraUsage: L("metric_pref_extra_usage") case .average: L("metric_pref_average") case .monthlyPlan: L("metric_mistral_monthly_plan") } } } enum KiroMenuBarDisplayMode: String, CaseIterable, Identifiable { case automatic case hidden case creditsLeft case percentLeft case creditsAndPercent case usedAndTotal case overageCreditsWhenExhausted case overageCostWhenExhausted case overageCreditsAndCostWhenExhausted var id: String { self.rawValue } var label: String { switch self { case .automatic: "Automatic" case .hidden: "Hidden" case .creditsLeft: "Credits left" case .percentLeft: "Percent left" case .creditsAndPercent: "Credits + percent" case .usedAndTotal: "Used / total" case .overageCreditsWhenExhausted: "Overage credits at zero" case .overageCostWhenExhausted: "Overage cost at zero" case .overageCreditsAndCostWhenExhausted: "Overage credits + cost at zero" } } } enum MultiAccountMenuLayout: String, CaseIterable, Identifiable { case segmented case stacked var id: String { self.rawValue } var label: String { switch self { case .segmented: L("multi_account_layout_segmented") case .stacked: L("multi_account_layout_stacked") } } } struct CachedCodexAccountReconciliationSnapshot { let activeSource: CodexActiveSource let loadedAt: Date let snapshot: CodexAccountReconciliationSnapshot } struct CachedCodexAccountMenuProjection: Equatable { let activeSource: CodexActiveSource let loadedAt: Date let projection: CodexVisibleAccountProjection } enum CodexAccountMenuProjectionRevalidationResult: Equatable { case skipped case discarded case unchanged case updated } @MainActor @Observable final class SettingsStore { static let sharedDefaults = AppGroupSupport.sharedDefaults() static let mergedOverviewProviderLimit = 3 static let productionCodexAccountReconciliationSnapshotCacheInterval: TimeInterval = 2 static let isRunningTests: Bool = { let env = ProcessInfo.processInfo.environment if env["XCTestConfigurationFilePath"] != nil { return true } if env["TESTING_LIBRARY_VERSION"] != nil { return true } if env["SWIFT_TESTING"] != nil { return true } return NSClassFromString("XCTestCase") != nil }() #if DEBUG static var codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting: TimeInterval? #endif @ObservationIgnored let userDefaults: UserDefaults @ObservationIgnored let configStore: CodexBarConfigStore @ObservationIgnored let antigravityOAuthCredentialsStore: AntigravityOAuthCredentialsStore @ObservationIgnored var config: CodexBarConfig @ObservationIgnored var configPersistTask: Task<Void, Never>? @ObservationIgnored var configLoading = false @ObservationIgnored var tokenAccountsLoaded = false @ObservationIgnored var cachedCodexAccountReconciliationSnapshot: CachedCodexAccountReconciliationSnapshot? @ObservationIgnored var cachedCodexAccountMenuProjection: CachedCodexAccountMenuProjection? @ObservationIgnored var codexAccountReconciliationGeneration: UInt = 0 #if DEBUG @ObservationIgnored var _test_codexAccountSnapshotLoader: (@Sendable (CodexActiveSource) -> CodexAccountReconciliationSnapshot)? #endif @ObservationIgnored var mergedMenuLastSelectedWasOverviewStorage = false @ObservationIgnored var selectedMenuProviderRawStorage: String? var defaultsState: SettingsDefaultsState var configRevision: Int = 0 var providerOrder: [UsageProvider] = [] var providerEnablement: [UsageProvider: Bool] = [:] static func shouldBridgeSharedDefaults(for userDefaults: UserDefaults) -> Bool { if !self.isRunningTests { return true } if userDefaults === UserDefaults.standard { return true } if let shared = sharedDefaults, userDefaults === shared { return true } return false } init( userDefaults: UserDefaults = .standard, configStore: CodexBarConfigStore = CodexBarConfigStore(), zaiTokenStore: any ZaiTokenStoring = KeychainZaiTokenStore(), syntheticTokenStore: any SyntheticTokenStoring = KeychainSyntheticTokenStore(), codexCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "codex-cookie", promptKind: .codexCookie), claudeCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "claude-cookie", promptKind: .claudeCookie), cursorCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "cursor-cookie", promptKind: .cursorCookie), opencodeCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "opencode-cookie", promptKind: .opencodeCookie), factoryCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "factory-cookie", promptKind: .factoryCookie), minimaxCookieStore: any MiniMaxCookieStoring = KeychainMiniMaxCookieStore(), minimaxAPITokenStore: any MiniMaxAPITokenStoring = KeychainMiniMaxAPITokenStore(), kimiTokenStore: any KimiTokenStoring = KeychainKimiTokenStore(), kimiK2TokenStore: any KimiK2TokenStoring = KeychainKimiK2TokenStore(), augmentCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "augment-cookie", promptKind: .augmentCookie), ampCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "amp-cookie", promptKind: .ampCookie), copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore(), tokenAccountStore: any ProviderTokenAccountStoring = FileTokenAccountStore(), antigravityOAuthCredentialsStore: AntigravityOAuthCredentialsStore = AntigravityOAuthCredentialsStore(), performInitialProviderDetection: Bool = !SettingsStore.isRunningTests) { let appGroupID = AppGroupSupport.currentGroupID() let appGroupMigration: AppGroupSupport.MigrationResult if Self.isRunningTests { appGroupMigration = AppGroupSupport.migrateLegacyDataIfNeeded(standardDefaults: userDefaults) } else { Self.scheduleAppGroupMigration() appGroupMigration = AppGroupSupport.MigrationResult(status: .targetUnavailable) } let sharedDefaultsAvailable = Self.sharedDefaults != nil if !Self.isRunningTests { CodexBarLog.logger(LogCategories.settings).info( "App group resolved", metadata: [ "groupID": appGroupID, "sharedDefaultsAvailable": sharedDefaultsAvailable ? "1" : "0", "migrationStatus": appGroupMigration.status.rawValue, "migratedSnapshot": appGroupMigration.copiedSnapshot ? "1" : "0", "migratedDefaults": "\(appGroupMigration.copiedDefaults)", ]) } if userDefaults.object(forKey: "openAIWebAccessEnabled") == nil, let legacyOpenAIWebAccess = userDefaults.object(forKey: "openAIWebAccess") as? Bool { userDefaults.set(legacyOpenAIWebAccess, forKey: "openAIWebAccessEnabled") } let hasStoredOpenAIWebAccessPreference = userDefaults.object(forKey: "openAIWebAccessEnabled") != nil let hadExistingConfig = (try? configStore.load()) != nil let legacyStores = CodexBarConfigMigrator.LegacyStores( zaiTokenStore: zaiTokenStore, syntheticTokenStore: syntheticTokenStore, codexCookieStore: codexCookieStore, claudeCookieStore: claudeCookieStore, cursorCookieStore: cursorCookieStore, opencodeCookieStore: opencodeCookieStore, factoryCookieStore: factoryCookieStore, minimaxCookieStore: minimaxCookieStore, minimaxAPITokenStore: minimaxAPITokenStore, kimiTokenStore: kimiTokenStore, kimiK2TokenStore: kimiK2TokenStore, augmentCookieStore: augmentCookieStore, ampCookieStore: ampCookieStore, copilotTokenStore: copilotTokenStore, tokenAccountStore: tokenAccountStore) let config = CodexBarConfigMigrator.loadOrMigrate( configStore: configStore, userDefaults: userDefaults, stores: legacyStores) self.userDefaults = userDefaults self.configStore = configStore self.antigravityOAuthCredentialsStore = antigravityOAuthCredentialsStore self.config = config self.configLoading = true let defaultsState = Self.loadDefaultsState(userDefaults: userDefaults) self.defaultsState = defaultsState self.mergedMenuLastSelectedWasOverviewStorage = defaultsState.mergedMenuLastSelectedWasOverview self.selectedMenuProviderRawStorage = defaultsState.selectedMenuProviderRaw self.updateProviderState(config: config) self.configLoading = false CodexBarLog.setFileLoggingEnabled(self.debugFileLoggingEnabled) userDefaults.removeObject(forKey: "showCodexUsage") userDefaults.removeObject(forKey: "showClaudeUsage") LaunchAtLoginManager.setEnabled(self.launchAtLogin) if performInitialProviderDetection { self.runInitialProviderDetectionIfNeeded() } self.ensureAlibabaProviderAutoEnabledIfNeeded() self.applyTokenCostDefaultIfNeeded() if self.claudeUsageDataSource != .cli { if Self.isRunningTests { self.claudeWebExtrasEnabled = false } else { self.defaultsState.claudeWebExtrasEnabledRaw = false } } let resolvedOpenAIWebAccessEnabled = if hasStoredOpenAIWebAccessPreference { self.defaultsState.openAIWebAccessEnabled } else { Self.inferredInitialOpenAIWebAccessEnabled( config: config, hadExistingConfig: hadExistingConfig) } if Self.isRunningTests { self.openAIWebAccessEnabled = resolvedOpenAIWebAccessEnabled } else { self.defaultsState.openAIWebAccessEnabled = resolvedOpenAIWebAccessEnabled } KeychainAccessGate.isDisabled = self.debugDisableKeychainAccess } } extension SettingsStore { private static func scheduleAppGroupMigration() { Task.detached(priority: .utility) { let result = AppGroupSupport.migrateLegacyDataIfNeeded() CodexBarLog.logger(LogCategories.settings).info( "App group migration completed", metadata: [ "migrationStatus": result.status.rawValue, "migratedSnapshot": result.copiedSnapshot ? "1" : "0", "migratedDefaults": "\(result.copiedDefaults)", ]) } } private static func inferredInitialOpenAIWebAccessEnabled( config: CodexBarConfig, hadExistingConfig: Bool) -> Bool { guard let codex = config.providerConfig(for: .codex) else { return false } if let cookieSource = codex.cookieSource { return cookieSource.isEnabled } if codex.sanitizedCookieHeader != nil { return true } return hadExistingConfig } private static func loadDefaultsState(userDefaults: UserDefaults) -> SettingsDefaultsState { let refreshDefault = userDefaults.string(forKey: "refreshFrequency") .flatMap(RefreshFrequency.init(rawValue:)) let refreshFrequency = refreshDefault ?? .fiveMinutes if Self.isRunningTests, refreshDefault == nil { userDefaults.set(refreshFrequency.rawValue, forKey: "refreshFrequency") } let launchAtLogin = userDefaults.object(forKey: "launchAtLogin") as? Bool ?? false let debugMenuEnabled = userDefaults.object(forKey: "debugMenuEnabled") as? Bool ?? false let debugDisableKeychainAccess = Self.loadDebugDisableKeychainAccess(userDefaults: userDefaults) let debugFileLoggingEnabled = userDefaults.object(forKey: "debugFileLoggingEnabled") as? Bool ?? false let debugLogLevelRaw = userDefaults.string(forKey: "debugLogLevel") ?? CodexBarLog.Level.verbose.rawValue if Self.isRunningTests, userDefaults.string(forKey: "debugLogLevel") == nil { userDefaults.set(debugLogLevelRaw, forKey: "debugLogLevel") } let debugLoadingPatternRaw = userDefaults.string(forKey: "debugLoadingPattern") let debugKeepCLISessionsAlive = userDefaults.object(forKey: "debugKeepCLISessionsAlive") as? Bool ?? false let statusChecksEnabled = userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true let sessionQuotaDefault = userDefaults.object(forKey: "sessionQuotaNotificationsEnabled") as? Bool let sessionQuotaNotificationsEnabled = sessionQuotaDefault ?? true if Self.isRunningTests, sessionQuotaDefault == nil { userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled") } let quotaWarnings = Self.loadQuotaWarningDefaults(userDefaults: userDefaults) let quotaWarningMarkersVisibleDefault = userDefaults.object(forKey: "quotaWarningMarkersVisible") as? Bool let quotaWarningMarkersVisible = quotaWarningMarkersVisibleDefault ?? true if Self.isRunningTests, quotaWarningMarkersVisibleDefault == nil { userDefaults.set(true, forKey: "quotaWarningMarkersVisible") } let weeklyProgressWorkDays = userDefaults.object(forKey: "weeklyProgressWorkDays") as? Int let usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false let resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false let providerChangelogLinksEnabled = userDefaults.object( forKey: "providerChangelogLinksEnabled") as? Bool ?? false let menuBarShowsBrandIconWithPercent = userDefaults.object( forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false let menuBarHidesCritters = userDefaults.object(forKey: "menuBarHidesCritters") as? Bool ?? false let menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode") ?? MenuBarDisplayMode.percent.rawValue let kiroMenuBarDisplayModeRaw = userDefaults.string(forKey: "kiroMenuBarDisplayMode") ?? KiroMenuBarDisplayMode.automatic.rawValue let historicalTrackingEnabled = userDefaults.object(forKey: "historicalTrackingEnabled") as? Bool ?? false let multiAccountMenuLayoutRaw = Self.loadMultiAccountMenuLayoutRaw(userDefaults: userDefaults) let resolvedPreferences = Self.loadMenuBarMetricPreferences(userDefaults: userDefaults) let copilotBudgetExtrasEnabled = userDefaults.object(forKey: "copilotBudgetExtrasEnabled") as? Bool ?? false let copilotIconSecondaryWindowIDRaw = Self.loadCopilotIconSecondaryWindowIDRaw(userDefaults: userDefaults) let costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? false let rawCostUsageHistoryDays = userDefaults.object(forKey: "tokenCostUsageHistoryDays") as? Int ?? 30 let costUsageHistoryDays = max(1, min(365, rawCostUsageHistoryDays)) let hidePersonalInfo = userDefaults.object(forKey: "hidePersonalInfo") as? Bool ?? false let randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false let confettiOnWeeklyLimitResetsEnabled = userDefaults.object( forKey: "confettiOnWeeklyLimitResetsEnabled") as? Bool ?? false let menuBarShowsHighestUsage = userDefaults.object(forKey: "menuBarShowsHighestUsage") as? Bool ?? false let claudeOAuthKeychainPromptModeRaw = userDefaults.string(forKey: "claudeOAuthKeychainPromptMode") let claudeOAuthKeychainReadStrategyRaw = userDefaults.string(forKey: "claudeOAuthKeychainReadStrategy") let claudeWebExtrasEnabledRaw = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool let showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true if Self.isRunningTests, creditsExtrasDefault == nil { userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage") } let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool let openAIWebAccessEnabled = openAIWebAccessDefault ?? false if Self.isRunningTests, openAIWebAccessDefault == nil { userDefaults.set(false, forKey: "openAIWebAccessEnabled") } let openAIWebBatterySaverDefault = userDefaults.object(forKey: "openAIWebBatterySaverEnabled") as? Bool let openAIWebBatterySaverEnabled = openAIWebBatterySaverDefault ?? false if Self.isRunningTests, openAIWebBatterySaverDefault == nil { userDefaults.set(false, forKey: "openAIWebBatterySaverEnabled") } let providerStorageFootprintsDefault = userDefaults.object(forKey: "providerStorageFootprintsEnabled") as? Bool let providerStorageFootprintsEnabled = providerStorageFootprintsDefault ?? false if Self.isRunningTests, providerStorageFootprintsDefault == nil { userDefaults.set(false, forKey: "providerStorageFootprintsEnabled") } let jetbrainsIDEBasePath = userDefaults.string(forKey: "jetbrainsIDEBasePath") ?? "" let mergeIcons = userDefaults.object(forKey: "mergeIcons") as? Bool ?? true let switcherShowsIcons = userDefaults.object(forKey: "switcherShowsIcons") as? Bool ?? true let mergedMenuLastSelectedWasOverview = userDefaults.object( forKey: "mergedMenuLastSelectedWasOverview") as? Bool ?? false let mergedOverviewSelectedProvidersRaw = userDefaults.array( forKey: "mergedOverviewSelectedProviders") as? [String] ?? [] let selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider") let providerDetectionCompleted = userDefaults.object(forKey: "providerDetectionCompleted") as? Bool ?? false let providersSortedAlphabetically = userDefaults.object( forKey: "providersSortedAlphabetically") as? Bool ?? false let appLanguageRaw = userDefaults.string(forKey: "appLanguage") return SettingsDefaultsState( refreshFrequency: refreshFrequency, launchAtLogin: launchAtLogin, debugMenuEnabled: debugMenuEnabled, debugDisableKeychainAccess: debugDisableKeychainAccess, debugFileLoggingEnabled: debugFileLoggingEnabled, debugLogLevelRaw: debugLogLevelRaw, debugLoadingPatternRaw: debugLoadingPatternRaw, debugKeepCLISessionsAlive: debugKeepCLISessionsAlive, statusChecksEnabled: statusChecksEnabled, sessionQuotaNotificationsEnabled: sessionQuotaNotificationsEnabled, quotaWarningNotificationsEnabled: quotaWarnings.notificationsEnabled, quotaWarningThresholdsRaw: quotaWarnings.thresholdsRaw, quotaWarningSessionThresholdsRaw: quotaWarnings.sessionThresholdsRaw, quotaWarningWeeklyThresholdsRaw: quotaWarnings.weeklyThresholdsRaw, quotaWarningSessionEnabled: quotaWarnings.sessionEnabled, quotaWarningWeeklyEnabled: quotaWarnings.weeklyEnabled, quotaWarningSoundEnabled: quotaWarnings.soundEnabled, quotaWarningMarkersVisible: quotaWarningMarkersVisible, weeklyProgressWorkDays: weeklyProgressWorkDays, usageBarsShowUsed: usageBarsShowUsed, resetTimesShowAbsolute: resetTimesShowAbsolute, providerChangelogLinksEnabled: providerChangelogLinksEnabled, menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent, menuBarHidesCritters: menuBarHidesCritters, menuBarDisplayModeRaw: menuBarDisplayModeRaw, kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw, historicalTrackingEnabled: historicalTrackingEnabled, multiAccountMenuLayoutRaw: multiAccountMenuLayoutRaw, menuBarMetricPreferencesRaw: resolvedPreferences, copilotBudgetExtrasEnabled: copilotBudgetExtrasEnabled, copilotIconSecondaryWindowIDRaw: copilotIconSecondaryWindowIDRaw, costUsageEnabled: costUsageEnabled, costUsageHistoryDays: costUsageHistoryDays, hidePersonalInfo: hidePersonalInfo, randomBlinkEnabled: randomBlinkEnabled, confettiOnWeeklyLimitResetsEnabled: confettiOnWeeklyLimitResetsEnabled, menuBarShowsHighestUsage: menuBarShowsHighestUsage, claudeOAuthKeychainPromptModeRaw: claudeOAuthKeychainPromptModeRaw, claudeOAuthKeychainReadStrategyRaw: claudeOAuthKeychainReadStrategyRaw, claudeWebExtrasEnabledRaw: claudeWebExtrasEnabledRaw, showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage, openAIWebAccessEnabled: openAIWebAccessEnabled, openAIWebBatterySaverEnabled: openAIWebBatterySaverEnabled, providerStorageFootprintsEnabled: providerStorageFootprintsEnabled, jetbrainsIDEBasePath: jetbrainsIDEBasePath, mergeIcons: mergeIcons, switcherShowsIcons: switcherShowsIcons, mergedMenuLastSelectedWasOverview: mergedMenuLastSelectedWasOverview, mergedOverviewSelectedProvidersRaw: mergedOverviewSelectedProvidersRaw, selectedMenuProviderRaw: selectedMenuProviderRaw, providerDetectionCompleted: providerDetectionCompleted, providersSortedAlphabetically: providersSortedAlphabetically, appLanguageRaw: appLanguageRaw, terminalAppRaw: userDefaults.string(forKey: "terminalApp")) } private static func loadMenuBarMetricPreferences(userDefaults: UserDefaults) -> [String: String] { let storedPreferences = userDefaults.dictionary(forKey: "menuBarMetricPreferences") as? [String: String] ?? [:] let preferences: [String: String] = if !storedPreferences.isEmpty { storedPreferences } else if let menuBarMetricRaw = userDefaults.string(forKey: "menuBarMetricPreference"), let legacyPreference = MenuBarMetricPreference(rawValue: menuBarMetricRaw) { Dictionary( uniqueKeysWithValues: UsageProvider.allCases.map { ($0.rawValue, legacyPreference.rawValue) }) } else { [:] } let migrationKey = "antigravityTwoPoolMetricPreferenceMigrated" guard !userDefaults.bool(forKey: migrationKey) else { return preferences } // Tagged builds through v0.35 used primary=Claude, secondary=Gemini Pro, // and tertiary=Gemini Flash. Remap those meanings once to the two-pool schema. var migrated = preferences switch MenuBarMetricPreference(rawValue: migrated[UsageProvider.antigravity.rawValue] ?? "") { case .primary: migrated[UsageProvider.antigravity.rawValue] = MenuBarMetricPreference.secondary.rawValue case .secondary: migrated[UsageProvider.antigravity.rawValue] = MenuBarMetricPreference.primary.rawValue case .tertiary: migrated[UsageProvider.antigravity.rawValue] = MenuBarMetricPreference.primary.rawValue