From 2f0998c6c81c4e3c01b55787483f1b17c5c6fd70 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 17 Jan 2026 11:16:58 +0800 Subject: [PATCH] fix(opencode): prevent config nesting and use slugified provider IDs - Skip backfill logic for OpenCode (additive mode doesn't need it) - Add defensive check in write_live_snapshot to extract provider fragment - Use slugified name as provider ID for readable config keys --- src-tauri/src/services/provider/live.rs | 39 +++++++++++++++++++++++-- src-tauri/src/services/provider/mod.rs | 16 ++++++---- src/lib/query/mutations.ts | 21 ++++++++++++- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 9b73796e..9585d3fb 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -125,9 +125,29 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re use crate::opencode_config; use crate::provider::OpenCodeProviderConfig; + // Defensive check: if settings_config is a full config structure, extract provider fragment + let config_to_write = if let Some(obj) = provider.settings_config.as_object() { + // Detect full config structure (has $schema or top-level provider field) + if obj.contains_key("$schema") || obj.contains_key("provider") { + log::warn!( + "OpenCode provider '{}' has full config structure in settings_config, attempting to extract fragment", + provider.id + ); + // Try to extract from provider.{id} + obj.get("provider") + .and_then(|p| p.get(&provider.id)) + .cloned() + .unwrap_or_else(|| provider.settings_config.clone()) + } else { + provider.settings_config.clone() + } + } else { + provider.settings_config.clone() + }; + // Convert settings_config to OpenCodeProviderConfig let opencode_config_result = - serde_json::from_value::(provider.settings_config.clone()); + serde_json::from_value::(config_to_write.clone()); match opencode_config_result { Ok(config) => { @@ -140,8 +160,21 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re provider.id, e ); - // Still try to write as raw JSON - opencode_config::set_provider(&provider.id, provider.settings_config.clone())?; + // Only write if config looks like a valid provider fragment + if config_to_write.get("npm").is_some() + || config_to_write.get("options").is_some() + { + opencode_config::set_provider(&provider.id, config_to_write)?; + log::info!( + "OpenCode provider '{}' written as raw JSON to live config", + provider.id + ); + } else { + log::error!( + "OpenCode provider '{}' has invalid config structure, skipping write", + provider.id + ); + } } } } diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index a749e770..c9bc0a43 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -351,12 +351,16 @@ impl ProviderService { if let Some(current_id) = current_id { if current_id != id { - // Only backfill when switching to a different provider - if let Ok(live_config) = read_live_settings(app_type.clone()) { - if let Some(mut current_provider) = providers.get(¤t_id).cloned() { - current_provider.settings_config = live_config; - // Ignore backfill failure, don't affect switch flow - let _ = state.db.save_provider(app_type.as_str(), ¤t_provider); + // OpenCode uses additive mode - all providers coexist in the same file, + // no backfill needed (backfill is for exclusive mode apps like Claude/Codex/Gemini) + if !matches!(app_type, AppType::OpenCode) { + // Only backfill when switching to a different provider + if let Ok(live_config) = read_live_settings(app_type.clone()) { + if let Some(mut current_provider) = providers.get(¤t_id).cloned() { + current_provider.settings_config = live_config; + // Ignore backfill failure, don't affect switch flow + let _ = state.db.save_provider(app_type.as_str(), ¤t_provider); + } } } } diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index b5118680..2a3aee8c 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -6,15 +6,34 @@ import type { Provider, Settings } from "@/types"; import { extractErrorMessage } from "@/utils/errorUtils"; import { generateUUID } from "@/utils/uuid"; +/** + * Convert a name to a URL-safe slug for use as provider ID + * Used for OpenCode's additive mode where ID becomes the config key + */ +function slugify(name: string): string { + return name + .toLowerCase() + .trim() + .replace(/[\s_]+/g, "-") // spaces and underscores → hyphens + .replace(/[^a-z0-9-]/g, "") // remove special characters + .replace(/-+/g, "-") // collapse multiple hyphens + .replace(/^-|-$/g, ""); // trim leading/trailing hyphens +} + export const useAddProviderMutation = (appId: AppId) => { const queryClient = useQueryClient(); const { t } = useTranslation(); return useMutation({ mutationFn: async (providerInput: Omit) => { + // OpenCode: use slugified name as ID (for readable config keys) + // Other apps: use random UUID + const id = + appId === "opencode" ? slugify(providerInput.name) : generateUUID(); + const newProvider: Provider = { ...providerInput, - id: generateUUID(), + id, createdAt: Date.now(), }; await providersApi.add(newProvider, appId);