Added a lot of functionality. Bugfixes and changes

This commit is contained in:
2026-02-15 16:46:06 +01:00
parent 2434e554f8
commit 04c9b8da1e
31 changed files with 6653 additions and 239 deletions

View File

@@ -16,7 +16,16 @@ class SettingsService {
// In-memory cache of DB settings for fast reads
private var cache: [String: String] = [:]
// Keychain keys (secrets only)
// Encrypted database keys (for API keys)
private enum EncryptedKeys {
static let openrouterAPIKey = "openrouterAPIKey"
static let anthropicAPIKey = "anthropicAPIKey"
static let openaiAPIKey = "openaiAPIKey"
static let googleAPIKey = "googleAPIKey"
static let googleSearchEngineID = "googleSearchEngineID"
}
// Old keychain keys (for migration only)
private enum KeychainKeys {
static let openrouterAPIKey = "com.oai.apikey.openrouter"
static let anthropicAPIKey = "com.oai.apikey.anthropic"
@@ -32,6 +41,9 @@ class SettingsService {
// Migrate from UserDefaults on first launch
migrateFromUserDefaultsIfNeeded()
// Migrate API keys from Keychain to encrypted database
migrateFromKeychainIfNeeded()
}
// MARK: - Provider Settings
@@ -102,6 +114,20 @@ class SettingsService {
}
}
var customPromptMode: Settings.CustomPromptMode {
get {
if let rawValue = cache["customPromptMode"],
let mode = Settings.CustomPromptMode(rawValue: rawValue) {
return mode
}
return .append // Default to append mode
}
set {
cache["customPromptMode"] = newValue.rawValue
DatabaseService.shared.setSetting(key: "customPromptMode", value: newValue.rawValue)
}
}
// MARK: - Feature Settings
var onlineMode: Bool {
@@ -157,6 +183,26 @@ class SettingsService {
}
}
// MARK: - Toolbar Settings
/// Toolbar icon size default 16 (minimum)
var toolbarIconSize: Double {
get { cache["toolbarIconSize"].flatMap(Double.init) ?? 16.0 }
set {
cache["toolbarIconSize"] = String(newValue)
DatabaseService.shared.setSetting(key: "toolbarIconSize", value: String(newValue))
}
}
/// Show labels on toolbar icons default false
var showToolbarLabels: Bool {
get { cache["showToolbarLabels"] == "true" }
set {
cache["showToolbarLabels"] = String(newValue)
DatabaseService.shared.setSetting(key: "showToolbarLabels", value: String(newValue))
}
}
// MARK: - MCP Permissions
var mcpCanWriteFiles: Bool {
@@ -262,63 +308,392 @@ class SettingsService {
cache["ollamaBaseURL"] != nil && !(cache["ollamaBaseURL"]!.isEmpty)
}
// MARK: - API Keys (Keychain)
// MARK: - API Keys (Encrypted Database)
var openrouterAPIKey: String? {
get { getKeychainValue(for: KeychainKeys.openrouterAPIKey) }
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.openrouterAPIKey) }
set {
if let value = newValue {
setKeychainValue(value, for: KeychainKeys.openrouterAPIKey)
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.openrouterAPIKey, value: value)
} else {
deleteKeychainValue(for: KeychainKeys.openrouterAPIKey)
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.openrouterAPIKey)
}
}
}
var anthropicAPIKey: String? {
get { getKeychainValue(for: KeychainKeys.anthropicAPIKey) }
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.anthropicAPIKey) }
set {
if let value = newValue {
setKeychainValue(value, for: KeychainKeys.anthropicAPIKey)
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.anthropicAPIKey, value: value)
} else {
deleteKeychainValue(for: KeychainKeys.anthropicAPIKey)
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.anthropicAPIKey)
}
}
}
var openaiAPIKey: String? {
get { getKeychainValue(for: KeychainKeys.openaiAPIKey) }
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.openaiAPIKey) }
set {
if let value = newValue {
setKeychainValue(value, for: KeychainKeys.openaiAPIKey)
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.openaiAPIKey, value: value)
} else {
deleteKeychainValue(for: KeychainKeys.openaiAPIKey)
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.openaiAPIKey)
}
}
}
var googleAPIKey: String? {
get { getKeychainValue(for: KeychainKeys.googleAPIKey) }
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.googleAPIKey) }
set {
if let value = newValue {
setKeychainValue(value, for: KeychainKeys.googleAPIKey)
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.googleAPIKey, value: value)
} else {
deleteKeychainValue(for: KeychainKeys.googleAPIKey)
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.googleAPIKey)
}
}
}
var googleSearchEngineID: String? {
get { getKeychainValue(for: KeychainKeys.googleSearchEngineID) }
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.googleSearchEngineID) }
set {
if let value = newValue {
setKeychainValue(value, for: KeychainKeys.googleSearchEngineID)
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.googleSearchEngineID, value: value)
} else {
deleteKeychainValue(for: KeychainKeys.googleSearchEngineID)
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.googleSearchEngineID)
}
}
}
// MARK: - Git Sync Settings
var syncEnabled: Bool {
get { cache["syncEnabled"] == "true" }
set {
cache["syncEnabled"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncEnabled", value: String(newValue))
}
}
var syncRepoURL: String {
get { cache["syncRepoURL"] ?? "" }
set {
let trimmed = newValue.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty {
cache.removeValue(forKey: "syncRepoURL")
DatabaseService.shared.deleteSetting(key: "syncRepoURL")
} else {
cache["syncRepoURL"] = trimmed
DatabaseService.shared.setSetting(key: "syncRepoURL", value: trimmed)
}
}
}
var syncLocalPath: String {
get { cache["syncLocalPath"] ?? "~/Library/Application Support/oAI/sync" }
set {
cache["syncLocalPath"] = newValue
DatabaseService.shared.setSetting(key: "syncLocalPath", value: newValue)
}
}
var syncAuthMethod: String {
get { cache["syncAuthMethod"] ?? "token" } // Default to access token
set {
cache["syncAuthMethod"] = newValue
DatabaseService.shared.setSetting(key: "syncAuthMethod", value: newValue)
}
}
// Encrypted sync credentials
var syncUsername: String? {
get { try? DatabaseService.shared.getEncryptedSetting(key: "syncUsername") }
set {
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: "syncUsername", value: value)
} else {
DatabaseService.shared.deleteEncryptedSetting(key: "syncUsername")
}
}
}
var syncPassword: String? {
get { try? DatabaseService.shared.getEncryptedSetting(key: "syncPassword") }
set {
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: "syncPassword", value: value)
} else {
DatabaseService.shared.deleteEncryptedSetting(key: "syncPassword")
}
}
}
var syncAccessToken: String? {
get { try? DatabaseService.shared.getEncryptedSetting(key: "syncAccessToken") }
set {
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: "syncAccessToken", value: value)
} else {
DatabaseService.shared.deleteEncryptedSetting(key: "syncAccessToken")
}
}
}
var syncAutoExport: Bool {
get { cache["syncAutoExport"] == "true" }
set {
cache["syncAutoExport"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoExport", value: String(newValue))
}
}
var syncAutoPull: Bool {
get { cache["syncAutoPull"] == "true" }
set {
cache["syncAutoPull"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoPull", value: String(newValue))
}
}
var syncConfigured: Bool {
guard !syncRepoURL.isEmpty else { return false }
switch syncAuthMethod {
case "ssh":
return true // SSH uses system keys
case "password":
return syncUsername != nil && syncPassword != nil
case "token":
return syncAccessToken != nil
default:
return false
}
}
// MARK: - Auto-Sync Settings
var syncAutoSave: Bool {
get { cache["syncAutoSave"] == "true" }
set {
cache["syncAutoSave"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoSave", value: String(newValue))
}
}
var syncAutoSaveMinMessages: Int {
get { cache["syncAutoSaveMinMessages"].flatMap(Int.init) ?? 5 }
set {
cache["syncAutoSaveMinMessages"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoSaveMinMessages", value: String(newValue))
}
}
var syncAutoSaveOnModelSwitch: Bool {
get { cache["syncAutoSaveOnModelSwitch"] == "true" }
set {
cache["syncAutoSaveOnModelSwitch"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoSaveOnModelSwitch", value: String(newValue))
}
}
var syncAutoSaveOnAppQuit: Bool {
get { cache["syncAutoSaveOnAppQuit"] == "true" }
set {
cache["syncAutoSaveOnAppQuit"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoSaveOnAppQuit", value: String(newValue))
}
}
var syncAutoSaveOnIdle: Bool {
get { cache["syncAutoSaveOnIdle"] == "true" }
set {
cache["syncAutoSaveOnIdle"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoSaveOnIdle", value: String(newValue))
}
}
var syncAutoSaveIdleMinutes: Int {
get { cache["syncAutoSaveIdleMinutes"].flatMap(Int.init) ?? 5 }
set {
cache["syncAutoSaveIdleMinutes"] = String(newValue)
DatabaseService.shared.setSetting(key: "syncAutoSaveIdleMinutes", value: String(newValue))
}
}
var syncLastAutoSaveConversationId: String? {
get { cache["syncLastAutoSaveConversationId"] }
set {
if let value = newValue {
cache["syncLastAutoSaveConversationId"] = value
DatabaseService.shared.setSetting(key: "syncLastAutoSaveConversationId", value: value)
} else {
cache.removeValue(forKey: "syncLastAutoSaveConversationId")
DatabaseService.shared.deleteSetting(key: "syncLastAutoSaveConversationId")
}
}
}
// MARK: - Email Handler Settings
var emailHandlerEnabled: Bool {
get { cache["emailHandlerEnabled"] == "true" }
set {
cache["emailHandlerEnabled"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailHandlerEnabled", value: String(newValue))
}
}
var emailHandlerProvider: String {
get { cache["emailHandlerProvider"] ?? "openrouter" }
set {
cache["emailHandlerProvider"] = newValue
DatabaseService.shared.setSetting(key: "emailHandlerProvider", value: newValue)
}
}
var emailHandlerModel: String {
get { cache["emailHandlerModel"] ?? "" }
set {
cache["emailHandlerModel"] = newValue
DatabaseService.shared.setSetting(key: "emailHandlerModel", value: newValue)
}
}
var emailSubjectIdentifier: String {
get { cache["emailSubjectIdentifier"] ?? "[OAIBOT]" }
set {
cache["emailSubjectIdentifier"] = newValue
DatabaseService.shared.setSetting(key: "emailSubjectIdentifier", value: newValue)
}
}
var emailRateLimitEnabled: Bool {
get { cache["emailRateLimitEnabled"] == "true" }
set {
cache["emailRateLimitEnabled"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailRateLimitEnabled", value: String(newValue))
}
}
var emailRateLimitPerHour: Int {
get { cache["emailRateLimitPerHour"].flatMap(Int.init) ?? 10 }
set {
cache["emailRateLimitPerHour"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailRateLimitPerHour", value: String(newValue))
}
}
var emailMaxTokens: Int {
get { cache["emailMaxTokens"].flatMap(Int.init) ?? 2000 }
set {
cache["emailMaxTokens"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailMaxTokens", value: String(newValue))
}
}
var emailHandlerSystemPrompt: String? {
get { cache["emailHandlerSystemPrompt"] }
set {
if let value = newValue, !value.isEmpty {
cache["emailHandlerSystemPrompt"] = value
DatabaseService.shared.setSetting(key: "emailHandlerSystemPrompt", value: value)
} else {
cache.removeValue(forKey: "emailHandlerSystemPrompt")
DatabaseService.shared.deleteSetting(key: "emailHandlerSystemPrompt")
}
}
}
var emailOnlineMode: Bool {
get { cache["emailOnlineMode"] == "true" }
set {
cache["emailOnlineMode"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailOnlineMode", value: String(newValue))
}
}
var emailHandlerConfigured: Bool {
guard emailHandlerEnabled else { return false }
guard !emailHandlerModel.isEmpty else { return false }
// Check if email server is configured
guard emailServerConfigured else { return false }
return true
}
// MARK: - Email Server Settings
var emailImapHost: String? {
get { cache["emailImapHost"] }
set {
if let value = newValue, !value.isEmpty {
cache["emailImapHost"] = value
DatabaseService.shared.setSetting(key: "emailImapHost", value: value)
} else {
cache.removeValue(forKey: "emailImapHost")
DatabaseService.shared.deleteSetting(key: "emailImapHost")
}
}
}
var emailSmtpHost: String? {
get { cache["emailSmtpHost"] }
set {
if let value = newValue, !value.isEmpty {
cache["emailSmtpHost"] = value
DatabaseService.shared.setSetting(key: "emailSmtpHost", value: value)
} else {
cache.removeValue(forKey: "emailSmtpHost")
DatabaseService.shared.deleteSetting(key: "emailSmtpHost")
}
}
}
var emailUsername: String? {
get { try? DatabaseService.shared.getEncryptedSetting(key: "emailUsername") }
set {
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: "emailUsername", value: value)
} else {
DatabaseService.shared.deleteEncryptedSetting(key: "emailUsername")
}
}
}
var emailPassword: String? {
get { try? DatabaseService.shared.getEncryptedSetting(key: "emailPassword") }
set {
if let value = newValue, !value.isEmpty {
try? DatabaseService.shared.setEncryptedSetting(key: "emailPassword", value: value)
} else {
DatabaseService.shared.deleteEncryptedSetting(key: "emailPassword")
}
}
}
var emailImapPort: Int {
get { cache["emailImapPort"].flatMap(Int.init) ?? 993 }
set {
cache["emailImapPort"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailImapPort", value: String(newValue))
}
}
var emailSmtpPort: Int {
get { cache["emailSmtpPort"].flatMap(Int.init) ?? 587 }
set {
cache["emailSmtpPort"] = String(newValue)
DatabaseService.shared.setSetting(key: "emailSmtpPort", value: String(newValue))
}
}
var emailServerConfigured: Bool {
guard let imapHost = emailImapHost, !imapHost.isEmpty else { return false }
guard let smtpHost = emailSmtpHost, !smtpHost.isEmpty else { return false }
guard let username = emailUsername, !username.isEmpty else { return false }
guard let password = emailPassword, !password.isEmpty else { return false }
return true
}
// MARK: - UserDefaults Migration
private func migrateFromUserDefaultsIfNeeded() {
@@ -367,7 +742,47 @@ class SettingsService {
DatabaseService.shared.setSetting(key: "_migrated", value: "true")
}
// MARK: - Keychain Helpers
// MARK: - Keychain Migration
private func migrateFromKeychainIfNeeded() {
// Skip if already migrated
guard cache["_keychain_migrated"] == nil else { return }
Log.settings.info("Migrating API keys from Keychain to encrypted database...")
let keysToMigrate: [(keychainKey: String, encryptedKey: String)] = [
(KeychainKeys.openrouterAPIKey, EncryptedKeys.openrouterAPIKey),
(KeychainKeys.anthropicAPIKey, EncryptedKeys.anthropicAPIKey),
(KeychainKeys.openaiAPIKey, EncryptedKeys.openaiAPIKey),
(KeychainKeys.googleAPIKey, EncryptedKeys.googleAPIKey),
(KeychainKeys.googleSearchEngineID, EncryptedKeys.googleSearchEngineID),
]
var migratedCount = 0
for (keychainKey, encryptedKey) in keysToMigrate {
// Read from keychain
if let value = getKeychainValue(for: keychainKey), !value.isEmpty {
// Write to encrypted database
do {
try DatabaseService.shared.setEncryptedSetting(key: encryptedKey, value: value)
// Delete from keychain after successful migration
deleteKeychainValue(for: keychainKey)
migratedCount += 1
Log.settings.info("Migrated \(encryptedKey) from Keychain to encrypted database")
} catch {
Log.settings.error("Failed to migrate \(encryptedKey): \(error.localizedDescription)")
}
}
}
Log.settings.info("Keychain migration complete: \(migratedCount) keys migrated")
// Mark migration complete
cache["_keychain_migrated"] = "true"
DatabaseService.shared.setSetting(key: "_keychain_migrated", value: "true")
}
// MARK: - Keychain Helpers (for migration only)
private func getKeychainValue(for key: String) -> String? {
let query: [String: Any] = [