From 392756e37330b3b55aff76a2d22fc403300a41a9 Mon Sep 17 00:00:00 2001 From: Xyfer <39858937+cxyfer@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:42:44 +0800 Subject: [PATCH 1/5] fix(gemini): convert timeout params to Gemini CLI format (#580) Claude Code/Codex uses startup_timeout_sec and tool_timeout_sec, but Gemini CLI only supports a single timeout param in milliseconds. - Collect startup and tool timeout separately with defaults - startup_timeout_sec default: 10s - tool_timeout_sec default: 60s - Take max of both values as final timeout - Support both sec and ms variants - Remove original fields and insert Gemini-compatible timeout --- src-tauri/src/gemini_mcp.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src-tauri/src/gemini_mcp.rs b/src-tauri/src/gemini_mcp.rs index 62a959c18..72d7a86d5 100644 --- a/src-tauri/src/gemini_mcp.rs +++ b/src-tauri/src/gemini_mcp.rs @@ -134,6 +134,33 @@ pub fn set_mcp_servers_map( obj.remove("homepage"); obj.remove("docs"); + // Timeout 转换:Claude/Codex 使用 startup_timeout_sec/tool_timeout_sec + // Gemini CLI 只支持 timeout(单位 ms) + // 默认值:startup=10s, tool=60s + const DEFAULT_STARTUP_MS: u64 = 10_000; + const DEFAULT_TOOL_MS: u64 = 60_000; + + let extract_timeout = + |obj: &mut Map, key: &str, multiplier: u64| -> Option { + obj.remove(key).and_then(|val| { + val.as_u64() + .map(|n| n * multiplier) + .or_else(|| val.as_f64().map(|f| (f * multiplier as f64) as u64)) + }) + }; + + // 分别收集 startup 和 tool timeout,未设置时使用默认值 + let startup_ms = extract_timeout(&mut obj, "startup_timeout_sec", 1000) + .or_else(|| extract_timeout(&mut obj, "startup_timeout_ms", 1)) + .unwrap_or(DEFAULT_STARTUP_MS); + let tool_ms = extract_timeout(&mut obj, "tool_timeout_sec", 1000) + .or_else(|| extract_timeout(&mut obj, "tool_timeout_ms", 1)) + .unwrap_or(DEFAULT_TOOL_MS); + + // 取最大值作为 Gemini timeout + let final_timeout = startup_ms.max(tool_ms); + obj.insert("timeout".to_string(), Value::Number(final_timeout.into())); + out.insert(id.clone(), Value::Object(obj)); } From 83db457b1044c039304a8d0dbae287bb7d6aedcf Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 10 Jan 2026 11:03:34 +0800 Subject: [PATCH 2/5] refactor(proxy): disable OpenRouter compat mode by default and hide UI toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenRouter now natively supports Claude Code compatible API (/v1/messages), so format transformation (Anthropic ↔ OpenAI) is no longer needed by default. - Change default value from `true` to `false` in both frontend and backend - Hide the "OpenRouter Compatibility Mode" toggle in provider form - Users can still enable it manually by adding `"openrouter_compat_mode": true` in config JSON - Update unit tests to reflect new default behavior --- src-tauri/src/proxy/providers/claude.rs | 15 +++++++++++++-- src/components/providers/forms/ProviderForm.tsx | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 87a0d85c9..04d4ec6ff 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -62,7 +62,8 @@ impl ClaudeAdapter { let normalized = value.trim().to_lowercase(); normalized == "true" || normalized == "1" } - _ => true, + // OpenRouter now supports Claude Code compatible API, default to passthrough + _ => false, } } @@ -465,12 +466,22 @@ mod tests { })); assert!(!adapter.needs_transform(&anthropic_provider)); + // OpenRouter provider without explicit setting now defaults to passthrough (no transform) let openrouter_provider = create_provider(json!({ "env": { "ANTHROPIC_BASE_URL": "https://openrouter.ai/api" } })); - assert!(adapter.needs_transform(&openrouter_provider)); + assert!(!adapter.needs_transform(&openrouter_provider)); + + // OpenRouter provider with explicit compat mode enabled should transform + let openrouter_enabled = create_provider(json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://openrouter.ai/api" + }, + "openrouter_compat_mode": true + })); + assert!(adapter.needs_transform(&openrouter_enabled)); let openrouter_disabled = create_provider(json!({ "env": { diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 514367740..cb97473d5 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -236,7 +236,7 @@ export function ProviderForm({ } catch { // ignore } - return true; + return false; // OpenRouter now supports Claude Code compatible API, no need for transform }, [isOpenRouterProvider, settingsConfigValue]); const handleOpenRouterCompatChange = useCallback( @@ -864,7 +864,7 @@ export function ProviderForm({ defaultOpusModel={defaultOpusModel} onModelChange={handleModelChange} speedTestEndpoints={speedTestEndpoints} - showOpenRouterCompatToggle={isOpenRouterProvider} + showOpenRouterCompatToggle={false} openRouterCompatEnabled={openRouterCompatEnabled} onOpenRouterCompatChange={handleOpenRouterCompatChange} /> From 95ed6d6903a3f933d498aa697911a7bfceef67d8 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 10 Jan 2026 11:05:18 +0800 Subject: [PATCH 3/5] fix(usage): prevent usage script config from leaking between providers Add key prop to UsageScriptModal to ensure component remounts when switching between different providers. This fixes issue #569 where configuring usage query for one provider would incorrectly apply the same configuration to all providers. The root cause was that useState initialization only runs on first mount, and due to useLastValidValue hook keeping the modal rendered during close animation, the component might not fully unmount when switching providers rapidly. Closes #569 --- src-tauri/Cargo.lock | 2 +- src/App.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 51a67662e..e48e0090e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "cc-switch" -version = "3.9.0" +version = "3.9.1" dependencies = [ "anyhow", "async-stream", diff --git a/src/App.tsx b/src/App.tsx index 65e08ba4b..a78d9ce18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -806,6 +806,7 @@ function App() { {effectiveUsageProvider && ( Date: Sat, 10 Jan 2026 11:10:23 +0800 Subject: [PATCH 4/5] fix(provider-form): reset baseUrl and apiKey states when switching presets Fix state synchronization in useBaseUrlState and useApiKeyState hooks to properly clear values when config is reset. Previously, when switching from a preset to "custom", the baseUrl and apiKey states would retain their old values because the sync logic only updated when new values existed, not when they were cleared. Changes: - useBaseUrlState: Always sync baseUrl to config value (empty if undefined) - useApiKeyState: Remove hasApiKeyField check that prevented clearing --- src/components/providers/forms/hooks/useApiKeyState.ts | 6 +----- src/components/providers/forms/hooks/useBaseUrlState.ts | 5 +++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/providers/forms/hooks/useApiKeyState.ts b/src/components/providers/forms/hooks/useApiKeyState.ts index 9cd5fde39..9101d2e35 100644 --- a/src/components/providers/forms/hooks/useApiKeyState.ts +++ b/src/components/providers/forms/hooks/useApiKeyState.ts @@ -43,11 +43,7 @@ export function useApiKeyState({ return; } - // 仅当配置确实包含 API Key 字段时才同步(避免无意清空用户正在输入的 key) - if (!hasApiKeyField(initialConfig, appType)) { - return; - } - + // 从配置中提取 API Key(如果不存在则返回空字符串) const extracted = getApiKeyFromConfig(initialConfig, appType); if (extracted !== apiKey) { setApiKey(extracted); diff --git a/src/components/providers/forms/hooks/useBaseUrlState.ts b/src/components/providers/forms/hooks/useBaseUrlState.ts index 61b1ad997..2deffcbac 100644 --- a/src/components/providers/forms/hooks/useBaseUrlState.ts +++ b/src/components/providers/forms/hooks/useBaseUrlState.ts @@ -41,8 +41,9 @@ export function useBaseUrlState({ try { const config = JSON.parse(settingsConfig || "{}"); const envUrl: unknown = config?.env?.ANTHROPIC_BASE_URL; - if (typeof envUrl === "string" && envUrl && envUrl.trim() !== baseUrl) { - setBaseUrl(envUrl.trim()); + const nextUrl = typeof envUrl === "string" ? envUrl.trim() : ""; + if (nextUrl !== baseUrl) { + setBaseUrl(nextUrl); } } catch { // ignore From 76fa8306886c49626de3820ef66758f508bde9c5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 11 Jan 2026 16:37:18 +0800 Subject: [PATCH 5/5] fix(live): sync skills to app directories on config path change When users change app config directories (claudeConfigDir, codexConfigDir, geminiConfigDir), MCP servers were being synced to the new paths but Skills were not. This adds Skill synchronization to sync_current_to_live() to ensure installed Skills are also copied to the new app directories. --- src-tauri/src/services/provider/live.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 6389827b9..0192f6ecd 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -148,6 +148,15 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> { // MCP sync McpService::sync_all_enabled(state)?; + + // Skill sync + for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] { + if let Err(e) = crate::services::skill::SkillService::sync_to_app(&state.db, &app_type) { + log::warn!("同步 Skill 到 {:?} 失败: {}", app_type, e); + // Continue syncing other apps, don't abort + } + } + Ok(()) }