863 lines
33 KiB
Swift
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)
|
|
}
|
|
}
|