import AppKitimport CodexBarCoreimport Observationimport ServiceManagement enum RefreshFrequency: String, CaseIterable, Identifiable { case manual case oneMinute case twoMinutes case fiveMinutes case fifteenMinutes 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 } } var label: String { switch self { case .manual: "Manual" case .oneMinute: "1 min" case .twoMinutes: "2 min" case .fiveMinutes: "5 min" case .fifteenMinutes: "15 min" } }} @MainActor@Observablefinal class SettingsStore { /// Persisted provider display order. /// /// Stored as raw `UsageProvider` strings so new providers can be appended automatically without breaking. private var providerOrderRaw: [String] { didSet { self.userDefaults.set(self.providerOrderRaw, forKey: "providerOrder") } } var refreshFrequency: RefreshFrequency { didSet { self.userDefaults.set(self.refreshFrequency.rawValue, forKey: "refreshFrequency") } } var launchAtLogin: Bool { didSet { self.userDefaults.set(self.launchAtLogin, forKey: "launchAtLogin") LaunchAtLoginManager.setEnabled(self.launchAtLogin) } } /// Hidden toggle to reveal debug-only menu items (enable via defaults write com.steipete.CodexBar debugMenuEnabled /// -bool YES). var debugMenuEnabled: Bool { didSet { self.userDefaults.set(self.debugMenuEnabled, forKey: "debugMenuEnabled") } } private var debugLoadingPatternRaw: String? { didSet { if let raw = self.debugLoadingPatternRaw { self.userDefaults.set(raw, forKey: "debugLoadingPattern") } else { self.userDefaults.removeObject(forKey: "debugLoadingPattern") } } } var statusChecksEnabled: Bool { didSet { self.userDefaults.set(self.statusChecksEnabled, forKey: "statusChecksEnabled") } } var sessionQuotaNotificationsEnabled: Bool { didSet { self.userDefaults.set(self.sessionQuotaNotificationsEnabled, forKey: "sessionQuotaNotificationsEnabled") } } /// When enabled, progress bars show "percent used" instead of "percent left". var usageBarsShowUsed: Bool { didSet { self.userDefaults.set(self.usageBarsShowUsed, forKey: "usageBarsShowUsed") } } /// Optional: show reset times as absolute clock values instead of countdowns. var resetTimesShowAbsolute: Bool { didSet { self.userDefaults.set(self.resetTimesShowAbsolute, forKey: "resetTimesShowAbsolute") } } /// Optional: use provider branding icons with a percentage in the menu bar. var menuBarShowsBrandIconWithPercent: Bool { didSet { self.userDefaults.set(self.menuBarShowsBrandIconWithPercent, forKey: "menuBarShowsBrandIconWithPercent") } } /// Optional: show provider cost summary from local usage logs (Codex + Claude). var costUsageEnabled: Bool { didSet { self.userDefaults.set(self.costUsageEnabled, forKey: "tokenCostUsageEnabled") } } var randomBlinkEnabled: Bool { didSet { self.userDefaults.set(self.randomBlinkEnabled, forKey: "randomBlinkEnabled") } } /// Optional: augment Claude usage with claude.ai web API (via browser cookies), /// incl. "Extra usage" spend. var claudeWebExtrasEnabled: Bool { didSet { self.userDefaults.set(self.claudeWebExtrasEnabled, forKey: "claudeWebExtrasEnabled") } } /// Optional: show Codex credits + Claude extra usage sections in the menu UI. var showOptionalCreditsAndExtraUsage: Bool { didSet { self.userDefaults.set(self.showOptionalCreditsAndExtraUsage, forKey: "showOptionalCreditsAndExtraUsage") } } /// Optional: fetch OpenAI web dashboard extras for Codex (browser cookies). var openAIWebAccessEnabled: Bool { didSet { self.userDefaults.set(self.openAIWebAccessEnabled, forKey: "openAIWebAccessEnabled") } } private var codexUsageDataSourceRaw: String? { didSet { if let raw = self.codexUsageDataSourceRaw { self.userDefaults.set(raw, forKey: "codexUsageDataSource") } else { self.userDefaults.removeObject(forKey: "codexUsageDataSource") } } } private var claudeUsageDataSourceRaw: String? { didSet { if let raw = self.claudeUsageDataSourceRaw { self.userDefaults.set(raw, forKey: "claudeUsageDataSource") } else { self.userDefaults.removeObject(forKey: "claudeUsageDataSource") } } } private var codexCookieSourceRaw: String? { didSet { if let raw = self.codexCookieSourceRaw { self.userDefaults.set(raw, forKey: "codexCookieSource") } else { self.userDefaults.removeObject(forKey: "codexCookieSource") } } } private var claudeCookieSourceRaw: String? { didSet { if let raw = self.claudeCookieSourceRaw { self.userDefaults.set(raw, forKey: "claudeCookieSource") } else { self.userDefaults.removeObject(forKey: "claudeCookieSource") } } } private var cursorCookieSourceRaw: String? { didSet { if let raw = self.cursorCookieSourceRaw { self.userDefaults.set(raw, forKey: "cursorCookieSource") } else { self.userDefaults.removeObject(forKey: "cursorCookieSource") } } } private var factoryCookieSourceRaw: String? { didSet { if let raw = self.factoryCookieSourceRaw { self.userDefaults.set(raw, forKey: "factoryCookieSource") } else { self.userDefaults.removeObject(forKey: "factoryCookieSource") } } } private var minimaxCookieSourceRaw: String? { didSet { if let raw = self.minimaxCookieSourceRaw { self.userDefaults.set(raw, forKey: "minimaxCookieSource") } else { self.userDefaults.removeObject(forKey: "minimaxCookieSource") } } } private var augmentCookieSourceRaw: String? { didSet { if let raw = self.augmentCookieSourceRaw { self.userDefaults.set(raw, forKey: "augmentCookieSource") } else { self.userDefaults.removeObject(forKey: "augmentCookieSource") } } } /// Optional: collapse provider icons into a single menu bar item with an in-menu switcher. var mergeIcons: Bool { didSet { self.userDefaults.set(self.mergeIcons, forKey: "mergeIcons") } } /// Optional: show provider icons in the in-menu switcher. var switcherShowsIcons: Bool { didSet { self.userDefaults.set(self.switcherShowsIcons, forKey: "switcherShowsIcons") } } /// z.ai API token (stored in Keychain). var zaiAPIToken: String { didSet { self.schedulePersistZaiAPIToken() } } /// Codex OpenAI cookie header (stored in Keychain). var codexCookieHeader: String { didSet { self.schedulePersistCodexCookieHeader() } } /// Claude session cookie header (stored in Keychain). var claudeCookieHeader: String { didSet { self.schedulePersistClaudeCookieHeader() } } /// Cursor session cookie header (stored in Keychain). var cursorCookieHeader: String { didSet { self.schedulePersistCursorCookieHeader() } } /// Factory session cookie header (stored in Keychain). var factoryCookieHeader: String { didSet { self.schedulePersistFactoryCookieHeader() } } /// MiniMax cookie header (stored in Keychain). var minimaxCookieHeader: String { didSet { self.schedulePersistMiniMaxCookieHeader() } } /// Augment session cookie header (stored in Keychain). var augmentCookieHeader: String { didSet { self.schedulePersistAugmentCookieHeader() } } /// Copilot API token (stored in Keychain). var copilotAPIToken: String { didSet { self.schedulePersistCopilotAPIToken() } } private var selectedMenuProviderRaw: String? { didSet { if let raw = self.selectedMenuProviderRaw { self.userDefaults.set(raw, forKey: "selectedMenuProvider") } else { self.userDefaults.removeObject(forKey: "selectedMenuProvider") } } } /// Optional override for the loading animation pattern, exposed via the Debug tab. var debugLoadingPattern: LoadingPattern? { get { self.debugLoadingPatternRaw.flatMap(LoadingPattern.init(rawValue:)) } set { self.debugLoadingPatternRaw = newValue?.rawValue } } var selectedMenuProvider: UsageProvider? { get { self.selectedMenuProviderRaw.flatMap(UsageProvider.init(rawValue:)) } set { self.selectedMenuProviderRaw = newValue?.rawValue } } var resetTimeDisplayStyle: ResetTimeDisplayStyle { self.resetTimesShowAbsolute ? .absolute : .countdown } var codexUsageDataSource: CodexUsageDataSource { get { CodexUsageDataSource(rawValue: self.codexUsageDataSourceRaw ?? "") ?? .auto } set { self.codexUsageDataSourceRaw = newValue.rawValue } } var claudeUsageDataSource: ClaudeUsageDataSource { get { ClaudeUsageDataSource(rawValue: self.claudeUsageDataSourceRaw ?? "") ?? .auto } set { self.claudeUsageDataSourceRaw = newValue.rawValue if newValue != .cli { self.claudeWebExtrasEnabled = false } } } var codexCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.codexCookieSourceRaw ?? "") ?? .auto } set { self.codexCookieSourceRaw = newValue.rawValue self.openAIWebAccessEnabled = newValue.isEnabled } } var claudeCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.claudeCookieSourceRaw ?? "") ?? .auto } set { self.claudeCookieSourceRaw = newValue.rawValue } } var cursorCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.cursorCookieSourceRaw ?? "") ?? .auto } set { self.cursorCookieSourceRaw = newValue.rawValue } } var factoryCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.factoryCookieSourceRaw ?? "") ?? .auto } set { self.factoryCookieSourceRaw = newValue.rawValue } } var minimaxCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.minimaxCookieSourceRaw ?? "") ?? .auto } set { self.minimaxCookieSourceRaw = newValue.rawValue } } var augmentCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.augmentCookieSourceRaw ?? "") ?? .auto } set { self.augmentCookieSourceRaw = newValue.rawValue } } var menuObservationToken: Int { _ = self.providerOrderRaw _ = self.refreshFrequency _ = self.launchAtLogin _ = self.debugMenuEnabled _ = self.statusChecksEnabled _ = self.sessionQuotaNotificationsEnabled _ = self.usageBarsShowUsed _ = self.resetTimesShowAbsolute _ = self.menuBarShowsBrandIconWithPercent _ = self.costUsageEnabled _ = self.randomBlinkEnabled _ = self.claudeWebExtrasEnabled _ = self.showOptionalCreditsAndExtraUsage _ = self.openAIWebAccessEnabled _ = self.codexUsageDataSource _ = self.claudeUsageDataSource _ = self.codexCookieSource _ = self.claudeCookieSource _ = self.cursorCookieSource _ = self.factoryCookieSource _ = self.minimaxCookieSource _ = self.mergeIcons _ = self.switcherShowsIcons _ = self.zaiAPIToken _ = self.codexCookieHeader _ = self.claudeCookieHeader _ = self.cursorCookieHeader _ = self.factoryCookieHeader _ = self.minimaxCookieHeader _ = self.copilotAPIToken _ = self.debugLoadingPattern _ = self.selectedMenuProvider _ = self.providerToggleRevision return 0 } private var providerDetectionCompleted: Bool { didSet { self.userDefaults.set(self.providerDetectionCompleted, forKey: "providerDetectionCompleted") } } @ObservationIgnored private let userDefaults: UserDefaults @ObservationIgnored private let toggleStore: ProviderToggleStore @ObservationIgnored private let zaiTokenStore: any ZaiTokenStoring @ObservationIgnored private var zaiTokenPersistTask: Task<Void, Never>? @ObservationIgnored private var zaiTokenLoaded = false @ObservationIgnored private var zaiTokenLoading = false @ObservationIgnored private let codexCookieStore: any CookieHeaderStoring @ObservationIgnored private var codexCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var codexCookieLoaded = false @ObservationIgnored private var codexCookieLoading = false @ObservationIgnored private let claudeCookieStore: any CookieHeaderStoring @ObservationIgnored private var claudeCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var claudeCookieLoaded = false @ObservationIgnored private var claudeCookieLoading = false @ObservationIgnored private let cursorCookieStore: any CookieHeaderStoring @ObservationIgnored private var cursorCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var cursorCookieLoaded = false @ObservationIgnored private var cursorCookieLoading = false @ObservationIgnored private let factoryCookieStore: any CookieHeaderStoring @ObservationIgnored private var factoryCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var factoryCookieLoaded = false @ObservationIgnored private var factoryCookieLoading = false @ObservationIgnored private let minimaxCookieStore: any MiniMaxCookieStoring @ObservationIgnored private var minimaxCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var minimaxCookieLoaded = false @ObservationIgnored private var minimaxCookieLoading = false @ObservationIgnored private let augmentCookieStore: any CookieHeaderStoring @ObservationIgnored private var augmentCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var augmentCookieLoaded = false @ObservationIgnored private var augmentCookieLoading = false @ObservationIgnored private let copilotTokenStore: any CopilotTokenStoring @ObservationIgnored private var copilotTokenPersistTask: Task<Void, Never>? @ObservationIgnored private var copilotTokenLoaded = false @ObservationIgnored private var copilotTokenLoading = false // Cache enablement so tight UI loops (menu bar animations) don't hit UserDefaults each tick. @ObservationIgnored private var cachedProviderEnablement: [UsageProvider: Bool] = [:] @ObservationIgnored private var cachedProviderEnablementRevision: Int = -1 @ObservationIgnored private var cachedEnabledProviders: [UsageProvider] = [] @ObservationIgnored private var cachedEnabledProvidersRevision: Int = -1 @ObservationIgnored private var cachedEnabledProvidersOrderRaw: [String] = [] // Cache order to avoid re-building sets/arrays every animation tick. @ObservationIgnored private var cachedProviderOrder: [UsageProvider] = [] @ObservationIgnored private var cachedProviderOrderRaw: [String] = [] private var providerToggleRevision: Int = 0 init( userDefaults: UserDefaults = .standard, zaiTokenStore: any ZaiTokenStoring = KeychainZaiTokenStore(), 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), factoryCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "factory-cookie", promptKind: .factoryCookie), minimaxCookieStore: any MiniMaxCookieStoring = KeychainMiniMaxCookieStore(), augmentCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "augment-cookie", promptKind: .augmentCookie), copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore()) { self.userDefaults = userDefaults self.zaiTokenStore = zaiTokenStore self.codexCookieStore = codexCookieStore self.claudeCookieStore = claudeCookieStore self.cursorCookieStore = cursorCookieStore self.factoryCookieStore = factoryCookieStore self.minimaxCookieStore = minimaxCookieStore self.augmentCookieStore = augmentCookieStore self.copilotTokenStore = copilotTokenStore self.providerOrderRaw = userDefaults.stringArray(forKey: "providerOrder") ?? [] let raw = userDefaults.string(forKey: "refreshFrequency") ?? RefreshFrequency.fiveMinutes.rawValue self.refreshFrequency = RefreshFrequency(rawValue: raw) ?? .fiveMinutes self.launchAtLogin = userDefaults.object(forKey: "launchAtLogin") as? Bool ?? false self.debugMenuEnabled = userDefaults.object(forKey: "debugMenuEnabled") as? Bool ?? false self.debugLoadingPatternRaw = userDefaults.string(forKey: "debugLoadingPattern") self.statusChecksEnabled = userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true let sessionQuotaNotificationsDefault = userDefaults.object( forKey: "sessionQuotaNotificationsEnabled") as? Bool self.sessionQuotaNotificationsEnabled = sessionQuotaNotificationsDefault ?? true if sessionQuotaNotificationsDefault == nil { self.userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled") } self.usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false self.resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false self.menuBarShowsBrandIconWithPercent = userDefaults.object( forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false self.costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? false self.randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false self.claudeWebExtrasEnabled = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool self.showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true if creditsExtrasDefault == nil { self.userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage") } let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool let openAIWebAccessEnabled = openAIWebAccessDefault ?? true self.openAIWebAccessEnabled = openAIWebAccessEnabled if openAIWebAccessDefault == nil { self.userDefaults.set(true, forKey: "openAIWebAccessEnabled") } let codexSourceRaw = userDefaults.string(forKey: "codexUsageDataSource") self.codexUsageDataSourceRaw = codexSourceRaw ?? CodexUsageDataSource.auto.rawValue let claudeSourceRaw = userDefaults.string(forKey: "claudeUsageDataSource") self.claudeUsageDataSourceRaw = claudeSourceRaw ?? ClaudeUsageDataSource.auto.rawValue let codexCookieRaw = userDefaults.string(forKey: "codexCookieSource") if let codexCookieRaw { self.codexCookieSourceRaw = codexCookieRaw } else { let fallback = openAIWebAccessEnabled ? ProviderCookieSource.auto : .off self.codexCookieSourceRaw = fallback.rawValue } self.claudeCookieSourceRaw = userDefaults.string(forKey: "claudeCookieSource") ?? ProviderCookieSource.auto.rawValue self.cursorCookieSourceRaw = userDefaults.string(forKey: "cursorCookieSource") ?? ProviderCookieSource.auto.rawValue self.factoryCookieSourceRaw = userDefaults.string(forKey: "factoryCookieSource") ?? ProviderCookieSource.auto.rawValue self.minimaxCookieSourceRaw = userDefaults.string(forKey: "minimaxCookieSource")Lines 1–500 of 1040. 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 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 } } var label: String { switch self { case .manual: "Manual" case .oneMinute: "1 min" case .twoMinutes: "2 min" case .fiveMinutes: "5 min" case .fifteenMinutes: "15 min" } } } @MainActor @Observable final class SettingsStore { /// Persisted provider display order. /// /// Stored as raw `UsageProvider` strings so new providers can be appended automatically without breaking. private var providerOrderRaw: [String] { didSet { self.userDefaults.set(self.providerOrderRaw, forKey: "providerOrder") } } var refreshFrequency: RefreshFrequency { didSet { self.userDefaults.set(self.refreshFrequency.rawValue, forKey: "refreshFrequency") } } var launchAtLogin: Bool { didSet { self.userDefaults.set(self.launchAtLogin, forKey: "launchAtLogin") LaunchAtLoginManager.setEnabled(self.launchAtLogin) } } /// Hidden toggle to reveal debug-only menu items (enable via defaults write com.steipete.CodexBar debugMenuEnabled /// -bool YES). var debugMenuEnabled: Bool { didSet { self.userDefaults.set(self.debugMenuEnabled, forKey: "debugMenuEnabled") } } private var debugLoadingPatternRaw: String? { didSet { if let raw = self.debugLoadingPatternRaw { self.userDefaults.set(raw, forKey: "debugLoadingPattern") } else { self.userDefaults.removeObject(forKey: "debugLoadingPattern") } } } var statusChecksEnabled: Bool { didSet { self.userDefaults.set(self.statusChecksEnabled, forKey: "statusChecksEnabled") } } var sessionQuotaNotificationsEnabled: Bool { didSet { self.userDefaults.set(self.sessionQuotaNotificationsEnabled, forKey: "sessionQuotaNotificationsEnabled") } } /// When enabled, progress bars show "percent used" instead of "percent left". var usageBarsShowUsed: Bool { didSet { self.userDefaults.set(self.usageBarsShowUsed, forKey: "usageBarsShowUsed") } } /// Optional: show reset times as absolute clock values instead of countdowns. var resetTimesShowAbsolute: Bool { didSet { self.userDefaults.set(self.resetTimesShowAbsolute, forKey: "resetTimesShowAbsolute") } } /// Optional: use provider branding icons with a percentage in the menu bar. var menuBarShowsBrandIconWithPercent: Bool { didSet { self.userDefaults.set(self.menuBarShowsBrandIconWithPercent, forKey: "menuBarShowsBrandIconWithPercent") } } /// Optional: show provider cost summary from local usage logs (Codex + Claude). var costUsageEnabled: Bool { didSet { self.userDefaults.set(self.costUsageEnabled, forKey: "tokenCostUsageEnabled") } } var randomBlinkEnabled: Bool { didSet { self.userDefaults.set(self.randomBlinkEnabled, forKey: "randomBlinkEnabled") } } /// Optional: augment Claude usage with claude.ai web API (via browser cookies), /// incl. "Extra usage" spend. var claudeWebExtrasEnabled: Bool { didSet { self.userDefaults.set(self.claudeWebExtrasEnabled, forKey: "claudeWebExtrasEnabled") } } /// Optional: show Codex credits + Claude extra usage sections in the menu UI. var showOptionalCreditsAndExtraUsage: Bool { didSet { self.userDefaults.set(self.showOptionalCreditsAndExtraUsage, forKey: "showOptionalCreditsAndExtraUsage") } } /// Optional: fetch OpenAI web dashboard extras for Codex (browser cookies). var openAIWebAccessEnabled: Bool { didSet { self.userDefaults.set(self.openAIWebAccessEnabled, forKey: "openAIWebAccessEnabled") } } private var codexUsageDataSourceRaw: String? { didSet { if let raw = self.codexUsageDataSourceRaw { self.userDefaults.set(raw, forKey: "codexUsageDataSource") } else { self.userDefaults.removeObject(forKey: "codexUsageDataSource") } } } private var claudeUsageDataSourceRaw: String? { didSet { if let raw = self.claudeUsageDataSourceRaw { self.userDefaults.set(raw, forKey: "claudeUsageDataSource") } else { self.userDefaults.removeObject(forKey: "claudeUsageDataSource") } } } private var codexCookieSourceRaw: String? { didSet { if let raw = self.codexCookieSourceRaw { self.userDefaults.set(raw, forKey: "codexCookieSource") } else { self.userDefaults.removeObject(forKey: "codexCookieSource") } } } private var claudeCookieSourceRaw: String? { didSet { if let raw = self.claudeCookieSourceRaw { self.userDefaults.set(raw, forKey: "claudeCookieSource") } else { self.userDefaults.removeObject(forKey: "claudeCookieSource") } } } private var cursorCookieSourceRaw: String? { didSet { if let raw = self.cursorCookieSourceRaw { self.userDefaults.set(raw, forKey: "cursorCookieSource") } else { self.userDefaults.removeObject(forKey: "cursorCookieSource") } } } private var factoryCookieSourceRaw: String? { didSet { if let raw = self.factoryCookieSourceRaw { self.userDefaults.set(raw, forKey: "factoryCookieSource") } else { self.userDefaults.removeObject(forKey: "factoryCookieSource") } } } private var minimaxCookieSourceRaw: String? { didSet { if let raw = self.minimaxCookieSourceRaw { self.userDefaults.set(raw, forKey: "minimaxCookieSource") } else { self.userDefaults.removeObject(forKey: "minimaxCookieSource") } } } private var augmentCookieSourceRaw: String? { didSet { if let raw = self.augmentCookieSourceRaw { self.userDefaults.set(raw, forKey: "augmentCookieSource") } else { self.userDefaults.removeObject(forKey: "augmentCookieSource") } } } /// Optional: collapse provider icons into a single menu bar item with an in-menu switcher. var mergeIcons: Bool { didSet { self.userDefaults.set(self.mergeIcons, forKey: "mergeIcons") } } /// Optional: show provider icons in the in-menu switcher. var switcherShowsIcons: Bool { didSet { self.userDefaults.set(self.switcherShowsIcons, forKey: "switcherShowsIcons") } } /// z.ai API token (stored in Keychain). var zaiAPIToken: String { didSet { self.schedulePersistZaiAPIToken() } } /// Codex OpenAI cookie header (stored in Keychain). var codexCookieHeader: String { didSet { self.schedulePersistCodexCookieHeader() } } /// Claude session cookie header (stored in Keychain). var claudeCookieHeader: String { didSet { self.schedulePersistClaudeCookieHeader() } } /// Cursor session cookie header (stored in Keychain). var cursorCookieHeader: String { didSet { self.schedulePersistCursorCookieHeader() } } /// Factory session cookie header (stored in Keychain). var factoryCookieHeader: String { didSet { self.schedulePersistFactoryCookieHeader() } } /// MiniMax cookie header (stored in Keychain). var minimaxCookieHeader: String { didSet { self.schedulePersistMiniMaxCookieHeader() } } /// Augment session cookie header (stored in Keychain). var augmentCookieHeader: String { didSet { self.schedulePersistAugmentCookieHeader() } } /// Copilot API token (stored in Keychain). var copilotAPIToken: String { didSet { self.schedulePersistCopilotAPIToken() } } private var selectedMenuProviderRaw: String? { didSet { if let raw = self.selectedMenuProviderRaw { self.userDefaults.set(raw, forKey: "selectedMenuProvider") } else { self.userDefaults.removeObject(forKey: "selectedMenuProvider") } } } /// Optional override for the loading animation pattern, exposed via the Debug tab. var debugLoadingPattern: LoadingPattern? { get { self.debugLoadingPatternRaw.flatMap(LoadingPattern.init(rawValue:)) } set { self.debugLoadingPatternRaw = newValue?.rawValue } } var selectedMenuProvider: UsageProvider? { get { self.selectedMenuProviderRaw.flatMap(UsageProvider.init(rawValue:)) } set { self.selectedMenuProviderRaw = newValue?.rawValue } } var resetTimeDisplayStyle: ResetTimeDisplayStyle { self.resetTimesShowAbsolute ? .absolute : .countdown } var codexUsageDataSource: CodexUsageDataSource { get { CodexUsageDataSource(rawValue: self.codexUsageDataSourceRaw ?? "") ?? .auto } set { self.codexUsageDataSourceRaw = newValue.rawValue } } var claudeUsageDataSource: ClaudeUsageDataSource { get { ClaudeUsageDataSource(rawValue: self.claudeUsageDataSourceRaw ?? "") ?? .auto } set { self.claudeUsageDataSourceRaw = newValue.rawValue if newValue != .cli { self.claudeWebExtrasEnabled = false } } } var codexCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.codexCookieSourceRaw ?? "") ?? .auto } set { self.codexCookieSourceRaw = newValue.rawValue self.openAIWebAccessEnabled = newValue.isEnabled } } var claudeCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.claudeCookieSourceRaw ?? "") ?? .auto } set { self.claudeCookieSourceRaw = newValue.rawValue } } var cursorCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.cursorCookieSourceRaw ?? "") ?? .auto } set { self.cursorCookieSourceRaw = newValue.rawValue } } var factoryCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.factoryCookieSourceRaw ?? "") ?? .auto } set { self.factoryCookieSourceRaw = newValue.rawValue } } var minimaxCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.minimaxCookieSourceRaw ?? "") ?? .auto } set { self.minimaxCookieSourceRaw = newValue.rawValue } } var augmentCookieSource: ProviderCookieSource { get { ProviderCookieSource(rawValue: self.augmentCookieSourceRaw ?? "") ?? .auto } set { self.augmentCookieSourceRaw = newValue.rawValue } } var menuObservationToken: Int { _ = self.providerOrderRaw _ = self.refreshFrequency _ = self.launchAtLogin _ = self.debugMenuEnabled _ = self.statusChecksEnabled _ = self.sessionQuotaNotificationsEnabled _ = self.usageBarsShowUsed _ = self.resetTimesShowAbsolute _ = self.menuBarShowsBrandIconWithPercent _ = self.costUsageEnabled _ = self.randomBlinkEnabled _ = self.claudeWebExtrasEnabled _ = self.showOptionalCreditsAndExtraUsage _ = self.openAIWebAccessEnabled _ = self.codexUsageDataSource _ = self.claudeUsageDataSource _ = self.codexCookieSource _ = self.claudeCookieSource _ = self.cursorCookieSource _ = self.factoryCookieSource _ = self.minimaxCookieSource _ = self.mergeIcons _ = self.switcherShowsIcons _ = self.zaiAPIToken _ = self.codexCookieHeader _ = self.claudeCookieHeader _ = self.cursorCookieHeader _ = self.factoryCookieHeader _ = self.minimaxCookieHeader _ = self.copilotAPIToken _ = self.debugLoadingPattern _ = self.selectedMenuProvider _ = self.providerToggleRevision return 0 } private var providerDetectionCompleted: Bool { didSet { self.userDefaults.set(self.providerDetectionCompleted, forKey: "providerDetectionCompleted") } } @ObservationIgnored private let userDefaults: UserDefaults @ObservationIgnored private let toggleStore: ProviderToggleStore @ObservationIgnored private let zaiTokenStore: any ZaiTokenStoring @ObservationIgnored private var zaiTokenPersistTask: Task<Void, Never>? @ObservationIgnored private var zaiTokenLoaded = false @ObservationIgnored private var zaiTokenLoading = false @ObservationIgnored private let codexCookieStore: any CookieHeaderStoring @ObservationIgnored private var codexCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var codexCookieLoaded = false @ObservationIgnored private var codexCookieLoading = false @ObservationIgnored private let claudeCookieStore: any CookieHeaderStoring @ObservationIgnored private var claudeCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var claudeCookieLoaded = false @ObservationIgnored private var claudeCookieLoading = false @ObservationIgnored private let cursorCookieStore: any CookieHeaderStoring @ObservationIgnored private var cursorCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var cursorCookieLoaded = false @ObservationIgnored private var cursorCookieLoading = false @ObservationIgnored private let factoryCookieStore: any CookieHeaderStoring @ObservationIgnored private var factoryCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var factoryCookieLoaded = false @ObservationIgnored private var factoryCookieLoading = false @ObservationIgnored private let minimaxCookieStore: any MiniMaxCookieStoring @ObservationIgnored private var minimaxCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var minimaxCookieLoaded = false @ObservationIgnored private var minimaxCookieLoading = false @ObservationIgnored private let augmentCookieStore: any CookieHeaderStoring @ObservationIgnored private var augmentCookiePersistTask: Task<Void, Never>? @ObservationIgnored private var augmentCookieLoaded = false @ObservationIgnored private var augmentCookieLoading = false @ObservationIgnored private let copilotTokenStore: any CopilotTokenStoring @ObservationIgnored private var copilotTokenPersistTask: Task<Void, Never>? @ObservationIgnored private var copilotTokenLoaded = false @ObservationIgnored private var copilotTokenLoading = false // Cache enablement so tight UI loops (menu bar animations) don't hit UserDefaults each tick. @ObservationIgnored private var cachedProviderEnablement: [UsageProvider: Bool] = [:] @ObservationIgnored private var cachedProviderEnablementRevision: Int = -1 @ObservationIgnored private var cachedEnabledProviders: [UsageProvider] = [] @ObservationIgnored private var cachedEnabledProvidersRevision: Int = -1 @ObservationIgnored private var cachedEnabledProvidersOrderRaw: [String] = [] // Cache order to avoid re-building sets/arrays every animation tick. @ObservationIgnored private var cachedProviderOrder: [UsageProvider] = [] @ObservationIgnored private var cachedProviderOrderRaw: [String] = [] private var providerToggleRevision: Int = 0 init( userDefaults: UserDefaults = .standard, zaiTokenStore: any ZaiTokenStoring = KeychainZaiTokenStore(), 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), factoryCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "factory-cookie", promptKind: .factoryCookie), minimaxCookieStore: any MiniMaxCookieStoring = KeychainMiniMaxCookieStore(), augmentCookieStore: any CookieHeaderStoring = KeychainCookieHeaderStore( account: "augment-cookie", promptKind: .augmentCookie), copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore()) { self.userDefaults = userDefaults self.zaiTokenStore = zaiTokenStore self.codexCookieStore = codexCookieStore self.claudeCookieStore = claudeCookieStore self.cursorCookieStore = cursorCookieStore self.factoryCookieStore = factoryCookieStore self.minimaxCookieStore = minimaxCookieStore self.augmentCookieStore = augmentCookieStore self.copilotTokenStore = copilotTokenStore self.providerOrderRaw = userDefaults.stringArray(forKey: "providerOrder") ?? [] let raw = userDefaults.string(forKey: "refreshFrequency") ?? RefreshFrequency.fiveMinutes.rawValue self.refreshFrequency = RefreshFrequency(rawValue: raw) ?? .fiveMinutes self.launchAtLogin = userDefaults.object(forKey: "launchAtLogin") as? Bool ?? false self.debugMenuEnabled = userDefaults.object(forKey: "debugMenuEnabled") as? Bool ?? false self.debugLoadingPatternRaw = userDefaults.string(forKey: "debugLoadingPattern") self.statusChecksEnabled = userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true let sessionQuotaNotificationsDefault = userDefaults.object( forKey: "sessionQuotaNotificationsEnabled") as? Bool self.sessionQuotaNotificationsEnabled = sessionQuotaNotificationsDefault ?? true if sessionQuotaNotificationsDefault == nil { self.userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled") } self.usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false self.resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false self.menuBarShowsBrandIconWithPercent = userDefaults.object( forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false self.costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? false self.randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false self.claudeWebExtrasEnabled = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool self.showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true if creditsExtrasDefault == nil { self.userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage") } let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool let openAIWebAccessEnabled = openAIWebAccessDefault ?? true self.openAIWebAccessEnabled = openAIWebAccessEnabled if openAIWebAccessDefault == nil { self.userDefaults.set(true, forKey: "openAIWebAccessEnabled") } let codexSourceRaw = userDefaults.string(forKey: "codexUsageDataSource") self.codexUsageDataSourceRaw = codexSourceRaw ?? CodexUsageDataSource.auto.rawValue let claudeSourceRaw = userDefaults.string(forKey: "claudeUsageDataSource") self.claudeUsageDataSourceRaw = claudeSourceRaw ?? ClaudeUsageDataSource.auto.rawValue let codexCookieRaw = userDefaults.string(forKey: "codexCookieSource") if let codexCookieRaw { self.codexCookieSourceRaw = codexCookieRaw } else { let fallback = openAIWebAccessEnabled ? ProviderCookieSource.auto : .off self.codexCookieSourceRaw = fallback.rawValue } self.claudeCookieSourceRaw = userDefaults.string(forKey: "claudeCookieSource") ?? ProviderCookieSource.auto.rawValue self.cursorCookieSourceRaw = userDefaults.string(forKey: "cursorCookieSource") ?? ProviderCookieSource.auto.rawValue self.factoryCookieSourceRaw = userDefaults.string(forKey: "factoryCookieSource") ?? ProviderCookieSource.auto.rawValue self.minimaxCookieSourceRaw = userDefaults.string(forKey: "minimaxCookieSource")