chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,868 @@
|
||||
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
|
||||
/// Newest/most advanced option; kept last so the picker still lists the fixed intervals
|
||||
/// in ascending cadence order before it.
|
||||
case adaptive
|
||||
|
||||
var id: String {
|
||||
self.rawValue
|
||||
}
|
||||
|
||||
/// nil for `.manual` (no timer) and `.adaptive` (delay is computed per tick by
|
||||
/// `AdaptiveRefreshPolicy`, not a fixed interval).
|
||||
var seconds: TimeInterval? {
|
||||
switch self {
|
||||
case .manual: nil
|
||||
case .oneMinute: 60
|
||||
case .twoMinutes: 120
|
||||
case .fiveMinutes: 300
|
||||
case .fifteenMinutes: 900
|
||||
case .thirtyMinutes: 1800
|
||||
case .adaptive: nil
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
case .adaptive: L("refresh_adaptive")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MenuBarMetricPreference: String, CaseIterable, Identifiable {
|
||||
case automatic
|
||||
case primary
|
||||
case secondary
|
||||
case primaryAndSecondary
|
||||
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 .primaryAndSecondary: "\(L("metric_pref_primary")) + \(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: L("Automatic")
|
||||
case .hidden: L("Hidden")
|
||||
case .creditsLeft: L("Credits left")
|
||||
case .percentLeft: L("Percent left")
|
||||
case .creditsAndPercent: L("Credits + percent")
|
||||
case .usedAndTotal: L("Used / total")
|
||||
case .overageCreditsWhenExhausted: L("Overage credits at zero")
|
||||
case .overageCostWhenExhausted: L("Overage cost at zero")
|
||||
case .overageCreditsAndCostWhenExhausted: L("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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CostSummaryDisplayStyle: String, CaseIterable, Identifiable {
|
||||
case inlineSummary
|
||||
case costSubmenu
|
||||
case both
|
||||
|
||||
var id: String {
|
||||
self.rawValue
|
||||
}
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .inlineSummary: L("cost_summary_style_inline")
|
||||
case .costSubmenu: L("cost_summary_style_submenu")
|
||||
case .both: L("cost_summary_style_both")
|
||||
}
|
||||
}
|
||||
|
||||
var helpText: String {
|
||||
switch self {
|
||||
case .inlineSummary: L("cost_summary_style_inline_help")
|
||||
case .costSubmenu: L("cost_summary_style_submenu_help")
|
||||
case .both: L("cost_summary_style_both_help")
|
||||
}
|
||||
}
|
||||
|
||||
var showsInlineSummary: Bool {
|
||||
self != .costSubmenu
|
||||
}
|
||||
|
||||
var showsCostSubmenu: Bool {
|
||||
self != .inlineSummary
|
||||
}
|
||||
}
|
||||
|
||||
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 backgroundWorkSettingsRevision: Int = 0
|
||||
var providerOrder: [UsageProvider] = []
|
||||
var providerEnablement: [UsageProvider: Bool] = [:]
|
||||
@ObservationIgnored var providerEnablementRevisions: [UsageProvider: UInt64] = [:]
|
||||
@ObservationIgnored var providerConfigRevisions: [UsageProvider: UInt64] = [:]
|
||||
@ObservationIgnored var providerConfigFingerprints: [UsageProvider: Data] = [:]
|
||||
|
||||
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 struct NotificationDefaults {
|
||||
let statusChecksEnabled: Bool
|
||||
let sessionQuotaNotificationsEnabled: Bool
|
||||
let predictivePaceWarningNotificationsEnabled: Bool
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
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 refreshAllProvidersOnMenuOpen = userDefaults.object(
|
||||
forKey: "refreshAllProvidersOnMenuOpen") as? Bool ?? false
|
||||
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 notificationDefaults = Self.loadNotificationDefaults(userDefaults: userDefaults)
|
||||
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 menuBarShowsResetTimeWhenExhausted = userDefaults.object(
|
||||
forKey: "menuBarShowsResetTimeWhenExhausted") as? Bool ?? false
|
||||
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 costComparisonPeriodsEnabled = userDefaults.object(
|
||||
forKey: "costComparisonPeriodsEnabled") as? Bool ?? false
|
||||
let costSummaryDisplayStyleRaw = Self.loadCostSummaryDisplayStyleRaw(
|
||||
userDefaults: userDefaults,
|
||||
costUsageEnabled: costUsageEnabled)
|
||||
let hidePersonalInfo = userDefaults.object(forKey: "hidePersonalInfo") as? Bool ?? false
|
||||
let randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false
|
||||
let confettiOnReset = Self.loadConfettiOnResetDefaults(userDefaults: userDefaults)
|
||||
let menuBarShowsHighestUsage = userDefaults.object(forKey: "menuBarShowsHighestUsage") as? Bool ?? false
|
||||
let claudeOAuthKeychainReadStrategyRaw = Self.loadClaudeOAuthKeychainReadStrategyRaw(userDefaults: userDefaults)
|
||||
let claudeOAuthKeychainPromptModeRaw = userDefaults.string(forKey: "claudeOAuthKeychainPromptMode")
|
||||
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 codexSparkUsageVisibleDefault = userDefaults.object(forKey: "codexSparkUsageVisible") as? Bool
|
||||
let codexSparkUsageVisible = codexSparkUsageVisibleDefault ?? true
|
||||
if Self.isRunningTests, codexSparkUsageVisibleDefault == nil {
|
||||
userDefaults.set(true, forKey: "codexSparkUsageVisible")
|
||||
}
|
||||
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")
|
||||
let agentSessionsEnabled = userDefaults.object(forKey: "agentSessionsEnabled") as? Bool ?? false
|
||||
let agentSessionsManualHosts = userDefaults.string(forKey: "agentSessionsManualHosts") ?? ""
|
||||
return SettingsDefaultsState(
|
||||
refreshFrequency: refreshFrequency,
|
||||
refreshAllProvidersOnMenuOpen: refreshAllProvidersOnMenuOpen,
|
||||
launchAtLogin: launchAtLogin,
|
||||
debugMenuEnabled: debugMenuEnabled,
|
||||
debugDisableKeychainAccess: debugDisableKeychainAccess,
|
||||
debugFileLoggingEnabled: debugFileLoggingEnabled,
|
||||
debugLogLevelRaw: debugLogLevelRaw,
|
||||
debugLoadingPatternRaw: debugLoadingPatternRaw,
|
||||
debugKeepCLISessionsAlive: debugKeepCLISessionsAlive,
|
||||
statusChecksEnabled: notificationDefaults.statusChecksEnabled,
|
||||
sessionQuotaNotificationsEnabled: notificationDefaults.sessionQuotaNotificationsEnabled,
|
||||
quotaWarningNotificationsEnabled: quotaWarnings.notificationsEnabled,
|
||||
predictivePaceWarningNotificationsEnabled: notificationDefaults.predictivePaceWarningNotificationsEnabled,
|
||||
quotaWarningThresholdsRaw: quotaWarnings.thresholdsRaw,
|
||||
quotaWarningSessionThresholdsRaw: quotaWarnings.sessionThresholdsRaw,
|
||||
quotaWarningWeeklyThresholdsRaw: quotaWarnings.weeklyThresholdsRaw,
|
||||
quotaWarningSessionEnabled: quotaWarnings.sessionEnabled,
|
||||
quotaWarningWeeklyEnabled: quotaWarnings.weeklyEnabled,
|
||||
quotaWarningSoundEnabled: quotaWarnings.soundEnabled,
|
||||
quotaWarningOnScreenAlertEnabled: quotaWarnings.onScreenAlertEnabled,
|
||||
quotaWarningMarkersVisible: quotaWarningMarkersVisible,
|
||||
weeklyProgressWorkDays: weeklyProgressWorkDays,
|
||||
usageBarsShowUsed: usageBarsShowUsed,
|
||||
resetTimesShowAbsolute: resetTimesShowAbsolute,
|
||||
providerChangelogLinksEnabled: providerChangelogLinksEnabled,
|
||||
menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent,
|
||||
menuBarHidesCritters: menuBarHidesCritters,
|
||||
menuBarDisplayModeRaw: menuBarDisplayModeRaw,
|
||||
menuBarShowsResetTimeWhenExhausted: menuBarShowsResetTimeWhenExhausted,
|
||||
kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw,
|
||||
historicalTrackingEnabled: historicalTrackingEnabled,
|
||||
multiAccountMenuLayoutRaw: multiAccountMenuLayoutRaw,
|
||||
menuBarMetricPreferencesRaw: resolvedPreferences,
|
||||
copilotBudgetExtrasEnabled: copilotBudgetExtrasEnabled,
|
||||
copilotIconSecondaryWindowIDRaw: copilotIconSecondaryWindowIDRaw,
|
||||
costUsageEnabled: costUsageEnabled,
|
||||
costUsageHistoryDays: costUsageHistoryDays,
|
||||
costComparisonPeriodsEnabled: costComparisonPeriodsEnabled,
|
||||
costSummaryDisplayStyleRaw: costSummaryDisplayStyleRaw,
|
||||
hidePersonalInfo: hidePersonalInfo,
|
||||
randomBlinkEnabled: randomBlinkEnabled,
|
||||
confettiOnSessionLimitResetsEnabled: confettiOnReset.session,
|
||||
confettiOnWeeklyLimitResetsEnabled: confettiOnReset.weekly,
|
||||
menuBarShowsHighestUsage: menuBarShowsHighestUsage,
|
||||
claudeOAuthKeychainPromptModeRaw: claudeOAuthKeychainPromptModeRaw,
|
||||
claudeOAuthKeychainReadStrategyRaw: claudeOAuthKeychainReadStrategyRaw,
|
||||
claudeWebExtrasEnabledRaw: claudeWebExtrasEnabledRaw,
|
||||
showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage,
|
||||
codexSparkUsageVisible: codexSparkUsageVisible,
|
||||
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"),
|
||||
agentSessionsEnabled: agentSessionsEnabled,
|
||||
agentSessionsManualHosts: agentSessionsManualHosts)
|
||||
}
|
||||
|
||||
private static func loadNotificationDefaults(userDefaults: UserDefaults) -> NotificationDefaults {
|
||||
NotificationDefaults(
|
||||
statusChecksEnabled: userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true,
|
||||
sessionQuotaNotificationsEnabled: self.loadSessionQuotaNotificationsDefault(userDefaults: userDefaults),
|
||||
predictivePaceWarningNotificationsEnabled: userDefaults.object(
|
||||
forKey: "predictivePaceWarningNotificationsEnabled") as? Bool ?? false)
|
||||
}
|
||||
|
||||
private static func loadCostSummaryDisplayStyleRaw(
|
||||
userDefaults: UserDefaults,
|
||||
costUsageEnabled: Bool) -> String
|
||||
{
|
||||
if let storedCostSummaryDisplayStyle = userDefaults.string(forKey: "costSummaryDisplayStyle"),
|
||||
CostSummaryDisplayStyle(rawValue: storedCostSummaryDisplayStyle) != nil
|
||||
{
|
||||
return storedCostSummaryDisplayStyle
|
||||
}
|
||||
let migratedStyle = CostSummaryDisplayStyle.both.rawValue
|
||||
if costUsageEnabled || userDefaults.object(forKey: "costSummaryDisplayStyle") != nil {
|
||||
userDefaults.set(migratedStyle, forKey: "costSummaryDisplayStyle")
|
||||
}
|
||||
return migratedStyle
|
||||
}
|
||||
|
||||
private static func loadClaudeOAuthKeychainReadStrategyRaw(userDefaults: UserDefaults) -> String? {
|
||||
let key = "claudeOAuthKeychainReadStrategy"
|
||||
guard let raw = userDefaults.string(forKey: key) else { return nil }
|
||||
guard let strategy = ClaudeOAuthKeychainReadStrategy(rawValue: raw) else { return raw }
|
||||
guard strategy == .securityCLIExperimental else { return raw }
|
||||
|
||||
let migrated = ClaudeOAuthKeychainReadStrategy.securityFramework.rawValue
|
||||
userDefaults.set(migrated, forKey: key)
|
||||
let promptModeKey = "claudeOAuthKeychainPromptMode"
|
||||
if userDefaults.string(forKey: promptModeKey) == nil {
|
||||
userDefaults.set(ClaudeOAuthKeychainPromptMode.never.rawValue, forKey: promptModeKey)
|
||||
}
|
||||
return migrated
|
||||
}
|
||||
|
||||
private static func loadConfettiOnResetDefaults(userDefaults: UserDefaults) -> (session: Bool, weekly: Bool) {
|
||||
(
|
||||
session: userDefaults.object(forKey: "confettiOnSessionLimitResetsEnabled") as? Bool ?? false,
|
||||
weekly: userDefaults.object(forKey: "confettiOnWeeklyLimitResetsEnabled") as? Bool ?? false)
|
||||
}
|
||||
|
||||
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
|
||||
case .automatic, .primaryAndSecondary, .extraUsage, .average, .monthlyPlan, .none:
|
||||
break
|
||||
}
|
||||
userDefaults.set(migrated, forKey: "menuBarMetricPreferences")
|
||||
userDefaults.set(true, forKey: migrationKey)
|
||||
return migrated
|
||||
}
|
||||
|
||||
private static func loadMultiAccountMenuLayoutRaw(userDefaults: UserDefaults) -> String {
|
||||
if let layout = userDefaults.string(forKey: "multiAccountMenuLayout") {
|
||||
return layout
|
||||
}
|
||||
let legacyShowAll = userDefaults.object(forKey: "showAllTokenAccountsInMenu") as? Bool ?? false
|
||||
return legacyShowAll ? MultiAccountMenuLayout.stacked.rawValue : MultiAccountMenuLayout.segmented.rawValue
|
||||
}
|
||||
|
||||
private static func loadCopilotIconSecondaryWindowIDRaw(userDefaults: UserDefaults) -> String {
|
||||
userDefaults.string(forKey: "copilotIconSecondaryWindowID") ?? CopilotIconSecondaryWindowSelection.chat
|
||||
}
|
||||
|
||||
private static func loadDebugDisableKeychainAccess(userDefaults: UserDefaults) -> Bool {
|
||||
if let stored = userDefaults.object(forKey: "debugDisableKeychainAccess") as? Bool {
|
||||
return stored
|
||||
}
|
||||
if Self.shouldBridgeSharedDefaults(for: userDefaults),
|
||||
let shared = Self.sharedDefaults?.object(forKey: "debugDisableKeychainAccess") as? Bool
|
||||
{
|
||||
if Self.isRunningTests {
|
||||
userDefaults.set(shared, forKey: "debugDisableKeychainAccess")
|
||||
}
|
||||
return shared
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private struct LoadedQuotaWarningDefaults {
|
||||
var notificationsEnabled: Bool
|
||||
var thresholdsRaw: [Int]
|
||||
var sessionThresholdsRaw: [Int]
|
||||
var weeklyThresholdsRaw: [Int]
|
||||
var sessionEnabled: Bool
|
||||
var weeklyEnabled: Bool
|
||||
var soundEnabled: Bool
|
||||
var onScreenAlertEnabled: Bool
|
||||
}
|
||||
|
||||
private static func loadSessionQuotaNotificationsDefault(userDefaults: UserDefaults) -> Bool {
|
||||
let stored = userDefaults.object(forKey: "sessionQuotaNotificationsEnabled") as? Bool
|
||||
if Self.isRunningTests, stored == nil {
|
||||
userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled")
|
||||
}
|
||||
return stored ?? true
|
||||
}
|
||||
|
||||
private static func loadQuotaWarningDefaults(userDefaults: UserDefaults) -> LoadedQuotaWarningDefaults {
|
||||
let notificationsEnabled = userDefaults.object(forKey: "quotaWarningNotificationsEnabled") as? Bool ?? false
|
||||
let rawThresholds = userDefaults.array(forKey: "quotaWarningThresholds") as? [Int]
|
||||
let thresholdsRaw = QuotaWarningThresholds.sanitized(rawThresholds ?? QuotaWarningThresholds.defaults)
|
||||
if Self.isRunningTests, rawThresholds != thresholdsRaw {
|
||||
userDefaults.set(thresholdsRaw, forKey: "quotaWarningThresholds")
|
||||
}
|
||||
let rawSessionThresholds = userDefaults.array(forKey: "quotaWarningSessionThresholds") as? [Int]
|
||||
let sessionThresholdsRaw = QuotaWarningThresholds.sanitized(rawSessionThresholds ?? thresholdsRaw)
|
||||
if Self.isRunningTests, rawSessionThresholds != sessionThresholdsRaw {
|
||||
userDefaults.set(sessionThresholdsRaw, forKey: "quotaWarningSessionThresholds")
|
||||
}
|
||||
let rawWeeklyThresholds = userDefaults.array(forKey: "quotaWarningWeeklyThresholds") as? [Int]
|
||||
let weeklyThresholdsRaw = QuotaWarningThresholds.sanitized(rawWeeklyThresholds ?? thresholdsRaw)
|
||||
if Self.isRunningTests, rawWeeklyThresholds != weeklyThresholdsRaw {
|
||||
userDefaults.set(weeklyThresholdsRaw, forKey: "quotaWarningWeeklyThresholds")
|
||||
}
|
||||
|
||||
let sessionDefault = userDefaults.object(forKey: "quotaWarningSessionEnabled") as? Bool
|
||||
let sessionEnabled = sessionDefault ?? true
|
||||
if Self.isRunningTests, sessionDefault == nil {
|
||||
userDefaults.set(true, forKey: "quotaWarningSessionEnabled")
|
||||
}
|
||||
|
||||
let weeklyDefault = userDefaults.object(forKey: "quotaWarningWeeklyEnabled") as? Bool
|
||||
let weeklyEnabled = weeklyDefault ?? true
|
||||
if Self.isRunningTests, weeklyDefault == nil {
|
||||
userDefaults.set(true, forKey: "quotaWarningWeeklyEnabled")
|
||||
}
|
||||
|
||||
let soundDefault = userDefaults.object(forKey: "quotaWarningSoundEnabled") as? Bool
|
||||
let soundEnabled = soundDefault ?? true
|
||||
if Self.isRunningTests, soundDefault == nil {
|
||||
userDefaults.set(true, forKey: "quotaWarningSoundEnabled")
|
||||
}
|
||||
|
||||
let onScreenAlertDefault = userDefaults.object(forKey: "quotaWarningOnScreenAlertEnabled") as? Bool
|
||||
let onScreenAlertEnabled = onScreenAlertDefault ?? false
|
||||
if Self.isRunningTests, onScreenAlertDefault == nil {
|
||||
userDefaults.set(false, forKey: "quotaWarningOnScreenAlertEnabled")
|
||||
}
|
||||
|
||||
return LoadedQuotaWarningDefaults(
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
thresholdsRaw: thresholdsRaw,
|
||||
sessionThresholdsRaw: sessionThresholdsRaw,
|
||||
weeklyThresholdsRaw: weeklyThresholdsRaw,
|
||||
sessionEnabled: sessionEnabled,
|
||||
weeklyEnabled: weeklyEnabled,
|
||||
soundEnabled: soundEnabled,
|
||||
onScreenAlertEnabled: onScreenAlertEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsStore {
|
||||
var configSnapshot: CodexBarConfig {
|
||||
_ = self.configRevision
|
||||
return self.config
|
||||
}
|
||||
|
||||
func updateProviderState(config: CodexBarConfig) {
|
||||
let rawOrder = config.providers.map(\.id.rawValue)
|
||||
self.providerOrder = Self.effectiveProviderOrder(raw: rawOrder)
|
||||
let metadata = ProviderDescriptorRegistry.metadata
|
||||
var enablement: [UsageProvider: Bool] = [:]
|
||||
enablement.reserveCapacity(metadata.count)
|
||||
for provider in UsageProvider.allCases {
|
||||
let defaultEnabled = metadata[provider]?.defaultEnabled ?? false
|
||||
let providerConfig = config.providerConfig(for: provider) ?? ProviderConfig(id: provider)
|
||||
let isEnabled = providerConfig.enabled ?? defaultEnabled
|
||||
if let previous = self.providerEnablement[provider], previous != isEnabled {
|
||||
self.providerEnablementRevisions[provider, default: 0] &+= 1
|
||||
}
|
||||
let fingerprint = Self.providerConfigFingerprint(providerConfig)
|
||||
if let previous = self.providerConfigFingerprints[provider], previous != fingerprint {
|
||||
self.providerConfigRevisions[provider, default: 0] &+= 1
|
||||
}
|
||||
self.providerConfigFingerprints[provider] = fingerprint
|
||||
enablement[provider] = isEnabled
|
||||
}
|
||||
self.providerEnablement = enablement
|
||||
}
|
||||
|
||||
private static func providerConfigFingerprint(_ config: ProviderConfig) -> Data {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys]
|
||||
return (try? encoder.encode(config)) ?? Data()
|
||||
}
|
||||
|
||||
func providerEnablementRevision(for provider: UsageProvider) -> UInt64 {
|
||||
self.providerEnablementRevisions[provider, default: 0]
|
||||
}
|
||||
|
||||
func providerConfigRevision(for provider: UsageProvider) -> UInt64 {
|
||||
self.providerConfigRevisions[provider, default: 0]
|
||||
}
|
||||
|
||||
func orderedProviders() -> [UsageProvider] {
|
||||
if self.providerOrder.isEmpty {
|
||||
self.updateProviderState(config: self.configSnapshot)
|
||||
}
|
||||
return self.providerOrder
|
||||
}
|
||||
|
||||
func moveProvider(fromOffsets: IndexSet, toOffset: Int) {
|
||||
var order = self.orderedProviders()
|
||||
order.move(fromOffsets: fromOffsets, toOffset: toOffset)
|
||||
self.setProviderOrder(order)
|
||||
}
|
||||
|
||||
func isProviderEnabled(provider: UsageProvider, metadata: ProviderMetadata) -> Bool {
|
||||
self.providerEnablement[provider] ?? metadata.defaultEnabled
|
||||
}
|
||||
|
||||
func isProviderEnabledCached(
|
||||
provider: UsageProvider,
|
||||
metadataByProvider: [UsageProvider: ProviderMetadata]) -> Bool
|
||||
{
|
||||
let defaultEnabled = metadataByProvider[provider]?.defaultEnabled ?? false
|
||||
return self.providerEnablement[provider] ?? defaultEnabled
|
||||
}
|
||||
|
||||
func enabledProvidersOrdered(metadataByProvider: [UsageProvider: ProviderMetadata]) -> [UsageProvider] {
|
||||
_ = metadataByProvider
|
||||
return self.orderedProviders().filter { self.providerEnablement[$0] ?? false }
|
||||
}
|
||||
|
||||
func setProviderEnabled(provider: UsageProvider, metadata _: ProviderMetadata, enabled: Bool) {
|
||||
CodexBarLog.logger(LogCategories.settings).debug(
|
||||
"Provider toggle updated",
|
||||
metadata: ["provider": provider.rawValue, "enabled": "\(enabled)"])
|
||||
self.updateProviderConfig(provider: provider) { entry in
|
||||
entry.enabled = enabled
|
||||
}
|
||||
if !enabled, self.selectedMenuProvider == provider {
|
||||
self.selectedMenuProvider = nil
|
||||
}
|
||||
}
|
||||
|
||||
func rerunProviderDetection() {
|
||||
self.runInitialProviderDetectionIfNeeded(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsStore {
|
||||
private static func effectiveProviderOrder(raw: [String]) -> [UsageProvider] {
|
||||
var seen: Set<UsageProvider> = []
|
||||
var ordered: [UsageProvider] = []
|
||||
|
||||
for rawValue in raw {
|
||||
guard let provider = UsageProvider(rawValue: rawValue) else { continue }
|
||||
guard !seen.contains(provider) else { continue }
|
||||
seen.insert(provider)
|
||||
ordered.append(provider)
|
||||
}
|
||||
|
||||
if ordered.isEmpty {
|
||||
ordered = UsageProvider.allCases
|
||||
seen = Set(ordered)
|
||||
}
|
||||
|
||||
if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {
|
||||
ordered.insert(.factory, at: zaiIndex)
|
||||
seen.insert(.factory)
|
||||
}
|
||||
|
||||
if !seen.contains(.minimax), let zaiIndex = ordered.firstIndex(of: .zai) {
|
||||
let insertIndex = ordered.index(after: zaiIndex)
|
||||
ordered.insert(.minimax, at: insertIndex)
|
||||
seen.insert(.minimax)
|
||||
}
|
||||
|
||||
for provider in UsageProvider.allCases where !seen.contains(provider) {
|
||||
ordered.append(provider)
|
||||
}
|
||||
|
||||
return ordered
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user