Files
steipete--codexbar/Sources/CodexBar/SettingsStore+Defaults.swift
T
2026-07-13 12:22:33 +08:00

863 lines
33 KiB
Swift

import CodexBarCore
import Foundation
import ServiceManagement
extension SettingsStore {
private static let mergedOverviewSelectionEditedActiveProvidersKey = "mergedOverviewSelectionEditedActiveProviders"
func noteBackgroundWorkSettingsChanged() {
self.backgroundWorkSettingsRevision &+= 1
}
var refreshFrequency: RefreshFrequency {
get { self.defaultsState.refreshFrequency }
set {
self.defaultsState.refreshFrequency = newValue
self.userDefaults.set(newValue.rawValue, forKey: "refreshFrequency")
self.noteBackgroundWorkSettingsChanged()
}
}
/// When enabled, keeping the menu open through its short refresh delay fetches usage for every
/// enabled provider. The periodic refresh clock remains unchanged. See `scheduleOpenMenuRefresh`.
var refreshAllProvidersOnMenuOpen: Bool {
get { self.defaultsState.refreshAllProvidersOnMenuOpen }
set {
self.defaultsState.refreshAllProvidersOnMenuOpen = newValue
self.userDefaults.set(newValue, forKey: "refreshAllProvidersOnMenuOpen")
}
}
var launchAtLogin: Bool {
get { self.defaultsState.launchAtLogin }
set {
self.defaultsState.launchAtLogin = newValue
self.userDefaults.set(newValue, forKey: "launchAtLogin")
LaunchAtLoginManager.setEnabled(newValue)
}
}
var debugMenuEnabled: Bool {
get { self.defaultsState.debugMenuEnabled }
set {
self.defaultsState.debugMenuEnabled = newValue
self.userDefaults.set(newValue, forKey: "debugMenuEnabled")
}
}
var debugDisableKeychainAccess: Bool {
get { self.defaultsState.debugDisableKeychainAccess }
set {
self.defaultsState.debugDisableKeychainAccess = newValue
self.userDefaults.set(newValue, forKey: "debugDisableKeychainAccess")
if Self.shouldBridgeSharedDefaults(for: self.userDefaults) {
Self.sharedDefaults?.set(newValue, forKey: "debugDisableKeychainAccess")
}
KeychainAccessGate.isDisabled = newValue
self.noteBackgroundWorkSettingsChanged()
}
}
var debugFileLoggingEnabled: Bool {
get { self.defaultsState.debugFileLoggingEnabled }
set {
self.defaultsState.debugFileLoggingEnabled = newValue
self.userDefaults.set(newValue, forKey: "debugFileLoggingEnabled")
CodexBarLog.setFileLoggingEnabled(newValue)
}
}
var debugLogLevel: CodexBarLog.Level {
get {
let raw = self.defaultsState.debugLogLevelRaw
return CodexBarLog.parseLevel(raw) ?? .verbose
}
set {
self.defaultsState.debugLogLevelRaw = newValue.rawValue
self.userDefaults.set(newValue.rawValue, forKey: "debugLogLevel")
CodexBarLog.setLogLevel(newValue)
}
}
var debugKeepCLISessionsAlive: Bool {
get { self.defaultsState.debugKeepCLISessionsAlive }
set {
self.defaultsState.debugKeepCLISessionsAlive = newValue
self.userDefaults.set(newValue, forKey: "debugKeepCLISessionsAlive")
self.noteBackgroundWorkSettingsChanged()
}
}
var isVerboseLoggingEnabled: Bool {
self.debugLogLevel.rank <= CodexBarLog.Level.verbose.rank
}
private var debugLoadingPatternRaw: String? {
get { self.defaultsState.debugLoadingPatternRaw }
set {
self.defaultsState.debugLoadingPatternRaw = newValue
if let raw = newValue {
self.userDefaults.set(raw, forKey: "debugLoadingPattern")
} else {
self.userDefaults.removeObject(forKey: "debugLoadingPattern")
}
}
}
var statusChecksEnabled: Bool {
get { self.defaultsState.statusChecksEnabled }
set {
self.defaultsState.statusChecksEnabled = newValue
self.userDefaults.set(newValue, forKey: "statusChecksEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var sessionQuotaNotificationsEnabled: Bool {
get { self.defaultsState.sessionQuotaNotificationsEnabled }
set {
self.defaultsState.sessionQuotaNotificationsEnabled = newValue
self.userDefaults.set(newValue, forKey: "sessionQuotaNotificationsEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var quotaWarningNotificationsEnabled: Bool {
get { self.defaultsState.quotaWarningNotificationsEnabled }
set {
self.defaultsState.quotaWarningNotificationsEnabled = newValue
self.userDefaults.set(newValue, forKey: "quotaWarningNotificationsEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var predictivePaceWarningNotificationsEnabled: Bool {
get { self.defaultsState.predictivePaceWarningNotificationsEnabled }
set {
guard self.defaultsState.predictivePaceWarningNotificationsEnabled != newValue else { return }
self.defaultsState.predictivePaceWarningNotificationsEnabled = newValue
self.userDefaults.set(newValue, forKey: "predictivePaceWarningNotificationsEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var quotaWarningThresholds: [Int] {
get { QuotaWarningThresholds.sanitized(self.defaultsState.quotaWarningThresholdsRaw) }
set {
let sanitized = QuotaWarningThresholds.sanitized(newValue)
guard QuotaWarningThresholds.sanitized(self.defaultsState.quotaWarningThresholdsRaw) != sanitized
|| QuotaWarningThresholds.sanitized(self.defaultsState.quotaWarningSessionThresholdsRaw) != sanitized
|| QuotaWarningThresholds.sanitized(self.defaultsState.quotaWarningWeeklyThresholdsRaw) != sanitized
else {
return
}
self.defaultsState.quotaWarningThresholdsRaw = sanitized
self.defaultsState.quotaWarningSessionThresholdsRaw = sanitized
self.defaultsState.quotaWarningWeeklyThresholdsRaw = sanitized
self.userDefaults.set(sanitized, forKey: "quotaWarningThresholds")
self.userDefaults.set(sanitized, forKey: "quotaWarningSessionThresholds")
self.userDefaults.set(sanitized, forKey: "quotaWarningWeeklyThresholds")
self.noteBackgroundWorkSettingsChanged()
}
}
func quotaWarningThresholds(_ window: QuotaWarningWindow) -> [Int] {
switch window {
case .session:
QuotaWarningThresholds.sanitized(self.defaultsState.quotaWarningSessionThresholdsRaw)
case .weekly:
QuotaWarningThresholds.sanitized(self.defaultsState.quotaWarningWeeklyThresholdsRaw)
}
}
func setQuotaWarningThresholds(_ window: QuotaWarningWindow, thresholds: [Int]) {
let sanitized = QuotaWarningThresholds.sanitized(thresholds)
guard self.quotaWarningThresholds(window) != sanitized else { return }
switch window {
case .session:
self.defaultsState.quotaWarningSessionThresholdsRaw = sanitized
self.userDefaults.set(sanitized, forKey: "quotaWarningSessionThresholds")
case .weekly:
self.defaultsState.quotaWarningWeeklyThresholdsRaw = sanitized
self.userDefaults.set(sanitized, forKey: "quotaWarningWeeklyThresholds")
}
self.noteBackgroundWorkSettingsChanged()
}
func quotaWarningWindowEnabled(_ window: QuotaWarningWindow) -> Bool {
switch window {
case .session:
self.defaultsState.quotaWarningSessionEnabled
case .weekly:
self.defaultsState.quotaWarningWeeklyEnabled
}
}
func setQuotaWarningWindowEnabled(_ window: QuotaWarningWindow, enabled: Bool) {
switch window {
case .session:
self.defaultsState.quotaWarningSessionEnabled = enabled
self.userDefaults.set(enabled, forKey: "quotaWarningSessionEnabled")
case .weekly:
self.defaultsState.quotaWarningWeeklyEnabled = enabled
self.userDefaults.set(enabled, forKey: "quotaWarningWeeklyEnabled")
}
self.noteBackgroundWorkSettingsChanged()
}
var quotaWarningSoundEnabled: Bool {
get { self.defaultsState.quotaWarningSoundEnabled }
set {
self.defaultsState.quotaWarningSoundEnabled = newValue
self.userDefaults.set(newValue, forKey: "quotaWarningSoundEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var quotaWarningOnScreenAlertEnabled: Bool {
get { self.defaultsState.quotaWarningOnScreenAlertEnabled }
set {
self.defaultsState.quotaWarningOnScreenAlertEnabled = newValue
self.userDefaults.set(newValue, forKey: "quotaWarningOnScreenAlertEnabled")
}
}
var quotaWarningMarkersVisible: Bool {
get { self.defaultsState.quotaWarningMarkersVisible }
set {
self.defaultsState.quotaWarningMarkersVisible = newValue
self.userDefaults.set(newValue, forKey: "quotaWarningMarkersVisible")
}
}
var weeklyProgressWorkDays: Int? {
get { self.defaultsState.weeklyProgressWorkDays }
set {
self.defaultsState.weeklyProgressWorkDays = newValue
if let newValue {
self.userDefaults.set(newValue, forKey: "weeklyProgressWorkDays")
} else {
self.userDefaults.removeObject(forKey: "weeklyProgressWorkDays")
}
}
}
var usageBarsShowUsed: Bool {
get { self.defaultsState.usageBarsShowUsed }
set {
self.defaultsState.usageBarsShowUsed = newValue
self.userDefaults.set(newValue, forKey: "usageBarsShowUsed")
}
}
var resetTimesShowAbsolute: Bool {
get { self.defaultsState.resetTimesShowAbsolute }
set {
self.defaultsState.resetTimesShowAbsolute = newValue
self.userDefaults.set(newValue, forKey: "resetTimesShowAbsolute")
}
}
var providerChangelogLinksEnabled: Bool {
get { self.defaultsState.providerChangelogLinksEnabled }
set {
self.defaultsState.providerChangelogLinksEnabled = newValue
self.userDefaults.set(newValue, forKey: "providerChangelogLinksEnabled")
}
}
var menuBarShowsBrandIconWithPercent: Bool {
get { self.defaultsState.menuBarShowsBrandIconWithPercent }
set {
self.defaultsState.menuBarShowsBrandIconWithPercent = newValue
self.userDefaults.set(newValue, forKey: "menuBarShowsBrandIconWithPercent")
}
}
var menuBarHidesCritters: Bool {
get { self.defaultsState.menuBarHidesCritters }
set {
self.defaultsState.menuBarHidesCritters = newValue
self.userDefaults.set(newValue, forKey: "menuBarHidesCritters")
}
}
private var menuBarDisplayModeRaw: String? {
get { self.defaultsState.menuBarDisplayModeRaw }
set {
self.defaultsState.menuBarDisplayModeRaw = newValue
if let raw = newValue {
self.userDefaults.set(raw, forKey: "menuBarDisplayMode")
} else {
self.userDefaults.removeObject(forKey: "menuBarDisplayMode")
}
}
}
var menuBarDisplayMode: MenuBarDisplayMode {
get { MenuBarDisplayMode(rawValue: self.menuBarDisplayModeRaw ?? "") ?? .percent }
set { self.menuBarDisplayModeRaw = newValue.rawValue }
}
var menuBarShowsResetTimeWhenExhausted: Bool {
get { self.defaultsState.menuBarShowsResetTimeWhenExhausted }
set {
self.defaultsState.menuBarShowsResetTimeWhenExhausted = newValue
self.userDefaults.set(newValue, forKey: "menuBarShowsResetTimeWhenExhausted")
}
}
private var kiroMenuBarDisplayModeRaw: String? {
get { self.defaultsState.kiroMenuBarDisplayModeRaw }
set {
self.defaultsState.kiroMenuBarDisplayModeRaw = newValue
if let raw = newValue {
self.userDefaults.set(raw, forKey: "kiroMenuBarDisplayMode")
} else {
self.userDefaults.removeObject(forKey: "kiroMenuBarDisplayMode")
}
}
}
var kiroMenuBarDisplayMode: KiroMenuBarDisplayMode {
get { KiroMenuBarDisplayMode(rawValue: self.kiroMenuBarDisplayModeRaw ?? "") ?? .automatic }
set { self.kiroMenuBarDisplayModeRaw = newValue.rawValue }
}
var multiAccountMenuLayout: MultiAccountMenuLayout {
get { MultiAccountMenuLayout(rawValue: self.defaultsState.multiAccountMenuLayoutRaw) ?? .segmented }
set {
self.defaultsState.multiAccountMenuLayoutRaw = newValue.rawValue
self.userDefaults.set(newValue.rawValue, forKey: "multiAccountMenuLayout")
self.noteBackgroundWorkSettingsChanged()
}
}
var showAllTokenAccountsInMenu: Bool {
get { self.multiAccountMenuLayout == .stacked }
set { self.multiAccountMenuLayout = newValue ? .stacked : .segmented }
}
var historicalTrackingEnabled: Bool {
get { self.defaultsState.historicalTrackingEnabled }
set {
self.defaultsState.historicalTrackingEnabled = newValue
self.userDefaults.set(newValue, forKey: "historicalTrackingEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var menuBarMetricPreferencesRaw: [String: String] {
get { self.defaultsState.menuBarMetricPreferencesRaw }
set {
self.defaultsState.menuBarMetricPreferencesRaw = newValue
self.userDefaults.set(newValue, forKey: "menuBarMetricPreferences")
}
}
var copilotIconSecondaryWindowIDRaw: String {
get { self.defaultsState.copilotIconSecondaryWindowIDRaw }
set {
self.defaultsState.copilotIconSecondaryWindowIDRaw = newValue
self.userDefaults.set(newValue, forKey: "copilotIconSecondaryWindowID")
}
}
var costUsageEnabled: Bool {
get { self.defaultsState.costUsageEnabled }
set {
self.defaultsState.costUsageEnabled = newValue
self.userDefaults.set(newValue, forKey: "tokenCostUsageEnabled")
self.noteBackgroundWorkSettingsChanged()
}
}
var costUsageHistoryDays: Int {
get { self.defaultsState.costUsageHistoryDays }
set {
let clamped = max(1, min(365, newValue))
self.defaultsState.costUsageHistoryDays = clamped
self.userDefaults.set(clamped, forKey: "tokenCostUsageHistoryDays")
self.noteBackgroundWorkSettingsChanged()
}
}
var costComparisonPeriodsEnabled: Bool {
get { self.defaultsState.costComparisonPeriodsEnabled }
set {
self.defaultsState.costComparisonPeriodsEnabled = newValue
self.userDefaults.set(newValue, forKey: "costComparisonPeriodsEnabled")
}
}
var costSummaryDisplayStyleRaw: String {
get { self.defaultsState.costSummaryDisplayStyleRaw }
set {
self.defaultsState.costSummaryDisplayStyleRaw = newValue
self.userDefaults.set(newValue, forKey: "costSummaryDisplayStyle")
}
}
var costSummaryDisplayStyle: CostSummaryDisplayStyle {
get { CostSummaryDisplayStyle(rawValue: self.costSummaryDisplayStyleRaw) ?? .both }
set { self.costSummaryDisplayStyleRaw = newValue.rawValue }
}
var hidePersonalInfo: Bool {
get { self.defaultsState.hidePersonalInfo }
set {
self.defaultsState.hidePersonalInfo = newValue
self.userDefaults.set(newValue, forKey: "hidePersonalInfo")
}
}
var randomBlinkEnabled: Bool {
get { self.defaultsState.randomBlinkEnabled }
set {
self.defaultsState.randomBlinkEnabled = newValue
self.userDefaults.set(newValue, forKey: "randomBlinkEnabled")
}
}
var confettiOnSessionLimitResetsEnabled: Bool {
get { self.defaultsState.confettiOnSessionLimitResetsEnabled }
set {
self.defaultsState.confettiOnSessionLimitResetsEnabled = newValue
self.userDefaults.set(newValue, forKey: "confettiOnSessionLimitResetsEnabled")
}
}
var confettiOnWeeklyLimitResetsEnabled: Bool {
get { self.defaultsState.confettiOnWeeklyLimitResetsEnabled }
set {
self.defaultsState.confettiOnWeeklyLimitResetsEnabled = newValue
self.userDefaults.set(newValue, forKey: "confettiOnWeeklyLimitResetsEnabled")
}
}
var menuBarShowsHighestUsage: Bool {
get { self.defaultsState.menuBarShowsHighestUsage }
set {
self.defaultsState.menuBarShowsHighestUsage = newValue
self.userDefaults.set(newValue, forKey: "menuBarShowsHighestUsage")
}
}
var claudeOAuthKeychainPromptMode: ClaudeOAuthKeychainPromptMode {
get {
let raw = self.defaultsState.claudeOAuthKeychainPromptModeRaw
return ClaudeOAuthKeychainPromptMode(rawValue: raw ?? "") ?? .onlyOnUserAction
}
set {
self.defaultsState.claudeOAuthKeychainPromptModeRaw = newValue.rawValue
self.userDefaults.set(newValue.rawValue, forKey: "claudeOAuthKeychainPromptMode")
self.noteBackgroundWorkSettingsChanged()
}
}
var claudeOAuthKeychainReadStrategy: ClaudeOAuthKeychainReadStrategy {
get {
guard let raw = self.defaultsState.claudeOAuthKeychainReadStrategyRaw else {
return .securityFramework
}
let strategy = ClaudeOAuthKeychainReadStrategy(rawValue: raw) ?? .securityFramework
return strategy == .securityCLIExperimental ? .securityFramework : strategy
}
set {
self.defaultsState.claudeOAuthKeychainReadStrategyRaw = newValue.rawValue
self.userDefaults.set(newValue.rawValue, forKey: "claudeOAuthKeychainReadStrategy")
self.noteBackgroundWorkSettingsChanged()
}
}
var claudeOAuthPromptFreeCredentialsEnabled: Bool {
get { self.claudeOAuthKeychainPromptMode == .never }
set {
self.claudeOAuthKeychainReadStrategy = .securityFramework
if newValue {
self.claudeOAuthKeychainPromptMode = .never
} else if self.claudeOAuthKeychainPromptMode == .never {
self.claudeOAuthKeychainPromptMode = .onlyOnUserAction
}
}
}
var claudeWebExtrasEnabled: Bool {
get { self.claudeWebExtrasEnabledRaw }
set { self.claudeWebExtrasEnabledRaw = newValue }
}
var copilotBudgetExtrasEnabled: Bool {
get { self.defaultsState.copilotBudgetExtrasEnabled }
set {
self.defaultsState.copilotBudgetExtrasEnabled = newValue
self.userDefaults.set(newValue, forKey: "copilotBudgetExtrasEnabled")
CodexBarLog.logger(LogCategories.settings).info(
"Copilot budget extras updated",
metadata: ["enabled": newValue ? "1" : "0"])
self.noteBackgroundWorkSettingsChanged()
}
}
private var claudeWebExtrasEnabledRaw: Bool {
get { self.defaultsState.claudeWebExtrasEnabledRaw }
set {
self.defaultsState.claudeWebExtrasEnabledRaw = newValue
self.userDefaults.set(newValue, forKey: "claudeWebExtrasEnabled")
CodexBarLog.logger(LogCategories.settings).info(
"Claude web extras updated",
metadata: ["enabled": newValue ? "1" : "0"])
self.noteBackgroundWorkSettingsChanged()
}
}
var showOptionalCreditsAndExtraUsage: Bool {
get { self.defaultsState.showOptionalCreditsAndExtraUsage }
set {
self.defaultsState.showOptionalCreditsAndExtraUsage = newValue
self.userDefaults.set(newValue, forKey: "showOptionalCreditsAndExtraUsage")
// This flag also controls ProviderFetchContext.includeOptionalUsage, so it is not display-only.
self.noteBackgroundWorkSettingsChanged()
}
}
var codexSparkUsageVisible: Bool {
get { self.defaultsState.codexSparkUsageVisible }
set {
self.defaultsState.codexSparkUsageVisible = newValue
self.userDefaults.set(newValue, forKey: "codexSparkUsageVisible")
}
}
var openAIWebAccessEnabled: Bool {
get { self.defaultsState.openAIWebAccessEnabled }
set {
self.defaultsState.openAIWebAccessEnabled = newValue
self.userDefaults.set(newValue, forKey: "openAIWebAccessEnabled")
CodexBarLog.logger(LogCategories.settings).info(
"OpenAI web access updated",
metadata: ["enabled": newValue ? "1" : "0"])
self.noteBackgroundWorkSettingsChanged()
}
}
var openAIWebBatterySaverEnabled: Bool {
get { self.defaultsState.openAIWebBatterySaverEnabled }
set {
self.defaultsState.openAIWebBatterySaverEnabled = newValue
self.userDefaults.set(newValue, forKey: "openAIWebBatterySaverEnabled")
CodexBarLog.logger(LogCategories.settings).info(
"OpenAI web battery saver updated",
metadata: ["enabled": newValue ? "1" : "0"])
self.noteBackgroundWorkSettingsChanged()
}
}
var providerStorageFootprintsEnabled: Bool {
get { self.defaultsState.providerStorageFootprintsEnabled }
set {
self.defaultsState.providerStorageFootprintsEnabled = newValue
self.userDefaults.set(newValue, forKey: "providerStorageFootprintsEnabled")
CodexBarLog.logger(LogCategories.settings).info(
"Provider storage footprints updated",
metadata: ["enabled": newValue ? "1" : "0"])
self.noteBackgroundWorkSettingsChanged()
}
}
var jetbrainsIDEBasePath: String {
get { self.defaultsState.jetbrainsIDEBasePath }
set {
self.defaultsState.jetbrainsIDEBasePath = newValue
self.userDefaults.set(newValue, forKey: "jetbrainsIDEBasePath")
}
}
var mergeIcons: Bool {
get { self.defaultsState.mergeIcons }
set {
self.defaultsState.mergeIcons = newValue
self.userDefaults.set(newValue, forKey: "mergeIcons")
}
}
var switcherShowsIcons: Bool {
get { self.defaultsState.switcherShowsIcons }
set {
self.defaultsState.switcherShowsIcons = newValue
self.userDefaults.set(newValue, forKey: "switcherShowsIcons")
}
}
var mergedMenuLastSelectedWasOverview: Bool {
get { self.mergedMenuLastSelectedWasOverviewStorage }
set {
self.mergedMenuLastSelectedWasOverviewStorage = newValue
self.userDefaults.set(newValue, forKey: "mergedMenuLastSelectedWasOverview")
}
}
private var mergedOverviewSelectedProvidersRaw: [String] {
get { self.defaultsState.mergedOverviewSelectedProvidersRaw }
set {
self.defaultsState.mergedOverviewSelectedProvidersRaw = newValue
self.userDefaults.set(newValue, forKey: "mergedOverviewSelectedProviders")
}
}
private var selectedMenuProviderRaw: String? {
get { self.selectedMenuProviderRawStorage }
set {
self.selectedMenuProviderRawStorage = newValue
if let raw = newValue {
self.userDefaults.set(raw, forKey: "selectedMenuProvider")
} else {
self.userDefaults.removeObject(forKey: "selectedMenuProvider")
}
}
}
var selectedMenuProvider: UsageProvider? {
get { self.selectedMenuProviderRaw.flatMap(UsageProvider.init(rawValue:)) }
set {
self.selectedMenuProviderRaw = newValue?.rawValue
}
}
var mergedOverviewSelectedProviders: [UsageProvider] {
get {
Self.decodeProviders(
self.mergedOverviewSelectedProvidersRaw,
maxCount: Self.mergedOverviewProviderLimit)
}
set {
let normalized = Self.normalizeProviders(newValue, maxCount: Self.mergedOverviewProviderLimit)
self.mergedOverviewSelectedProvidersRaw = normalized.map(\.rawValue)
}
}
private var hasMergedOverviewSelectionPreference: Bool {
self.userDefaults.object(forKey: "mergedOverviewSelectedProviders") != nil
}
private var mergedOverviewSelectionEditedActiveProvidersRaw: [String]? {
get {
self.userDefaults.array(forKey: Self.mergedOverviewSelectionEditedActiveProvidersKey) as? [String]
}
set {
if let newValue {
self.userDefaults.set(newValue, forKey: Self.mergedOverviewSelectionEditedActiveProvidersKey)
} else {
self.userDefaults.removeObject(forKey: Self.mergedOverviewSelectionEditedActiveProvidersKey)
}
}
}
private func mergedOverviewSelectionApplies(to activeProviders: [UsageProvider]) -> Bool {
guard let editedRaw = self.mergedOverviewSelectionEditedActiveProvidersRaw else { return false }
let editedSet = Set(editedRaw)
let activeSet = Set(Self.normalizeProviders(activeProviders).map(\.rawValue))
return editedSet == activeSet
}
private func markMergedOverviewSelectionEdited(for activeProviders: [UsageProvider]) {
let signature = Set(Self.normalizeProviders(activeProviders).map(\.rawValue))
self.mergedOverviewSelectionEditedActiveProvidersRaw = Array(signature).sorted()
}
private func clearMergedOverviewSelectionPreference() {
self.defaultsState.mergedOverviewSelectedProvidersRaw = []
self.userDefaults.removeObject(forKey: "mergedOverviewSelectedProviders")
self.mergedOverviewSelectionEditedActiveProvidersRaw = nil
}
func resolvedMergedOverviewProviders(
activeProviders: [UsageProvider],
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider]
{
guard maxVisibleProviders > 0 else { return [] }
let normalizedActive = Self.normalizeProviders(activeProviders)
guard self.hasMergedOverviewSelectionPreference else {
return Array(normalizedActive.prefix(maxVisibleProviders))
}
if normalizedActive.count <= maxVisibleProviders,
!self.mergedOverviewSelectionApplies(to: normalizedActive)
{
return normalizedActive
}
let selectedSet = Set(self.mergedOverviewSelectedProviders)
return Array(normalizedActive.filter { selectedSet.contains($0) }.prefix(maxVisibleProviders))
}
@discardableResult
func reconcileMergedOverviewSelectedProviders(
activeProviders: [UsageProvider],
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider]
{
guard maxVisibleProviders > 0 else {
self.clearMergedOverviewSelectionPreference()
return []
}
let normalizedActive = Self.normalizeProviders(activeProviders)
if normalizedActive.isEmpty {
self.clearMergedOverviewSelectionPreference()
return []
}
let shouldPersistResolvedSelection = normalizedActive.count > maxVisibleProviders ||
self.mergedOverviewSelectionApplies(to: normalizedActive)
if self.hasMergedOverviewSelectionPreference, shouldPersistResolvedSelection {
let selectedSet = Set(self.mergedOverviewSelectedProviders)
let sanitizedSelection = Array(
normalizedActive
.filter { selectedSet.contains($0) }
.prefix(maxVisibleProviders))
if sanitizedSelection != self.mergedOverviewSelectedProviders {
self.mergedOverviewSelectedProviders = sanitizedSelection
}
}
return self.resolvedMergedOverviewProviders(
activeProviders: normalizedActive,
maxVisibleProviders: maxVisibleProviders)
}
@discardableResult
func setMergedOverviewProviderSelection(
provider: UsageProvider,
isSelected: Bool,
activeProviders: [UsageProvider],
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider]
{
guard maxVisibleProviders > 0 else {
self.clearMergedOverviewSelectionPreference()
return []
}
let normalizedActive = Self.normalizeProviders(activeProviders)
guard normalizedActive.contains(provider) else {
return self.resolvedMergedOverviewProviders(
activeProviders: normalizedActive,
maxVisibleProviders: maxVisibleProviders)
}
let currentSelection = self.resolvedMergedOverviewProviders(
activeProviders: normalizedActive,
maxVisibleProviders: maxVisibleProviders)
var updatedSet = Set(currentSelection)
if isSelected {
guard updatedSet.contains(provider) || currentSelection.count < maxVisibleProviders else {
return currentSelection
}
updatedSet.insert(provider)
} else {
updatedSet.remove(provider)
}
let updatedSelection = Array(
normalizedActive
.filter { updatedSet.contains($0) }
.prefix(maxVisibleProviders))
self.mergedOverviewSelectedProviders = updatedSelection
self.markMergedOverviewSelectionEdited(for: normalizedActive)
return updatedSelection
}
var providerDetectionCompleted: Bool {
get { self.defaultsState.providerDetectionCompleted }
set {
self.defaultsState.providerDetectionCompleted = newValue
self.userDefaults.set(newValue, forKey: "providerDetectionCompleted")
}
}
/// Whether the Providers settings pane displays providers sorted alphabetically (enabled on
/// top). Defaults to `false`. Purely a display preference — it never rewrites the stored manual
/// order, so turning it on sorts the display without losing the user's hand-arranged sequence.
var providersSortedAlphabetically: Bool {
get { self.defaultsState.providersSortedAlphabetically }
set {
self.defaultsState.providersSortedAlphabetically = newValue
self.userDefaults.set(newValue, forKey: "providersSortedAlphabetically")
}
}
var appLanguage: String {
get { self.defaultsState.appLanguageRaw ?? "" }
set {
let stored = newValue.isEmpty ? nil : newValue
self.defaultsState.appLanguageRaw = stored
if let stored {
self.userDefaults.set(stored, forKey: "appLanguage")
if self.userDefaults !== UserDefaults.standard {
UserDefaults.standard.set(stored, forKey: "appLanguage")
}
UserDefaults.standard.removeObject(forKey: "AppleLanguages")
} else {
self.userDefaults.removeObject(forKey: "appLanguage")
if self.userDefaults !== UserDefaults.standard {
UserDefaults.standard.removeObject(forKey: "appLanguage")
}
UserDefaults.standard.removeObject(forKey: "AppleLanguages")
}
resetCodexBarLocalizationCache()
}
}
var debugLoadingPattern: LoadingPattern? {
get { self.debugLoadingPatternRaw.flatMap(LoadingPattern.init(rawValue:)) }
set { self.debugLoadingPatternRaw = newValue?.rawValue }
}
var terminalApp: TerminalApp {
get { TerminalApp(rawValue: self.defaultsState.terminalAppRaw ?? "") ?? .terminal }
set {
self.defaultsState.terminalAppRaw = newValue.rawValue
self.userDefaults.set(newValue.rawValue, forKey: "terminalApp")
}
}
var agentSessionsEnabled: Bool {
get { self.defaultsState.agentSessionsEnabled }
set {
self.defaultsState.agentSessionsEnabled = newValue
self.userDefaults.set(newValue, forKey: "agentSessionsEnabled")
}
}
var agentSessionsManualHosts: String {
get { self.defaultsState.agentSessionsManualHosts }
set {
self.defaultsState.agentSessionsManualHosts = newValue
self.userDefaults.set(newValue, forKey: "agentSessionsManualHosts")
}
}
}
extension SettingsStore {
private static func normalizeProviders(_ providers: [UsageProvider], maxCount: Int? = nil) -> [UsageProvider] {
var seen: Set<UsageProvider> = []
var normalized: [UsageProvider] = []
for provider in providers where !seen.contains(provider) {
seen.insert(provider)
normalized.append(provider)
if let maxCount, normalized.count >= maxCount { break }
}
return normalized
}
private static func decodeProviders(_ rawProviders: [String], maxCount: Int? = nil) -> [UsageProvider] {
var providers: [UsageProvider] = []
providers.reserveCapacity(rawProviders.count)
for raw in rawProviders {
guard let provider = UsageProvider(rawValue: raw) else { continue }
providers.append(provider)
}
return self.normalizeProviders(providers, maxCount: maxCount)
}
}