1 line
36 KiB
JSON
1 line
36 KiB
JSON
{"repo": "bincat233/nekotask", "files": {"app/src/main/java/me/superbear/todolist/ui/settings/SettingsCoordinator.kt": {"f6acb377d6bc3385ca8a30bd9b6a975e75d3e254": {"t": 1785745735.1078186, "n": 9973, "h": "a39051e25455bf08", "c": "package me.superbear.todolist.ui.settings\n\nimport ai.koog.prompt.llm.LLMProvider\nimport android.content.SharedPreferences\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.os.LocaleListCompat\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport me.superbear.todolist.BuildConfig\nimport me.superbear.todolist.assistant.LlmRuntime\nimport me.superbear.todolist.assistant.SearchRuntime\nimport me.superbear.todolist.assistant.search.SearchCapabilityKind\nimport me.superbear.todolist.assistant.subtask.DivisionStrategy\n\ndata class ProviderInfo(\n val displayName: String,\n val defaultModel: String,\n val fallbackModels: List<String>\n)\n\n/**\n * Owns AI provider/API-key/model settings, app-level AI preferences (useAI/maxSubtasks/strategy),\n * and language switching. Pushes provider/key/model changes into the shared [LlmRuntime].\n */\nclass SettingsCoordinator(\n private val prefs: SharedPreferences,\n private val llmRuntime: LlmRuntime,\n private val searchRuntime: SearchRuntime\n) {\n val PROVIDER_INFO = mapOf(\n LLMProvider.OpenAI to ProviderInfo(\"OpenAI\", \"gpt-5-mini\", listOf(\n \"gpt-5.5\", \"gpt-5.4\", \"gpt-5.4-mini\", \"gpt-5.4-nano\",\n \"gpt-5.2\", \"gpt-5.1\",\n \"gpt-5\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"gpt-4.1\", \"gpt-4.1-mini\", \"gpt-4.1-nano\",\n \"gpt-4o\", \"gpt-4o-mini\",\n \"o3-mini\", \"o4-mini\"\n )),\n LLMProvider.DeepSeek to ProviderInfo(\"DeepSeek\", \"deepseek-v4-flash\", listOf(\n \"deepseek-v4-flash\", \"deepseek-v4-pro\"\n ))\n )\n\n private val _selectedProvider = MutableStateFlow<LLMProvider>(LLMProvider.OpenAI)\n val selectedProvider: StateFlow<LLMProvider> = _selectedProvider.asStateFlow()\n\n private val _currentApiKey = MutableStateFlow(\"\")\n val currentApiKey: StateFlow<String> = _currentApiKey.asStateFlow()\n\n private val _selectedModel = MutableStateFlow(\"\")\n val selectedModel: StateFlow<String> = _selectedModel.asStateFlow()\n\n private val _availableModels = MutableStateFlow<List<String>>(emptyList())\n val availableModels: StateFlow<List<String>> = _availableModels.asStateFlow()\n\n private val _isLoadingModels = MutableStateFlow(false)\n val isLoadingModels: StateFlow<Boolean> = _isLoadingModels.asStateFlow()\n\n private val _selectedSearchCapability = MutableStateFlow(SearchCapabilityKind.TAVILY)\n val selectedSearchCapability: StateFlow<SearchCapabilityKind> = _selectedSearchCapability.asStateFlow()\n\n private val _currentSearchApiKey = MutableStateFlow(\"\")\n val currentSearchApiKey: StateFlow<String> = _currentSearchApiKey.asStateFlow()\n\n private val _settingsState = MutableStateFlow(\n SettingsState(\n useAI = prefs.getBoolean(\"settings_use_ai\", true),\n maxSubtasks = prefs.getInt(\"settings_max_subtasks\", 5),\n aiDivisionStrategy = prefs.getString(\"settings_ai_strategy\", null)\n ?.let { runCatching { DivisionStrategy.valueOf(it) }.getOrNull() }\n ?: DivisionStrategy.BALANCED,\n currentLanguage = AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: \"auto\"\n )\n )\n val settingsState: StateFlow<SettingsState> = _settingsState.asStateFlow()\n\n init {\n // Migrate old keys to provider-prefixed format\n val oldKey = prefs.getString(\"api_key\", null)\n if (oldKey != null && prefs.getString(\"openai_api_key\", null) == null) {\n prefs.edit().putString(\"openai_api_key\", oldKey).remove(\"api_key\").apply()\n }\n val oldModel = prefs.getString(\"selected_model\", null)\n if (oldModel != null && prefs.getString(\"openai_selected_model\", null) == null) {\n prefs.edit().putString(\"openai_selected_model\", oldModel).remove(\"selected_model\").apply()\n }\n\n // Debug-only, one-time seed: copy the developer's local.properties test keys into prefs\n // so debug builds work out of the box, the same way SeedManager seeds sample tasks once.\n // Runs exactly once, ever (guarded by DEBUG_KEYS_SEEDED_KEY) — after that, loadApiKey()\n // has no BuildConfig fallback, so explicitly clearing a key in Settings actually sticks\n // instead of reappearing on the next launch.\n if (BuildConfig.DEBUG && !prefs.getBoolean(DEBUG_KEYS_SEEDED_KEY, false)) {\n seedDebugKeyIfBlank(LLMProvider.OpenAI, BuildConfig.OPENAI_API_KEY)\n seedDebugKeyIfBlank(LLMProvider.DeepSeek, BuildConfig.DEEPSEEK_API_KEY)\n seedDebugKeyIfBlank(SEARCH_API_KEY_PREF, BuildConfig.TAVILY_API_KEY)\n prefs.edit().putBoolean(DEBUG_KEYS_SEEDED_KEY, true).apply()\n }\n\n val initialProviderStr = prefs.getString(\"selected_provider\", null)\n val initialProvider = when (initialProviderStr) {\n \"openai\" -> LLMProvider.OpenAI\n \"deepseek\" -> LLMProvider.DeepSeek\n else -> LLMProvider.OpenAI\n }\n val initialInfo = PROVIDER_INFO[initialProvider] ?: PROVIDER_INFO[LLMProvider.OpenAI]!!\n\n val initialKey = loadApiKey(initialProvider)\n val initialModel = loadModel(initialProvider, initialInfo)\n\n llmRuntime.setApiKey(initialProvider, initialKey)\n llmRuntime.selectProvider(initialProvider)\n llmRuntime.selectModelByName(initialModel)\n\n _selectedProvider.value = initialProvider\n _currentApiKey.value = initialKey\n _selectedModel.value = initialModel\n\n refreshModels()\n\n val initialSearchCapability = loadSearchCapability()\n val initialSearchKey = loadSearchApiKey()\n searchRuntime.setSearchCapability(initialSearchCapability)\n searchRuntime.setSearchApiKey(initialSearchKey)\n _selectedSearchCapability.value = initialSearchCapability\n _currentSearchApiKey.value = initialSearchKey\n }\n\n private fun loadApiKey(provider: LLMProvider): String {\n return prefs.getString(\"${provider.id.lowercase()}_api_key\", null) ?: \"\"\n }\n\n private fun seedDebugKeyIfBlank(provider: LLMProvider, debugKey: String) {\n seedDebugKeyIfBlank(prefKey = \"${provider.id.lowercase()}_api_key\", debugKey = debugKey)\n }\n\n private fun seedDebugKeyIfBlank(prefKey: String, debugKey: String) {\n if (debugKey.isBlank()) return\n if (prefs.getString(prefKey, null).isNullOrBlank()) {\n prefs.edit().putString(prefKey, debugKey).apply()\n }\n }\n\n private fun loadSearchApiKey(): String {\n return prefs.getString(SEARCH_API_KEY_PREF, null) ?: \"\"\n }\n\n private fun loadSearchCapability(): SearchCapabilityKind {\n return prefs.getString(SEARCH_CAPABILITY_PREF, null)\n ?.let { runCatching { SearchCapabilityKind.valueOf(it) }.getOrNull() }\n ?: SearchCapabilityKind.TAVILY\n }\n\n private fun loadModel(provider: LLMProvider, info: ProviderInfo = PROVIDER_INFO[provider]!!): String {\n val model = prefs.getString(\"${provider.id.lowercase()}_selected_model\", null)\n return model?.takeIf { it.isNotBlank() } ?: info.defaultModel\n }\n\n fun selectProvider(provider: LLMProvider) {\n val info = PROVIDER_INFO[provider] ?: return\n prefs.edit().putString(\"selected_provider\", provider.id).apply()\n\n val key = loadApiKey(provider)\n val model = loadModel(provider, info)\n llmRuntime.setApiKey(provider, key)\n llmRuntime.selectProvider(provider)\n llmRuntime.selectModelByName(model)\n\n _selectedProvider.value = provider\n _currentApiKey.value = key\n _selectedModel.value = model\n refreshModels()\n }\n\n fun setApiKey(provider: LLMProvider, key: String) {\n val trimmed = key.trim()\n prefs.edit().putString(\"${provider.id.lowercase()}_api_key\", trimmed).apply()\n if (_selectedProvider.value == provider) {\n llmRuntime.setApiKey(provider, trimmed)\n _currentApiKey.value = trimmed\n }\n }\n\n fun selectModel(model: String) {\n val provider = _selectedProvider.value\n prefs.edit().putString(\"${provider.id.lowercase()}_selected_model\", model).apply()\n llmRuntime.selectModelByName(model)\n _selectedModel.value = model\n }\n\n fun refreshModels() {\n val info = PROVIDER_INFO[_selectedProvider.value]\n if (info != null) {\n _availableModels.value = info.fallbackModels\n }\n }\n\n fun setSearchCapability(kind: SearchCapabilityKind) {\n prefs.edit().putString(SEARCH_CAPABILITY_PREF, kind.name).apply()\n searchRuntime.setSearchCapability(kind)\n _selectedSearchCapability.value = kind\n }\n\n fun setSearchApiKey(key: String) {\n val trimmed = key.trim()\n prefs.edit().putString(SEARCH_API_KEY_PREF, trimmed).apply()\n searchRuntime.setSearchApiKey(trimmed)\n _currentSearchApiKey.value = trimmed\n }\n\n fun updateSettings(settings: SettingsState) {\n prefs.edit()\n .putBoolean(\"settings_use_ai\", settings.useAI)\n .putInt(\"settings_max_subtasks\", settings.maxSubtasks)\n .putString(\"settings_ai_strategy\", settings.aiDivisionStrategy.name)\n .apply()\n _settingsState.value = settings\n }\n\n fun updateLanguage(languageTag: String) {\n val appLocale: LocaleListCompat = if (languageTag == \"auto\") {\n LocaleListCompat.getEmptyLocaleList()\n } else {\n LocaleListCompat.forLanguageTags(languageTag)\n }\n AppCompatDelegate.setApplicationLocales(appLocale)\n _settingsState.update { it.copy(currentLanguage = languageTag) }\n }\n\n companion object {\n private const val DEBUG_KEYS_SEEDED_KEY = \"debug_keys_seeded_v1\"\n private const val SEARCH_API_KEY_PREF = \"tavily_api_key\"\n private const val SEARCH_CAPABILITY_PREF = \"search_capability_kind\"\n }\n}\n"}, "aa0a2e33e41636d781c90a0eaf5796f86497b677": {"t": 1785755981.8827481, "n": 9973, "h": "a39051e25455bf08", "c": "package me.superbear.todolist.ui.settings\n\nimport ai.koog.prompt.llm.LLMProvider\nimport android.content.SharedPreferences\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.os.LocaleListCompat\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport me.superbear.todolist.BuildConfig\nimport me.superbear.todolist.assistant.LlmRuntime\nimport me.superbear.todolist.assistant.SearchRuntime\nimport me.superbear.todolist.assistant.search.SearchCapabilityKind\nimport me.superbear.todolist.assistant.subtask.DivisionStrategy\n\ndata class ProviderInfo(\n val displayName: String,\n val defaultModel: String,\n val fallbackModels: List<String>\n)\n\n/**\n * Owns AI provider/API-key/model settings, app-level AI preferences (useAI/maxSubtasks/strategy),\n * and language switching. Pushes provider/key/model changes into the shared [LlmRuntime].\n */\nclass SettingsCoordinator(\n private val prefs: SharedPreferences,\n private val llmRuntime: LlmRuntime,\n private val searchRuntime: SearchRuntime\n) {\n val PROVIDER_INFO = mapOf(\n LLMProvider.OpenAI to ProviderInfo(\"OpenAI\", \"gpt-5-mini\", listOf(\n \"gpt-5.5\", \"gpt-5.4\", \"gpt-5.4-mini\", \"gpt-5.4-nano\",\n \"gpt-5.2\", \"gpt-5.1\",\n \"gpt-5\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"gpt-4.1\", \"gpt-4.1-mini\", \"gpt-4.1-nano\",\n \"gpt-4o\", \"gpt-4o-mini\",\n \"o3-mini\", \"o4-mini\"\n )),\n LLMProvider.DeepSeek to ProviderInfo(\"DeepSeek\", \"deepseek-v4-flash\", listOf(\n \"deepseek-v4-flash\", \"deepseek-v4-pro\"\n ))\n )\n\n private val _selectedProvider = MutableStateFlow<LLMProvider>(LLMProvider.OpenAI)\n val selectedProvider: StateFlow<LLMProvider> = _selectedProvider.asStateFlow()\n\n private val _currentApiKey = MutableStateFlow(\"\")\n val currentApiKey: StateFlow<String> = _currentApiKey.asStateFlow()\n\n private val _selectedModel = MutableStateFlow(\"\")\n val selectedModel: StateFlow<String> = _selectedModel.asStateFlow()\n\n private val _availableModels = MutableStateFlow<List<String>>(emptyList())\n val availableModels: StateFlow<List<String>> = _availableModels.asStateFlow()\n\n private val _isLoadingModels = MutableStateFlow(false)\n val isLoadingModels: StateFlow<Boolean> = _isLoadingModels.asStateFlow()\n\n private val _selectedSearchCapability = MutableStateFlow(SearchCapabilityKind.TAVILY)\n val selectedSearchCapability: StateFlow<SearchCapabilityKind> = _selectedSearchCapability.asStateFlow()\n\n private val _currentSearchApiKey = MutableStateFlow(\"\")\n val currentSearchApiKey: StateFlow<String> = _currentSearchApiKey.asStateFlow()\n\n private val _settingsState = MutableStateFlow(\n SettingsState(\n useAI = prefs.getBoolean(\"settings_use_ai\", true),\n maxSubtasks = prefs.getInt(\"settings_max_subtasks\", 5),\n aiDivisionStrategy = prefs.getString(\"settings_ai_strategy\", null)\n ?.let { runCatching { DivisionStrategy.valueOf(it) }.getOrNull() }\n ?: DivisionStrategy.BALANCED,\n currentLanguage = AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: \"auto\"\n )\n )\n val settingsState: StateFlow<SettingsState> = _settingsState.asStateFlow()\n\n init {\n // Migrate old keys to provider-prefixed format\n val oldKey = prefs.getString(\"api_key\", null)\n if (oldKey != null && prefs.getString(\"openai_api_key\", null) == null) {\n prefs.edit().putString(\"openai_api_key\", oldKey).remove(\"api_key\").apply()\n }\n val oldModel = prefs.getString(\"selected_model\", null)\n if (oldModel != null && prefs.getString(\"openai_selected_model\", null) == null) {\n prefs.edit().putString(\"openai_selected_model\", oldModel).remove(\"selected_model\").apply()\n }\n\n // Debug-only, one-time seed: copy the developer's local.properties test keys into prefs\n // so debug builds work out of the box, the same way SeedManager seeds sample tasks once.\n // Runs exactly once, ever (guarded by DEBUG_KEYS_SEEDED_KEY) — after that, loadApiKey()\n // has no BuildConfig fallback, so explicitly clearing a key in Settings actually sticks\n // instead of reappearing on the next launch.\n if (BuildConfig.DEBUG && !prefs.getBoolean(DEBUG_KEYS_SEEDED_KEY, false)) {\n seedDebugKeyIfBlank(LLMProvider.OpenAI, BuildConfig.OPENAI_API_KEY)\n seedDebugKeyIfBlank(LLMProvider.DeepSeek, BuildConfig.DEEPSEEK_API_KEY)\n seedDebugKeyIfBlank(SEARCH_API_KEY_PREF, BuildConfig.TAVILY_API_KEY)\n prefs.edit().putBoolean(DEBUG_KEYS_SEEDED_KEY, true).apply()\n }\n\n val initialProviderStr = prefs.getString(\"selected_provider\", null)\n val initialProvider = when (initialProviderStr) {\n \"openai\" -> LLMProvider.OpenAI\n \"deepseek\" -> LLMProvider.DeepSeek\n else -> LLMProvider.OpenAI\n }\n val initialInfo = PROVIDER_INFO[initialProvider] ?: PROVIDER_INFO[LLMProvider.OpenAI]!!\n\n val initialKey = loadApiKey(initialProvider)\n val initialModel = loadModel(initialProvider, initialInfo)\n\n llmRuntime.setApiKey(initialProvider, initialKey)\n llmRuntime.selectProvider(initialProvider)\n llmRuntime.selectModelByName(initialModel)\n\n _selectedProvider.value = initialProvider\n _currentApiKey.value = initialKey\n _selectedModel.value = initialModel\n\n refreshModels()\n\n val initialSearchCapability = loadSearchCapability()\n val initialSearchKey = loadSearchApiKey()\n searchRuntime.setSearchCapability(initialSearchCapability)\n searchRuntime.setSearchApiKey(initialSearchKey)\n _selectedSearchCapability.value = initialSearchCapability\n _currentSearchApiKey.value = initialSearchKey\n }\n\n private fun loadApiKey(provider: LLMProvider): String {\n return prefs.getString(\"${provider.id.lowercase()}_api_key\", null) ?: \"\"\n }\n\n private fun seedDebugKeyIfBlank(provider: LLMProvider, debugKey: String) {\n seedDebugKeyIfBlank(prefKey = \"${provider.id.lowercase()}_api_key\", debugKey = debugKey)\n }\n\n private fun seedDebugKeyIfBlank(prefKey: String, debugKey: String) {\n if (debugKey.isBlank()) return\n if (prefs.getString(prefKey, null).isNullOrBlank()) {\n prefs.edit().putString(prefKey, debugKey).apply()\n }\n }\n\n private fun loadSearchApiKey(): String {\n return prefs.getString(SEARCH_API_KEY_PREF, null) ?: \"\"\n }\n\n private fun loadSearchCapability(): SearchCapabilityKind {\n return prefs.getString(SEARCH_CAPABILITY_PREF, null)\n ?.let { runCatching { SearchCapabilityKind.valueOf(it) }.getOrNull() }\n ?: SearchCapabilityKind.TAVILY\n }\n\n private fun loadModel(provider: LLMProvider, info: ProviderInfo = PROVIDER_INFO[provider]!!): String {\n val model = prefs.getString(\"${provider.id.lowercase()}_selected_model\", null)\n return model?.takeIf { it.isNotBlank() } ?: info.defaultModel\n }\n\n fun selectProvider(provider: LLMProvider) {\n val info = PROVIDER_INFO[provider] ?: return\n prefs.edit().putString(\"selected_provider\", provider.id).apply()\n\n val key = loadApiKey(provider)\n val model = loadModel(provider, info)\n llmRuntime.setApiKey(provider, key)\n llmRuntime.selectProvider(provider)\n llmRuntime.selectModelByName(model)\n\n _selectedProvider.value = provider\n _currentApiKey.value = key\n _selectedModel.value = model\n refreshModels()\n }\n\n fun setApiKey(provider: LLMProvider, key: String) {\n val trimmed = key.trim()\n prefs.edit().putString(\"${provider.id.lowercase()}_api_key\", trimmed).apply()\n if (_selectedProvider.value == provider) {\n llmRuntime.setApiKey(provider, trimmed)\n _currentApiKey.value = trimmed\n }\n }\n\n fun selectModel(model: String) {\n val provider = _selectedProvider.value\n prefs.edit().putString(\"${provider.id.lowercase()}_selected_model\", model).apply()\n llmRuntime.selectModelByName(model)\n _selectedModel.value = model\n }\n\n fun refreshModels() {\n val info = PROVIDER_INFO[_selectedProvider.value]\n if (info != null) {\n _availableModels.value = info.fallbackModels\n }\n }\n\n fun setSearchCapability(kind: SearchCapabilityKind) {\n prefs.edit().putString(SEARCH_CAPABILITY_PREF, kind.name).apply()\n searchRuntime.setSearchCapability(kind)\n _selectedSearchCapability.value = kind\n }\n\n fun setSearchApiKey(key: String) {\n val trimmed = key.trim()\n prefs.edit().putString(SEARCH_API_KEY_PREF, trimmed).apply()\n searchRuntime.setSearchApiKey(trimmed)\n _currentSearchApiKey.value = trimmed\n }\n\n fun updateSettings(settings: SettingsState) {\n prefs.edit()\n .putBoolean(\"settings_use_ai\", settings.useAI)\n .putInt(\"settings_max_subtasks\", settings.maxSubtasks)\n .putString(\"settings_ai_strategy\", settings.aiDivisionStrategy.name)\n .apply()\n _settingsState.value = settings\n }\n\n fun updateLanguage(languageTag: String) {\n val appLocale: LocaleListCompat = if (languageTag == \"auto\") {\n LocaleListCompat.getEmptyLocaleList()\n } else {\n LocaleListCompat.forLanguageTags(languageTag)\n }\n AppCompatDelegate.setApplicationLocales(appLocale)\n _settingsState.update { it.copy(currentLanguage = languageTag) }\n }\n\n companion object {\n private const val DEBUG_KEYS_SEEDED_KEY = \"debug_keys_seeded_v1\"\n private const val SEARCH_API_KEY_PREF = \"tavily_api_key\"\n private const val SEARCH_CAPABILITY_PREF = \"search_capability_kind\"\n }\n}\n"}, "7e1c99493920f24d3f49919990e4b4f6ed6366ad": {"t": 1785755983.3757684, "n": 7921, "h": "d5f2bb6ebdcba597", "c": "package me.superbear.todolist.ui.settings\n\nimport ai.koog.prompt.llm.LLMProvider\nimport android.content.SharedPreferences\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.os.LocaleListCompat\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport me.superbear.todolist.BuildConfig\nimport me.superbear.todolist.assistant.LlmRuntime\nimport me.superbear.todolist.assistant.subtask.DivisionStrategy\n\ndata class ProviderInfo(\n val displayName: String,\n val defaultModel: String,\n val fallbackModels: List<String>\n)\n\n/**\n * Owns AI provider/API-key/model settings, app-level AI preferences (useAI/maxSubtasks/strategy),\n * and language switching. Pushes provider/key/model changes into the shared [LlmRuntime].\n */\nclass SettingsCoordinator(\n private val prefs: SharedPreferences,\n private val llmRuntime: LlmRuntime\n) {\n val PROVIDER_INFO = mapOf(\n LLMProvider.OpenAI to ProviderInfo(\"OpenAI\", \"gpt-5-mini\", listOf(\n \"gpt-5.5\", \"gpt-5.4\", \"gpt-5.4-mini\", \"gpt-5.4-nano\",\n \"gpt-5.2\", \"gpt-5.1\",\n \"gpt-5\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"gpt-4.1\", \"gpt-4.1-mini\", \"gpt-4.1-nano\",\n \"gpt-4o\", \"gpt-4o-mini\",\n \"o3-mini\", \"o4-mini\"\n )),\n LLMProvider.DeepSeek to ProviderInfo(\"DeepSeek\", \"deepseek-v4-flash\", listOf(\n \"deepseek-v4-flash\", \"deepseek-v4-pro\"\n ))\n )\n\n private val _selectedProvider = MutableStateFlow<LLMProvider>(LLMProvider.OpenAI)\n val selectedProvider: StateFlow<LLMProvider> = _selectedProvider.asStateFlow()\n\n private val _currentApiKey = MutableStateFlow(\"\")\n val currentApiKey: StateFlow<String> = _currentApiKey.asStateFlow()\n\n private val _selectedModel = MutableStateFlow(\"\")\n val selectedModel: StateFlow<String> = _selectedModel.asStateFlow()\n\n private val _availableModels = MutableStateFlow<List<String>>(emptyList())\n val availableModels: StateFlow<List<String>> = _availableModels.asStateFlow()\n\n private val _isLoadingModels = MutableStateFlow(false)\n val isLoadingModels: StateFlow<Boolean> = _isLoadingModels.asStateFlow()\n\n private val _settingsState = MutableStateFlow(\n SettingsState(\n useAI = prefs.getBoolean(\"settings_use_ai\", true),\n maxSubtasks = prefs.getInt(\"settings_max_subtasks\", 5),\n aiDivisionStrategy = prefs.getString(\"settings_ai_strategy\", null)\n ?.let { runCatching { DivisionStrategy.valueOf(it) }.getOrNull() }\n ?: DivisionStrategy.BALANCED,\n currentLanguage = AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: \"auto\"\n )\n )\n val settingsState: StateFlow<SettingsState> = _settingsState.asStateFlow()\n\n init {\n // Migrate old keys to provider-prefixed format\n val oldKey = prefs.getString(\"api_key\", null)\n if (oldKey != null && prefs.getString(\"openai_api_key\", null) == null) {\n prefs.edit().putString(\"openai_api_key\", oldKey).remove(\"api_key\").apply()\n }\n val oldModel = prefs.getString(\"selected_model\", null)\n if (oldModel != null && prefs.getString(\"openai_selected_model\", null) == null) {\n prefs.edit().putString(\"openai_selected_model\", oldModel).remove(\"selected_model\").apply()\n }\n\n // Debug-only, one-time seed: copy the developer's local.properties test keys into prefs\n // so debug builds work out of the box, the same way SeedManager seeds sample tasks once.\n // Runs exactly once, ever (guarded by DEBUG_KEYS_SEEDED_KEY) — after that, loadApiKey()\n // has no BuildConfig fallback, so explicitly clearing a key in Settings actually sticks\n // instead of reappearing on the next launch.\n if (BuildConfig.DEBUG && !prefs.getBoolean(DEBUG_KEYS_SEEDED_KEY, false)) {\n seedDebugKeyIfBlank(LLMProvider.OpenAI, BuildConfig.OPENAI_API_KEY)\n seedDebugKeyIfBlank(LLMProvider.DeepSeek, BuildConfig.DEEPSEEK_API_KEY)\n prefs.edit().putBoolean(DEBUG_KEYS_SEEDED_KEY, true).apply()\n }\n\n val initialProviderStr = prefs.getString(\"selected_provider\", null)\n val initialProvider = when (initialProviderStr) {\n \"openai\" -> LLMProvider.OpenAI\n \"deepseek\" -> LLMProvider.DeepSeek\n else -> LLMProvider.OpenAI\n }\n val initialInfo = PROVIDER_INFO[initialProvider] ?: PROVIDER_INFO[LLMProvider.OpenAI]!!\n\n val initialKey = loadApiKey(initialProvider)\n val initialModel = loadModel(initialProvider, initialInfo)\n\n llmRuntime.setApiKey(initialProvider, initialKey)\n llmRuntime.selectProvider(initialProvider)\n llmRuntime.selectModelByName(initialModel)\n\n _selectedProvider.value = initialProvider\n _currentApiKey.value = initialKey\n _selectedModel.value = initialModel\n\n refreshModels()\n }\n\n private fun loadApiKey(provider: LLMProvider): String {\n return prefs.getString(\"${provider.id.lowercase()}_api_key\", null) ?: \"\"\n }\n\n private fun seedDebugKeyIfBlank(provider: LLMProvider, debugKey: String) {\n if (debugKey.isBlank()) return\n val prefKey = \"${provider.id.lowercase()}_api_key\"\n if (prefs.getString(prefKey, null).isNullOrBlank()) {\n prefs.edit().putString(prefKey, debugKey).apply()\n }\n }\n\n private fun loadModel(provider: LLMProvider, info: ProviderInfo = PROVIDER_INFO[provider]!!): String {\n val model = prefs.getString(\"${provider.id.lowercase()}_selected_model\", null)\n return model?.takeIf { it.isNotBlank() } ?: info.defaultModel\n }\n\n fun selectProvider(provider: LLMProvider) {\n val info = PROVIDER_INFO[provider] ?: return\n prefs.edit().putString(\"selected_provider\", provider.id).apply()\n\n val key = loadApiKey(provider)\n val model = loadModel(provider, info)\n llmRuntime.setApiKey(provider, key)\n llmRuntime.selectProvider(provider)\n llmRuntime.selectModelByName(model)\n\n _selectedProvider.value = provider\n _currentApiKey.value = key\n _selectedModel.value = model\n refreshModels()\n }\n\n fun setApiKey(provider: LLMProvider, key: String) {\n val trimmed = key.trim()\n prefs.edit().putString(\"${provider.id.lowercase()}_api_key\", trimmed).apply()\n if (_selectedProvider.value == provider) {\n llmRuntime.setApiKey(provider, trimmed)\n _currentApiKey.value = trimmed\n }\n }\n\n fun selectModel(model: String) {\n val provider = _selectedProvider.value\n prefs.edit().putString(\"${provider.id.lowercase()}_selected_model\", model).apply()\n llmRuntime.selectModelByName(model)\n _selectedModel.value = model\n }\n\n fun refreshModels() {\n val info = PROVIDER_INFO[_selectedProvider.value]\n if (info != null) {\n _availableModels.value = info.fallbackModels\n }\n }\n\n fun updateSettings(settings: SettingsState) {\n prefs.edit()\n .putBoolean(\"settings_use_ai\", settings.useAI)\n .putInt(\"settings_max_subtasks\", settings.maxSubtasks)\n .putString(\"settings_ai_strategy\", settings.aiDivisionStrategy.name)\n .apply()\n _settingsState.value = settings\n }\n\n fun updateLanguage(languageTag: String) {\n val appLocale: LocaleListCompat = if (languageTag == \"auto\") {\n LocaleListCompat.getEmptyLocaleList()\n } else {\n LocaleListCompat.forLanguageTags(languageTag)\n }\n AppCompatDelegate.setApplicationLocales(appLocale)\n _settingsState.update { it.copy(currentLanguage = languageTag) }\n }\n\n companion object {\n private const val DEBUG_KEYS_SEEDED_KEY = \"debug_keys_seeded_v1\"\n }\n}\n"}, "48d1716f697d5c6a2f29110659f2aa467d69e2f9": {"t": 1785755985.0732758, "n": 6699, "h": "10ae4c0dafcda107", "c": "package me.superbear.todolist.ui.settings\n\nimport ai.koog.prompt.llm.LLMProvider\nimport android.content.SharedPreferences\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.os.LocaleListCompat\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport me.superbear.todolist.BuildConfig\nimport me.superbear.todolist.assistant.LlmRuntime\nimport me.superbear.todolist.assistant.subtask.DivisionStrategy\n\ndata class ProviderInfo(\n val displayName: String,\n val defaultModel: String,\n val fallbackModels: List<String>\n)\n\n/**\n * Owns AI provider/API-key/model settings, app-level AI preferences (useAI/maxSubtasks/strategy),\n * and language switching. Pushes provider/key/model changes into the shared [LlmRuntime].\n */\nclass SettingsCoordinator(\n private val prefs: SharedPreferences,\n private val llmRuntime: LlmRuntime\n) {\n val PROVIDER_INFO = mapOf(\n LLMProvider.OpenAI to ProviderInfo(\"OpenAI\", \"gpt-5-mini\", listOf(\n \"gpt-5\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"gpt-4.1\", \"gpt-4.1-mini\", \"gpt-4.1-nano\",\n \"gpt-4o\", \"gpt-4o-mini\",\n \"o3-mini\", \"o4-mini\"\n )),\n LLMProvider.DeepSeek to ProviderInfo(\"DeepSeek\", \"deepseek-v4-flash\", listOf(\n \"deepseek-v4-flash\", \"deepseek-v4-pro\"\n ))\n )\n\n private val _selectedProvider = MutableStateFlow<LLMProvider>(LLMProvider.OpenAI)\n val selectedProvider: StateFlow<LLMProvider> = _selectedProvider.asStateFlow()\n\n private val _currentApiKey = MutableStateFlow(\"\")\n val currentApiKey: StateFlow<String> = _currentApiKey.asStateFlow()\n\n private val _selectedModel = MutableStateFlow(\"\")\n val selectedModel: StateFlow<String> = _selectedModel.asStateFlow()\n\n private val _availableModels = MutableStateFlow<List<String>>(emptyList())\n val availableModels: StateFlow<List<String>> = _availableModels.asStateFlow()\n\n private val _isLoadingModels = MutableStateFlow(false)\n val isLoadingModels: StateFlow<Boolean> = _isLoadingModels.asStateFlow()\n\n private val _settingsState = MutableStateFlow(\n SettingsState(\n useAI = prefs.getBoolean(\"settings_use_ai\", true),\n maxSubtasks = prefs.getInt(\"settings_max_subtasks\", 5),\n aiDivisionStrategy = prefs.getString(\"settings_ai_strategy\", null)\n ?.let { runCatching { DivisionStrategy.valueOf(it) }.getOrNull() }\n ?: DivisionStrategy.BALANCED,\n currentLanguage = AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: \"auto\"\n )\n )\n val settingsState: StateFlow<SettingsState> = _settingsState.asStateFlow()\n\n init {\n // Migrate old keys to provider-prefixed format\n val oldKey = prefs.getString(\"api_key\", null)\n if (oldKey != null && prefs.getString(\"openai_api_key\", null) == null) {\n prefs.edit().putString(\"openai_api_key\", oldKey).remove(\"api_key\").apply()\n }\n val oldModel = prefs.getString(\"selected_model\", null)\n if (oldModel != null && prefs.getString(\"openai_selected_model\", null) == null) {\n prefs.edit().putString(\"openai_selected_model\", oldModel).remove(\"selected_model\").apply()\n }\n\n val initialProviderStr = prefs.getString(\"selected_provider\", null)\n val initialProvider = when (initialProviderStr) {\n \"openai\" -> LLMProvider.OpenAI\n \"deepseek\" -> LLMProvider.DeepSeek\n else -> LLMProvider.OpenAI\n }\n val initialInfo = PROVIDER_INFO[initialProvider] ?: PROVIDER_INFO[LLMProvider.OpenAI]!!\n\n val initialKey = loadApiKey(initialProvider)\n val initialModel = loadModel(initialProvider, initialInfo)\n\n llmRuntime.setApiKey(initialProvider, initialKey)\n llmRuntime.selectProvider(initialProvider)\n llmRuntime.selectModelByName(initialModel)\n\n _selectedProvider.value = initialProvider\n _currentApiKey.value = initialKey\n _selectedModel.value = initialModel\n\n refreshModels()\n }\n\n private fun loadApiKey(provider: LLMProvider): String {\n val key = prefs.getString(\"${provider.id.lowercase()}_api_key\", null)\n return key?.takeIf { it.isNotBlank() } ?: BuildConfig.OPENAI_API_KEY\n }\n\n private fun loadModel(provider: LLMProvider, info: ProviderInfo = PROVIDER_INFO[provider]!!): String {\n val model = prefs.getString(\"${provider.id.lowercase()}_selected_model\", null)\n return model?.takeIf { it.isNotBlank() } ?: info.defaultModel\n }\n\n fun selectProvider(provider: LLMProvider) {\n val info = PROVIDER_INFO[provider] ?: return\n prefs.edit().putString(\"selected_provider\", provider.id).apply()\n\n val key = loadApiKey(provider)\n val model = loadModel(provider, info)\n llmRuntime.setApiKey(provider, key)\n llmRuntime.selectProvider(provider)\n llmRuntime.selectModelByName(model)\n\n _selectedProvider.value = provider\n _currentApiKey.value = key\n _selectedModel.value = model\n refreshModels()\n }\n\n fun setApiKey(provider: LLMProvider, key: String) {\n val trimmed = key.trim()\n prefs.edit().putString(\"${provider.id.lowercase()}_api_key\", trimmed).apply()\n if (_selectedProvider.value == provider) {\n llmRuntime.setApiKey(provider, trimmed)\n _currentApiKey.value = trimmed\n }\n }\n\n fun selectModel(model: String) {\n val provider = _selectedProvider.value\n prefs.edit().putString(\"${provider.id.lowercase()}_selected_model\", model).apply()\n llmRuntime.selectModelByName(model)\n _selectedModel.value = model\n }\n\n fun refreshModels() {\n val info = PROVIDER_INFO[_selectedProvider.value]\n if (info != null) {\n _availableModels.value = info.fallbackModels\n }\n }\n\n fun updateSettings(settings: SettingsState) {\n prefs.edit()\n .putBoolean(\"settings_use_ai\", settings.useAI)\n .putInt(\"settings_max_subtasks\", settings.maxSubtasks)\n .putString(\"settings_ai_strategy\", settings.aiDivisionStrategy.name)\n .apply()\n _settingsState.value = settings\n }\n\n fun updateLanguage(languageTag: String) {\n val appLocale: LocaleListCompat = if (languageTag == \"auto\") {\n LocaleListCompat.getEmptyLocaleList()\n } else {\n LocaleListCompat.forLanguageTags(languageTag)\n }\n AppCompatDelegate.setApplicationLocales(appLocale)\n _settingsState.update { it.copy(currentLanguage = languageTag) }\n }\n}\n"}}}} |