From a254304a005c47c1172823cafd79e60313014c0c Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 24 Feb 2026 23:08:22 +0800 Subject: [PATCH 01/24] feat: add AICoding partner provider presets and i18n promotion text Add AICoding as a partner provider across all five apps (Claude, Codex, Gemini, OpenClaw, OpenCode) with endpoint, API key URL, and partner promotion configuration. Add trilingual (zh/en/ja) promotion text for the first top-up discount. --- src/config/claudeProviderPresets.ts | 19 +++++++++++ src/config/codexProviderPresets.ts | 18 ++++++++++ src/config/geminiProviderPresets.ts | 22 ++++++++++++ src/config/openclawProviderPresets.ts | 48 ++++++++++++++++++++++++++- src/config/opencodeProviderPresets.ts | 30 ++++++++++++++++- src/i18n/locales/en.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/zh.json | 3 +- 8 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 80697e3eb..aac7bc3d3 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -476,6 +476,25 @@ export const providerPresets: ProviderPreset[] = [ icon: "aicodemirror", iconColor: "#000000", }, + { + name: "AICoding", + websiteUrl: "https://www.aicoding.sh", + apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://api.aicoding.sh", + ANTHROPIC_AUTH_TOKEN: "", + }, + }, + endpointCandidates: [ + "https://api.aicoding.sh", + ], + category: "third_party", + isPartner: true, // 合作伙伴 + partnerPromotionKey: "aicoding", // 促销信息 i18n key + icon: "aicoding", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index 9eac74c51..99af37aea 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -227,6 +227,24 @@ requires_openai_auth = true`, icon: "aicodemirror", iconColor: "#000000", }, + { + name: "AICoding", + websiteUrl: "https://www.aicoding.sh", + apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH", + auth: generateThirdPartyAuth(""), + config: generateThirdPartyConfig( + "aicoding", + "https://api.aicoding.sh", + "gpt-5.3-codex", + ), + endpointCandidates: [ + "https://api.aicoding.sh", + ], + isPartner: true, + partnerPromotionKey: "aicoding", + icon: "aicoding", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/geminiProviderPresets.ts b/src/config/geminiProviderPresets.ts index ec1e1958c..d89e42991 100644 --- a/src/config/geminiProviderPresets.ts +++ b/src/config/geminiProviderPresets.ts @@ -139,6 +139,28 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [ icon: "aicodemirror", iconColor: "#000000", }, + { + name: "AICoding", + websiteUrl: "https://www.aicoding.sh", + apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH", + settingsConfig: { + env: { + GOOGLE_GEMINI_BASE_URL: "https://api.aicoding.sh", + GEMINI_MODEL: "gemini-3-pro", + }, + }, + baseURL: "https://api.aicoding.sh", + model: "gemini-3-pro", + description: "AICoding", + category: "third_party", + isPartner: true, + partnerPromotionKey: "aicoding", + endpointCandidates: [ + "https://api.aicoding.sh", + ], + icon: "aicoding", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 0eab37322..3f7a7d3cc 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -1052,8 +1052,54 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ "aicodemirror/claude-opus-4-6": { alias: "Opus" }, }, }, - }, + }, + { + name: "AICoding", + websiteUrl: "https://www.aicoding.sh", + apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH", + settingsConfig: { + baseUrl: "https://api.aicoding.sh", + apiKey: "", + api: "anthropic-messages", + models: [ + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + contextWindow: 200000, + cost: { input: 5, output: 25 }, + }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "aicoding", + icon: "aicoding", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { + primary: "aicoding/claude-sonnet-4-5-20250929", + fallbacks: ["aicoding/claude-opus-4-6"], + }, + modelCatalog: { + "aicoding/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aicoding/claude-opus-4-6": { alias: "Opus" }, + }, + }, + }, // ========== Cloud Providers ========== { name: "AWS Bedrock", diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 007451164..75574b527 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -1133,7 +1133,35 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, }, - + { + name: "AICoding", + websiteUrl: "https://www.aicoding.sh", + apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH", + settingsConfig: { + npm: "@ai-sdk/anthropic", + name: "AICoding", + options: { + baseURL: "https://api.aicoding.sh", + apiKey: "", + }, + models: { + "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, + }, + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "aicoding", + icon: "aicoding", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, { name: "AWS Bedrock", websiteUrl: "https://aws.amazon.com/bedrock/", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b4deab8a9..330ab8ac6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -625,7 +625,8 @@ "cubence": "Cubence is an official partner of CC Switch. Register using this link and enter \"CCSWITCH\" promo code during recharge to get 10% off every top-up", "aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!", "rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!", - "aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!" + "aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!", + "aicoding": "AI Coding offers an exclusive discount for CC Switch users — 10% off your first top-up!" }, "parameterConfig": "Parameter Config - {{name}} *", "mainModel": "Main Model (optional)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index bfa30d4da..3db2bf85b 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -625,7 +625,8 @@ "cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ", "aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!", "rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!", - "aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!" + "aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!", + "aicoding": "AI Coding は CC Switch ユーザー向けに特別割引を提供しています。初回チャージが 10% オフ!" }, "parameterConfig": "パラメーター設定 - {{name}} *", "mainModel": "メインモデル(任意)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f5f872c0b..3e0004d20 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -625,7 +625,8 @@ "cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠", "aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!", "rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!", - "aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!" + "aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!", + "aicoding": "AI Coding 为 CC Switch 的用户提供了特殊优惠,首次充值可以享受 9 折优惠!" }, "parameterConfig": "参数配置 - {{name}} *", "mainModel": "主模型 (可选)", From 31712f076f0aba16628fc87d88a0cc7e808de581 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 25 Feb 2026 15:39:50 +0800 Subject: [PATCH 02/24] feat: add CrazyRouter partner provider presets, i18n, and register icons Add CrazyRouter as a partner provider across all five apps with endpoint, API key URL, and partner promotion config. Add trilingual promotion text for the 30% bonus credit offer. Register aicoding and crazyrouter icons in the icon index. Fix indentation in openclawProviderPresets. --- src/config/claudeProviderPresets.ts | 19 +++++++ src/config/codexProviderPresets.ts | 18 +++++++ src/config/geminiProviderPresets.ts | 22 ++++++++ src/config/openclawProviderPresets.ts | 32 ++++++++++++ src/config/opencodeProviderPresets.ts | 29 +++++++++++ src/i18n/locales/en.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/zh.json | 3 +- src/icons/extracted/aicoding.svg | 20 ++++++++ src/icons/extracted/crazyrouter.svg | 1 + src/icons/extracted/index.ts | 2 + src/icons/extracted/sssaicoding.svg | 73 +++++++++++++++++++++++++++ 12 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 src/icons/extracted/aicoding.svg create mode 100644 src/icons/extracted/crazyrouter.svg create mode 100644 src/icons/extracted/sssaicoding.svg diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index aac7bc3d3..b0d50d8c9 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -495,6 +495,25 @@ export const providerPresets: ProviderPreset[] = [ icon: "aicoding", iconColor: "#000000", }, + { + name: "CrazyRouter", + websiteUrl: "https://www.crazyrouter.com", + apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://crazyrouter.com", + ANTHROPIC_AUTH_TOKEN: "", + }, + }, + endpointCandidates: [ + "https://crazyrouter.com", + ], + category: "third_party", + isPartner: true, // 合作伙伴 + partnerPromotionKey: "crazyrouter", // 促销信息 i18n key + icon: "crazyrouter", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index 99af37aea..1354f05af 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -245,6 +245,24 @@ requires_openai_auth = true`, icon: "aicoding", iconColor: "#000000", }, + { + name: "CrazyRouter", + websiteUrl: "https://www.crazyrouter.com", + apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch", + auth: generateThirdPartyAuth(""), + config: generateThirdPartyConfig( + "crazyrouter", + "https://crazyrouter.com/v1", + "gpt-5.3-codex", + ), + endpointCandidates: [ + "https://crazyrouter.com/v1", + ], + isPartner: true, + partnerPromotionKey: "crazyrouter", + icon: "crazyrouter", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/geminiProviderPresets.ts b/src/config/geminiProviderPresets.ts index d89e42991..b969ca1c5 100644 --- a/src/config/geminiProviderPresets.ts +++ b/src/config/geminiProviderPresets.ts @@ -161,6 +161,28 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [ icon: "aicoding", iconColor: "#000000", }, + { + name: "CrazyRouter", + websiteUrl: "https://www.crazyrouter.com", + apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch", + settingsConfig: { + env: { + GOOGLE_GEMINI_BASE_URL: "https://crazyrouter.com", + GEMINI_MODEL: "gemini-3-pro", + }, + }, + baseURL: "https://crazyrouter.com", + model: "gemini-3-pro", + description: "CrazyRouter", + category: "third_party", + isPartner: true, + partnerPromotionKey: "crazyrouter", + endpointCandidates: [ + "https://crazyrouter.com", + ], + icon: "crazyrouter", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 3f7a7d3cc..ffe84d4c1 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -1100,6 +1100,38 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, }, }, + { + name: "CrazyRouter", + websiteUrl: "https://www.crazyrouter.com", + apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch", + settingsConfig: { + baseUrl: "https://crazyrouter.com", + apiKey: "", + api: "anthropic-messages", + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "crazyrouter", + icon: "crazyrouter", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { + primary: "crazyrouter/claude-sonnet-4-5-20250929", + fallbacks: ["crazyrouter/claude-opus-4-6"], + }, + modelCatalog: { + "crazyrouter/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "crazyrouter/claude-opus-4-6": { alias: "Opus" }, + }, + }, + }, // ========== Cloud Providers ========== { name: "AWS Bedrock", diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 75574b527..fd17c72a0 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -1162,6 +1162,35 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, }, + { + name: "CrazyRouter", + websiteUrl: "https://www.crazyrouter.com", + apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch", + settingsConfig: { + npm: "@ai-sdk/anthropic", + name: "CrazyRouter", + options: { + baseURL: "https://crazyrouter.com", + apiKey: "", + }, + models: { + "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, + }, + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "crazyrouter", + icon: "crazyrouter", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, { name: "AWS Bedrock", websiteUrl: "https://aws.amazon.com/bedrock/", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 330ab8ac6..cffc59bc4 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -626,7 +626,8 @@ "aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!", "rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!", "aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!", - "aicoding": "AI Coding offers an exclusive discount for CC Switch users — 10% off your first top-up!" + "aicoding": "AI Coding offers an exclusive discount for CC Switch users — 10% off your first top-up!", + "crazyrouter": "CrazyRouter offers an exclusive bonus for CC Switch users — 30% extra credit on your first top-up!" }, "parameterConfig": "Parameter Config - {{name}} *", "mainModel": "Main Model (optional)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3db2bf85b..194645c5e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -626,7 +626,8 @@ "aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!", "rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!", "aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!", - "aicoding": "AI Coding は CC Switch ユーザー向けに特別割引を提供しています。初回チャージが 10% オフ!" + "aicoding": "AI Coding は CC Switch ユーザー向けに特別割引を提供しています。初回チャージが 10% オフ!", + "crazyrouter": "CrazyRouter は CC Switch ユーザー向けに特別ボーナスを提供しています。初回チャージで 30% の追加クレジットがもらえます!" }, "parameterConfig": "パラメーター設定 - {{name}} *", "mainModel": "メインモデル(任意)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 3e0004d20..a5d6e79bd 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -626,7 +626,8 @@ "aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!", "rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!", "aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!", - "aicoding": "AI Coding 为 CC Switch 的用户提供了特殊优惠,首次充值可以享受 9 折优惠!" + "aicoding": "AI Coding 为 CC Switch 的用户提供了特殊优惠,首次充值可以享受 9 折优惠!", + "crazyrouter": "CrazyRouter 为 CC Switch 的用户提供了特殊优惠,首次充值赠予 30% 额外额度!" }, "parameterConfig": "参数配置 - {{name}} *", "mainModel": "主模型 (可选)", diff --git a/src/icons/extracted/aicoding.svg b/src/icons/extracted/aicoding.svg new file mode 100644 index 000000000..356b7237b --- /dev/null +++ b/src/icons/extracted/aicoding.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/icons/extracted/crazyrouter.svg b/src/icons/extracted/crazyrouter.svg new file mode 100644 index 000000000..0ba55b9ce --- /dev/null +++ b/src/icons/extracted/crazyrouter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index a87ab5d88..9d5284b95 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -3,6 +3,7 @@ export const icons: Record = { aicodemirror: `AICodeMirror`, + aicoding: `AICoding`, aigocode: `AiGoCode`, alibaba: `Alibaba`, anthropic: `Anthropic`, @@ -15,6 +16,7 @@ export const icons: Record = { cloudflare: `Cloudflare`, cohere: `Cohere`, copilot: `Copilot`, + crazyrouter: `CrazyRouter`, cubence: `Cubence`, deepseek: `DeepSeek`, doubao: `Doubao`, diff --git a/src/icons/extracted/sssaicoding.svg b/src/icons/extracted/sssaicoding.svg new file mode 100644 index 000000000..4a9478c27 --- /dev/null +++ b/src/icons/extracted/sssaicoding.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1001 1101 + + + + 0110 1011 + + + + 10 110 + + + + + + + + + + + + + + + + + + + + + S + + + SSSAiCode + From 73ca8340bbb9968dadd10ae1069733e48321d35d Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 25 Feb 2026 22:21:15 +0800 Subject: [PATCH 03/24] feat: add SSAI Code partner provider presets, i18n, icon, and fix models Add SSAI Code as a partner provider across all five apps with endpoint, API key URL, and partner promotion config. Rename brand from SSSAiCode to SSAI Code. Rename sssaicoding.svg to sssaicode.svg and register icon. Add trilingual promotion text for $10 bonus credit. Add missing models arrays in OpenClaw presets for CrazyRouter and SSAI Code. --- src/config/claudeProviderPresets.ts | 21 +++++++ src/config/codexProviderPresets.ts | 21 +++++++ src/config/geminiProviderPresets.ts | 24 ++++++++ src/config/openclawProviderPresets.ts | 60 +++++++++++++++++++ src/config/opencodeProviderPresets.ts | 29 +++++++++ src/i18n/locales/en.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/zh.json | 3 +- src/icons/extracted/index.ts | 1 + .../{sssaicoding.svg => sssaicode.svg} | 0 10 files changed, 162 insertions(+), 3 deletions(-) rename src/icons/extracted/{sssaicoding.svg => sssaicode.svg} (100%) diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index b0d50d8c9..8d132dad0 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -514,6 +514,27 @@ export const providerPresets: ProviderPreset[] = [ icon: "crazyrouter", iconColor: "#000000", }, + { + name: "SSSAiCode", + websiteUrl: "https://www.sssaicode.com", + apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://node-hk.sssaicode.com/api", + ANTHROPIC_AUTH_TOKEN: "", + }, + }, + endpointCandidates: [ + "https://node-hk.sssaicode.com/api", + "https://claude2.sssaicode.com/api", + "https://anti.sssaicode.com/api", + ], + category: "third_party", + isPartner: true, // 合作伙伴 + partnerPromotionKey: "sssaicode", // 促销信息 i18n key + icon: "sssaicode", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index 1354f05af..e35fbaf0e 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -263,6 +263,27 @@ requires_openai_auth = true`, icon: "crazyrouter", iconColor: "#000000", }, + { + name: "SSSAiCode", + websiteUrl: "https://www.sssaicode.com", + apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM", + auth: generateThirdPartyAuth(""), + config: generateThirdPartyConfig( + "sssaicode", + "https://node-hk.sssaicode.com/api", + "gpt-5.3-codex", + ), + endpointCandidates: [ + "https://node-hk.sssaicode.com/api/v1", + "https://claude2.sssaicode.com/api/v1", + "https://anti.sssaicode.com/api/v1", + ], + category: "third_party", + isPartner: true, // 合作伙伴 + partnerPromotionKey: "sssaicode", // 促销信息 i18n key + icon: "sssaicode", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/geminiProviderPresets.ts b/src/config/geminiProviderPresets.ts index b969ca1c5..61fbef290 100644 --- a/src/config/geminiProviderPresets.ts +++ b/src/config/geminiProviderPresets.ts @@ -183,6 +183,30 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [ icon: "crazyrouter", iconColor: "#000000", }, + { + name: "SSSAiCode", + websiteUrl: "https://www.sssaicode.com", + apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM", + settingsConfig: { + env: { + GOOGLE_GEMINI_BASE_URL: "https://node-hk.sssaicode.com/api", + GEMINI_MODEL: "gemini-3-pro", + }, + }, + baseURL: "https://node-hk.sssaicode.com/api", + model: "gemini-3-pro", + description: "SSSAiCode", + category: "third_party", + isPartner: true, + partnerPromotionKey: "sssaicode", + endpointCandidates: [ + "https://node-hk.sssaicode.com/api", + "https://claude2.sssaicode.com/api", + "https://anti.sssaicode.com/api", + ], + icon: "sssaicode", + iconColor: "#000000", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index ffe84d4c1..4974bf0ca 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -1108,6 +1108,20 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ baseUrl: "https://crazyrouter.com", apiKey: "", api: "anthropic-messages", + models: [ + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + contextWindow: 200000, + cost: { input: 5, output: 25 }, + }, + ], }, category: "third_party", isPartner: true, @@ -1132,6 +1146,52 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, }, }, + { + name: "SSSAiCode", + websiteUrl: "https://www.sssaicode.com", + apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM", + settingsConfig: { + baseUrl: "https://node-hk.sssaicode.com/api", + apiKey: "", + api: "anthropic-messages", + models: [ + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + contextWindow: 200000, + cost: { input: 5, output: 25 }, + }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "sssaicode", + icon: "sssaicode", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { + primary: "sssaicode/claude-sonnet-4-5-20250929", + fallbacks: ["sssaicode/claude-opus-4-6"], + }, + modelCatalog: { + "sssaicode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "sssaicode/claude-opus-4-6": { alias: "Opus" }, + }, + }, + }, // ========== Cloud Providers ========== { name: "AWS Bedrock", diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index fd17c72a0..ed03bec46 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -1191,6 +1191,35 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, }, + { + name: "SSSAiCode", + websiteUrl: "https://www.sssaicode.com", + apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM", + settingsConfig: { + npm: "@ai-sdk/anthropic", + name: "SSSAiCode", + options: { + baseURL: "https://node-hk.sssaicode.com/api", + apiKey: "", + }, + models: { + "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, + }, + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "sssaicode", + icon: "sssaicode", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, { name: "AWS Bedrock", websiteUrl: "https://aws.amazon.com/bedrock/", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index cffc59bc4..a1da78421 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -627,7 +627,8 @@ "rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!", "aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!", "aicoding": "AI Coding offers an exclusive discount for CC Switch users — 10% off your first top-up!", - "crazyrouter": "CrazyRouter offers an exclusive bonus for CC Switch users — 30% extra credit on your first top-up!" + "crazyrouter": "CrazyRouter offers an exclusive bonus for CC Switch users — 30% extra credit on your first top-up!", + "sssaicode": "SSAI Code offers an exclusive bonus for CC Switch users — $10 extra credit on every top-up!" }, "parameterConfig": "Parameter Config - {{name}} *", "mainModel": "Main Model (optional)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 194645c5e..f0db90efd 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -627,7 +627,8 @@ "rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!", "aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!", "aicoding": "AI Coding は CC Switch ユーザー向けに特別割引を提供しています。初回チャージが 10% オフ!", - "crazyrouter": "CrazyRouter は CC Switch ユーザー向けに特別ボーナスを提供しています。初回チャージで 30% の追加クレジットがもらえます!" + "crazyrouter": "CrazyRouter は CC Switch ユーザー向けに特別ボーナスを提供しています。初回チャージで 30% の追加クレジットがもらえます!", + "sssaicode": "SSAI Code は CC Switch ユーザー向けに特別ボーナスを提供しています。チャージごとに $10 の追加クレジットがもらえます!" }, "parameterConfig": "パラメーター設定 - {{name}} *", "mainModel": "メインモデル(任意)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index a5d6e79bd..fe5360dd8 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -627,7 +627,8 @@ "rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!", "aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!", "aicoding": "AI Coding 为 CC Switch 的用户提供了特殊优惠,首次充值可以享受 9 折优惠!", - "crazyrouter": "CrazyRouter 为 CC Switch 的用户提供了特殊优惠,首次充值赠予 30% 额外额度!" + "crazyrouter": "CrazyRouter 为 CC Switch 的用户提供了特殊优惠,首次充值赠予 30% 额外额度!", + "sssaicode": "SSAI Code 为 CC Switch 的用户提供了特殊优惠,每次充值赠予额外 $10 额度!" }, "parameterConfig": "参数配置 - {{name}} *", "mainModel": "主模型 (可选)", diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index 9d5284b95..4f32ce5c3 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -60,6 +60,7 @@ export const icons: Record = { aihubmix: `AiHubMix`, opencode: `OpenCode`, siliconflow: `SiliconCloud`, + sssaicode: `SSAI Code1001 11010110 101110 110SSSSAiCode`, catcoder: `KwaiKAT`, mcp: `ModelContextProtocol`, nvidia: `Nvidia`, diff --git a/src/icons/extracted/sssaicoding.svg b/src/icons/extracted/sssaicode.svg similarity index 100% rename from src/icons/extracted/sssaicoding.svg rename to src/icons/extracted/sssaicode.svg From 47435bd647adb89e0b78ca27d3452a46f9e84a88 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 25 Feb 2026 23:25:41 +0800 Subject: [PATCH 04/24] chore: update Claude model references from 4.5 to 4.6 in provider presets Update Sonnet and Opus model IDs/names to 4.6 across Claude, OpenClaw, and OpenCode provider preset configurations. OPENCODE_PRESET_MODEL_VARIANTS (SDK model catalog) is intentionally left unchanged. --- src/config/claudeProviderPresets.ts | 6 +- src/config/openclawProviderPresets.ts | 90 +++++++++++++-------------- src/config/opencodeProviderPresets.ts | 20 +++--- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 8d132dad0..1211fcb16 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -543,10 +543,10 @@ export const providerPresets: ProviderPreset[] = [ env: { ANTHROPIC_BASE_URL: "https://openrouter.ai/api", ANTHROPIC_AUTH_TOKEN: "", - ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.5", + ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.6", ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/claude-haiku-4.5", - ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.5", - ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.5", + ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6", + ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.6", }, }, category: "aggregator", diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 4974bf0ca..c8608879d 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -562,8 +562,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -587,11 +587,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aihubmix/claude-sonnet-4-5-20250929", + primary: "aihubmix/claude-sonnet-4-6", fallbacks: ["aihubmix/claude-opus-4-6"], }, modelCatalog: { - "aihubmix/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aihubmix/claude-sonnet-4-6": { alias: "Sonnet" }, "aihubmix/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -606,8 +606,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -631,11 +631,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "dmxapi/claude-sonnet-4-5-20250929", + primary: "dmxapi/claude-sonnet-4-6", fallbacks: ["dmxapi/claude-opus-4-6"], }, modelCatalog: { - "dmxapi/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "dmxapi/claude-sonnet-4-6": { alias: "Sonnet" }, "dmxapi/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -650,8 +650,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "openai-completions", models: [ { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "anthropic/claude-sonnet-4.6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -675,11 +675,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "openrouter/anthropic/claude-sonnet-4.5", + primary: "openrouter/anthropic/claude-sonnet-4.6", fallbacks: ["openrouter/anthropic/claude-opus-4.6"], }, modelCatalog: { - "openrouter/anthropic/claude-sonnet-4.5": { alias: "Sonnet" }, + "openrouter/anthropic/claude-sonnet-4.6": { alias: "Sonnet" }, "openrouter/anthropic/claude-opus-4.6": { alias: "Opus" }, }, }, @@ -834,8 +834,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -860,11 +860,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "packycode/claude-sonnet-4-5-20250929", + primary: "packycode/claude-sonnet-4-6", fallbacks: ["packycode/claude-opus-4-6"], }, modelCatalog: { - "packycode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "packycode/claude-sonnet-4-6": { alias: "Sonnet" }, "packycode/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -879,8 +879,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -906,11 +906,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "cubence/claude-sonnet-4-5-20250929", + primary: "cubence/claude-sonnet-4-6", fallbacks: ["cubence/claude-opus-4-6"], }, modelCatalog: { - "cubence/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "cubence/claude-sonnet-4-6": { alias: "Sonnet" }, "cubence/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -925,8 +925,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -952,11 +952,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aigocode/claude-sonnet-4-5-20250929", + primary: "aigocode/claude-sonnet-4-6", fallbacks: ["aigocode/claude-opus-4-6"], }, modelCatalog: { - "aigocode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aigocode/claude-sonnet-4-6": { alias: "Sonnet" }, "aigocode/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -971,8 +971,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -998,11 +998,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "rightcode/claude-sonnet-4-5-20250929", + primary: "rightcode/claude-sonnet-4-6", fallbacks: ["rightcode/claude-opus-4-6"], }, modelCatalog: { - "rightcode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "rightcode/claude-sonnet-4-6": { alias: "Sonnet" }, "rightcode/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -1017,8 +1017,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -1044,11 +1044,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aicodemirror/claude-sonnet-4-5-20250929", + primary: "aicodemirror/claude-sonnet-4-6", fallbacks: ["aicodemirror/claude-opus-4-6"], }, modelCatalog: { - "aicodemirror/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aicodemirror/claude-sonnet-4-6": { alias: "Sonnet" }, "aicodemirror/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -1064,8 +1064,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -1091,11 +1091,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aicoding/claude-sonnet-4-5-20250929", + primary: "aicoding/claude-sonnet-4-6", fallbacks: ["aicoding/claude-opus-4-6"], }, modelCatalog: { - "aicoding/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aicoding/claude-sonnet-4-6": { alias: "Sonnet" }, "aicoding/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -1105,13 +1105,13 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://www.crazyrouter.com", apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch", settingsConfig: { - baseUrl: "https://crazyrouter.com", + baseUrl: "https://crazyrouter.com/v1", apiKey: "", api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -1137,11 +1137,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "crazyrouter/claude-sonnet-4-5-20250929", + primary: "crazyrouter/claude-sonnet-4-6", fallbacks: ["crazyrouter/claude-opus-4-6"], }, modelCatalog: { - "crazyrouter/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "crazyrouter/claude-sonnet-4-6": { alias: "Sonnet" }, "crazyrouter/claude-opus-4-6": { alias: "Opus" }, }, }, @@ -1156,8 +1156,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ api: "anthropic-messages", models: [ { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", contextWindow: 200000, cost: { input: 3, output: 15 }, }, @@ -1183,11 +1183,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "sssaicode/claude-sonnet-4-5-20250929", + primary: "sssaicode/claude-sonnet-4-6", fallbacks: ["sssaicode/claude-opus-4-6"], }, modelCatalog: { - "sssaicode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "sssaicode/claude-sonnet-4-6": { alias: "Sonnet" }, "sssaicode/claude-opus-4-6": { alias: "Opus" }, }, }, diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index ed03bec46..2caff2528 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -890,7 +890,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -917,7 +917,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -944,7 +944,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "anthropic/claude-sonnet-4.5": { name: "Claude Sonnet 4.5" }, + "anthropic/claude-sonnet-4.6": { name: "Claude Sonnet 4.6" }, "anthropic/claude-opus-4.6": { name: "Claude Opus 4.6" }, }, }, @@ -998,7 +998,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -1026,7 +1026,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -1055,7 +1055,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -1116,7 +1116,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4.5": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4.6": { name: "Claude Sonnet 4.6" }, "claude-opus-4.6": { name: "Claude Opus 4.6" }, }, }, @@ -1145,7 +1145,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -1174,7 +1174,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, @@ -1203,7 +1203,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ apiKey: "", }, models: { - "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, + "claude-sonnet-4-6": { name: "Claude Sonnet 4.6" }, "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, From 87604b1809038831c7674d6c4d212c3ece95e506 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 09:32:51 +0800 Subject: [PATCH 05/24] fix: hide ClaudeQuickToggles when creating a new provider The quick toggles (hide AI attribution, extended thinking, teammates mode) patch the live config of the currently active provider, which is incorrect during provider creation. Only show them in edit mode. --- .../providers/forms/ProviderForm.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 2f7d33cae..6b2eb8e52 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -1496,22 +1496,24 @@ export function ProviderForm({ - { - try { - const cfg = JSON.parse( - form.getValues("settingsConfig") || "{}", - ); - jsonMergePatch(cfg, patch); - form.setValue( - "settingsConfig", - JSON.stringify(cfg, null, 2), - ); - } catch { - // invalid JSON in editor — skip mirror - } - }} - /> + {isEditMode && ( + { + try { + const cfg = JSON.parse( + form.getValues("settingsConfig") || "{}", + ); + jsonMergePatch(cfg, patch); + form.setValue( + "settingsConfig", + JSON.stringify(cfg, null, 2), + ); + } catch { + // invalid JSON in editor — skip mirror + } + }} + /> + )} form.setValue("settingsConfig", value)} From 24fa8a18efc4c633757e9807e1a741aa5b7e4f6d Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 09:55:09 +0800 Subject: [PATCH 06/24] fix: show context-aware JSON editor hint and hide quick toggles for non-active providers The hint text and ClaudeQuickToggles were misleading when editing non-current providers or creating new ones, since the editor only contains a config snippet rather than the full live settings.json. --- src/components/providers/EditProviderDialog.tsx | 1 + src/components/providers/forms/ProviderForm.tsx | 6 ++++-- src/i18n/locales/en.json | 1 + src/i18n/locales/ja.json | 1 + src/i18n/locales/zh.json | 1 + 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx index 672848134..bf7853235 100644 --- a/src/components/providers/EditProviderDialog.tsx +++ b/src/components/providers/EditProviderDialog.tsx @@ -192,6 +192,7 @@ export function EditProviderDialog({ onCancel={() => onOpenChange(false)} initialData={initialData} showButtons={false} + isCurrent={liveSettings !== null} /> ); diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 6b2eb8e52..bfc66ca0c 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -118,6 +118,7 @@ interface ProviderFormProps { iconColor?: string; }; showButtons?: boolean; + isCurrent?: boolean; } export function ProviderForm({ @@ -130,6 +131,7 @@ export function ProviderForm({ onManageUniversalProviders, initialData, showButtons = true, + isCurrent = false, }: ProviderFormProps) { const { t } = useTranslation(); const isEditMode = Boolean(initialData); @@ -1496,7 +1498,7 @@ export function ProviderForm({ - {isEditMode && ( + {isEditMode && isCurrent && ( { try { @@ -1527,7 +1529,7 @@ export function ProviderForm({ language="json" />

- {t("claudeConfig.fullSettingsHint")} + {t(isCurrent ? "claudeConfig.fullSettingsHint" : "claudeConfig.fragmentSettingsHint")}

{settingsConfigErrorField} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a1da78421..8ae71f854 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -52,6 +52,7 @@ "claudeConfig": { "configLabel": "Claude Code settings.json (JSON) *", "fullSettingsHint": "Full Claude Code settings.json content", + "fragmentSettingsHint": "Config snippet for this provider; will be written to settings.json when activated", "hideAttribution": "Hide AI Attribution", "alwaysThinking": "Extended Thinking", "enableTeammates": "Teammates Mode" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index f0db90efd..3ce28a6f4 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -52,6 +52,7 @@ "claudeConfig": { "configLabel": "Claude Code settings.json (JSON) *", "fullSettingsHint": "Claude Code の settings.json 全文", + "fragmentSettingsHint": "このプロバイダーの設定スニペット。有効化時に settings.json に書き込まれます", "hideAttribution": "AI署名を非表示", "alwaysThinking": "拡張思考", "enableTeammates": "Teammates モード" diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index fe5360dd8..2a2bfe007 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -52,6 +52,7 @@ "claudeConfig": { "configLabel": "Claude Code 配置 (JSON) *", "fullSettingsHint": "完整的 Claude Code settings.json 配置内容", + "fragmentSettingsHint": "此供应商的配置片段,激活后将写入 settings.json", "hideAttribution": "隐藏 AI 署名", "alwaysThinking": "扩展思考", "enableTeammates": "Teammates 模式" From 434392a669f7b76dc140d48c588dad7afd2732ca Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 11:16:04 +0800 Subject: [PATCH 07/24] perf: optimize session panel loading with parallel scan and head-tail JSONL reading - Parallelize 5 provider scans using std::thread::scope - Add read_head_tail_lines() to read only first 10 + last 30 lines from JSONL files, skipping potentially large middle sections - Cache Codex UUID regex with LazyLock to avoid repeated compilation - Skip expensive I/O in OpenCode when session title is already available --- src-tauri/src/session_manager/mod.rs | 25 ++++-- .../src/session_manager/providers/claude.rs | 63 ++++++++------- .../src/session_manager/providers/codex.rs | 80 ++++++++++--------- .../src/session_manager/providers/openclaw.rs | 57 +++++++------ .../src/session_manager/providers/opencode.rs | 9 ++- .../src/session_manager/providers/utils.rs | 48 +++++++++++ 6 files changed, 178 insertions(+), 104 deletions(-) diff --git a/src-tauri/src/session_manager/mod.rs b/src-tauri/src/session_manager/mod.rs index 8d833636f..f7ab61e98 100644 --- a/src-tauri/src/session_manager/mod.rs +++ b/src-tauri/src/session_manager/mod.rs @@ -37,12 +37,27 @@ pub struct SessionMessage { } pub fn scan_sessions() -> Vec { + let (r1, r2, r3, r4, r5) = std::thread::scope(|s| { + let h1 = s.spawn(codex::scan_sessions); + let h2 = s.spawn(claude::scan_sessions); + let h3 = s.spawn(opencode::scan_sessions); + let h4 = s.spawn(openclaw::scan_sessions); + let h5 = s.spawn(gemini::scan_sessions); + ( + h1.join().unwrap_or_default(), + h2.join().unwrap_or_default(), + h3.join().unwrap_or_default(), + h4.join().unwrap_or_default(), + h5.join().unwrap_or_default(), + ) + }); + let mut sessions = Vec::new(); - sessions.extend(codex::scan_sessions()); - sessions.extend(claude::scan_sessions()); - sessions.extend(opencode::scan_sessions()); - sessions.extend(openclaw::scan_sessions()); - sessions.extend(gemini::scan_sessions()); + sessions.extend(r1); + sessions.extend(r2); + sessions.extend(r3); + sessions.extend(r4); + sessions.extend(r5); sessions.sort_by(|a, b| { let a_ts = a.last_active_at.or(a.created_at).unwrap_or(0); diff --git a/src-tauri/src/session_manager/providers/claude.rs b/src-tauri/src/session_manager/providers/claude.rs index b953c015d..6f1d8f20a 100644 --- a/src-tauri/src/session_manager/providers/claude.rs +++ b/src-tauri/src/session_manager/providers/claude.rs @@ -7,7 +7,9 @@ use serde_json::Value; use crate::config::get_claude_config_dir; use crate::session_manager::{SessionMessage, SessionMeta}; -use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary}; +use super::utils::{ + extract_text, parse_timestamp_to_ms, path_basename, read_head_tail_lines, truncate_summary, +}; const PROVIDER_ID: &str = "claude"; @@ -73,60 +75,61 @@ fn parse_session(path: &Path) -> Option { return None; } - let file = File::open(path).ok()?; - let reader = BufReader::new(file); + let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?; let mut session_id: Option = None; let mut project_dir: Option = None; let mut created_at: Option = None; - let mut last_active_at: Option = None; - let mut summary: Option = None; - for line in reader.lines() { - let line = match line { - Ok(value) => value, - Err(_) => continue, - }; - let value: Value = match serde_json::from_str(&line) { + // Extract metadata from head lines + for line in &head { + let value: Value = match serde_json::from_str(line) { Ok(parsed) => parsed, Err(_) => continue, }; - if session_id.is_none() { session_id = value .get("sessionId") .and_then(Value::as_str) .map(|s| s.to_string()); } - if project_dir.is_none() { project_dir = value .get("cwd") .and_then(Value::as_str) .map(|s| s.to_string()); } - - if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { - if created_at.is_none() { - created_at = Some(ts); - } - last_active_at = Some(ts); + if created_at.is_none() { + created_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } + } - if value.get("isMeta").and_then(Value::as_bool) == Some(true) { - continue; - } + // Extract last_active_at and summary from tail lines (reverse order) + let mut last_active_at: Option = None; + let mut summary: Option = None; - let message = match value.get("message") { - Some(message) => message, - None => continue, + for line in tail.iter().rev() { + let value: Value = match serde_json::from_str(line) { + Ok(parsed) => parsed, + Err(_) => continue, }; - - let text = message.get("content").map(extract_text).unwrap_or_default(); - if text.trim().is_empty() { - continue; + if last_active_at.is_none() { + last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms); + } + if summary.is_none() { + if value.get("isMeta").and_then(Value::as_bool) == Some(true) { + continue; + } + if let Some(message) = value.get("message") { + let text = message.get("content").map(extract_text).unwrap_or_default(); + if !text.trim().is_empty() { + summary = Some(text); + } + } + } + if last_active_at.is_some() && summary.is_some() { + break; } - summary = Some(text); } let session_id = session_id.or_else(|| infer_session_id_from_filename(path)); diff --git a/src-tauri/src/session_manager/providers/codex.rs b/src-tauri/src/session_manager/providers/codex.rs index 6c2b62b58..56a4a3cf4 100644 --- a/src-tauri/src/session_manager/providers/codex.rs +++ b/src-tauri/src/session_manager/providers/codex.rs @@ -1,6 +1,7 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; +use std::sync::LazyLock; use regex::Regex; use serde_json::Value; @@ -8,10 +9,19 @@ use serde_json::Value; use crate::codex_config::get_codex_config_dir; use crate::session_manager::{SessionMessage, SessionMeta}; -use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary}; +use super::utils::{ + extract_text, parse_timestamp_to_ms, path_basename, read_head_tail_lines, truncate_summary, +}; const PROVIDER_ID: &str = "codex"; +static UUID_RE: LazyLock = LazyLock::new(|| { + Regex::new( + r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", + ) + .unwrap() +}); + pub fn scan_sessions() -> Vec { let root = get_codex_config_dir().join("sessions"); let mut files = Vec::new(); @@ -74,32 +84,21 @@ pub fn load_messages(path: &Path) -> Result, String> { } fn parse_session(path: &Path) -> Option { - let file = File::open(path).ok()?; - let reader = BufReader::new(file); + let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?; let mut session_id: Option = None; let mut project_dir: Option = None; let mut created_at: Option = None; - let mut last_active_at: Option = None; - let mut summary: Option = None; - for line in reader.lines() { - let line = match line { - Ok(value) => value, - Err(_) => continue, - }; - let value: Value = match serde_json::from_str(&line) { + // Extract metadata from head lines + for line in &head { + let value: Value = match serde_json::from_str(line) { Ok(parsed) => parsed, Err(_) => continue, }; - - if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { - if created_at.is_none() { - created_at = Some(ts); - } - last_active_at = Some(ts); + if created_at.is_none() { + created_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } - if value.get("type").and_then(Value::as_str) == Some("session_meta") { if let Some(payload) = value.get("payload") { if session_id.is_none() { @@ -118,27 +117,37 @@ fn parse_session(path: &Path) -> Option { created_at.get_or_insert(ts); } } - continue; } + } - if value.get("type").and_then(Value::as_str) != Some("response_item") { - continue; - } + // Extract last_active_at and summary from tail lines (reverse order) + let mut last_active_at: Option = None; + let mut summary: Option = None; - let payload = match value.get("payload") { - Some(payload) => payload, - None => continue, + for line in tail.iter().rev() { + let value: Value = match serde_json::from_str(line) { + Ok(parsed) => parsed, + Err(_) => continue, }; - - if payload.get("type").and_then(Value::as_str) != Some("message") { - continue; + if last_active_at.is_none() { + last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } - - let text = payload.get("content").map(extract_text).unwrap_or_default(); - if text.trim().is_empty() { - continue; + if summary.is_none() + && value.get("type").and_then(Value::as_str) == Some("response_item") + { + if let Some(payload) = value.get("payload") { + if payload.get("type").and_then(Value::as_str) == Some("message") { + let text = + payload.get("content").map(extract_text).unwrap_or_default(); + if !text.trim().is_empty() { + summary = Some(text); + } + } + } + } + if last_active_at.is_some() && summary.is_some() { + break; } - summary = Some(text); } let session_id = session_id.or_else(|| infer_session_id_from_filename(path)); @@ -166,10 +175,7 @@ fn parse_session(path: &Path) -> Option { fn infer_session_id_from_filename(path: &Path) -> Option { let file_name = path.file_name()?.to_string_lossy(); - let re = - Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") - .ok()?; - re.find(&file_name).map(|mat| mat.as_str().to_string()) + UUID_RE.find(&file_name).map(|mat| mat.as_str().to_string()) } fn collect_jsonl_files(root: &Path, files: &mut Vec) { diff --git a/src-tauri/src/session_manager/providers/openclaw.rs b/src-tauri/src/session_manager/providers/openclaw.rs index e8283bf0f..25b911514 100644 --- a/src-tauri/src/session_manager/providers/openclaw.rs +++ b/src-tauri/src/session_manager/providers/openclaw.rs @@ -7,7 +7,9 @@ use serde_json::Value; use crate::openclaw_config::get_openclaw_dir; use crate::session_manager::{SessionMessage, SessionMeta}; -use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary}; +use super::utils::{ + extract_text, parse_timestamp_to_ms, path_basename, read_head_tail_lines, truncate_summary, +}; const PROVIDER_ID: &str = "openclaw"; @@ -114,30 +116,22 @@ pub fn load_messages(path: &Path) -> Result, String> { } fn parse_session(path: &Path) -> Option { - let file = File::open(path).ok()?; - let reader = BufReader::new(file); + let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?; let mut session_id: Option = None; let mut cwd: Option = None; let mut created_at: Option = None; - let mut last_active_at: Option = None; let mut summary: Option = None; - for line in reader.lines() { - let line = match line { - Ok(value) => value, - Err(_) => continue, - }; - let value: Value = match serde_json::from_str(&line) { + // Extract metadata and first message summary from head lines + for line in &head { + let value: Value = match serde_json::from_str(line) { Ok(parsed) => parsed, Err(_) => continue, }; - if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { - if created_at.is_none() { - created_at = Some(ts); - } - last_active_at = Some(ts); + if created_at.is_none() { + created_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } let event_type = value.get("type").and_then(Value::as_str).unwrap_or(""); @@ -161,25 +155,28 @@ fn parse_session(path: &Path) -> Option { continue; } - if event_type != "message" { - continue; + // OpenClaw summary is the first message content + if event_type == "message" && summary.is_none() { + if let Some(message) = value.get("message") { + let text = message.get("content").map(extract_text).unwrap_or_default(); + if !text.trim().is_empty() { + summary = Some(text); + } + } } + } - // Extract first message content for summary - if summary.is_some() { - continue; - } - - let message = match value.get("message") { - Some(msg) => msg, - None => continue, + // Extract last_active_at from tail lines (reverse order) + let mut last_active_at: Option = None; + for line in tail.iter().rev() { + let value: Value = match serde_json::from_str(line) { + Ok(parsed) => parsed, + Err(_) => continue, }; - - let text = message.get("content").map(extract_text).unwrap_or_default(); - if text.trim().is_empty() { - continue; + if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { + last_active_at = Some(ts); + break; } - summary = Some(text); } // Fall back to filename as session ID diff --git a/src-tauri/src/session_manager/providers/opencode.rs b/src-tauri/src/session_manager/providers/opencode.rs index 4428bc57f..7cb5d115b 100644 --- a/src-tauri/src/session_manager/providers/opencode.rs +++ b/src-tauri/src/session_manager/providers/opencode.rs @@ -136,6 +136,7 @@ fn parse_session(storage: &Path, path: &Path) -> Option { .and_then(parse_timestamp_to_ms); // Derive title from directory basename if no explicit title + let has_title = title.is_some(); let display_title = title.or_else(|| { directory .as_deref() @@ -147,8 +148,12 @@ fn parse_session(storage: &Path, path: &Path) -> Option { let msg_dir = storage.join("message").join(&session_id); let source_path = msg_dir.to_string_lossy().to_string(); - // Get summary from first user message - let summary = get_first_user_summary(storage, &session_id); + // Skip expensive I/O if title already available from session JSON + let summary = if has_title { + display_title.clone() + } else { + get_first_user_summary(storage, &session_id) + }; Some(SessionMeta { provider_id: PROVIDER_ID.to_string(), diff --git a/src-tauri/src/session_manager/providers/utils.rs b/src-tauri/src/session_manager/providers/utils.rs index 76408ea35..9ac700b63 100644 --- a/src-tauri/src/session_manager/providers/utils.rs +++ b/src-tauri/src/session_manager/providers/utils.rs @@ -1,6 +1,54 @@ +use std::fs::File; +use std::io::{self, BufRead, BufReader, Seek, SeekFrom}; +use std::path::Path; + use chrono::{DateTime, FixedOffset}; use serde_json::Value; +/// Read the first `head_n` lines and last `tail_n` lines from a file. +/// For small files (< 16 KB), reads all lines once to avoid unnecessary seeking. +pub fn read_head_tail_lines( + path: &Path, + head_n: usize, + tail_n: usize, +) -> io::Result<(Vec, Vec)> { + let file = File::open(path)?; + let file_len = file.metadata()?.len(); + + // For small files, read all lines once and split + if file_len < 16_384 { + let reader = BufReader::new(file); + let all: Vec = reader.lines().map_while(Result::ok).collect(); + let head = all.iter().take(head_n).cloned().collect(); + let skip = all.len().saturating_sub(tail_n); + let tail = all.into_iter().skip(skip).collect(); + return Ok((head, tail)); + } + + // Read head lines from the beginning + let reader = BufReader::new(file); + let head: Vec = reader + .lines() + .take(head_n) + .map_while(Result::ok) + .collect(); + + // Seek to last ~16 KB for tail lines + let seek_pos = file_len.saturating_sub(16_384); + let mut file2 = File::open(path)?; + file2.seek(SeekFrom::Start(seek_pos))?; + let tail_reader = BufReader::new(file2); + let all_tail: Vec = tail_reader.lines().map_while(Result::ok).collect(); + + // Skip first partial line if we seeked into the middle of a line + let skip_first = if seek_pos > 0 { 1 } else { 0 }; + let usable: Vec = all_tail.into_iter().skip(skip_first).collect(); + let skip = usable.len().saturating_sub(tail_n); + let tail = usable.into_iter().skip(skip).collect(); + + Ok((head, tail)) +} + pub fn parse_timestamp_to_ms(value: &Value) -> Option { let raw = value.as_str()?; DateTime::parse_from_rfc3339(raw) From 2b308195102b5a380c27ee0ca7edc994c0f69445 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 15:11:13 +0800 Subject: [PATCH 08/24] =?UTF-8?q?chore:=20pre-release=20cleanup=20?= =?UTF-8?q?=E2=80=94=20remove=20debug=20logs,=20fix=20clippy=20warning,=20?= =?UTF-8?q?add=20missing=20ja=20translations,=20and=20format=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove 2 console.log statements from DeepLinkImportDialog - Fix clippy unnecessary_map_or: use is_some_and in live.rs - Add 17 missing Japanese i18n keys (skills, proxy, circuitBreaker, universalProvider) - Run prettier and cargo fmt to fix pre-existing formatting drift --- src-tauri/src/services/provider/live.rs | 2 +- .../src/session_manager/providers/codex.rs | 13 ++++------- .../src/session_manager/providers/utils.rs | 6 +---- src/components/DeepLinkImportDialog.tsx | 3 --- .../providers/forms/ProviderForm.tsx | 11 +++++---- src/config/claudeProviderPresets.ts | 14 ++++------- src/config/codexProviderPresets.ts | 10 +++----- src/config/geminiProviderPresets.ts | 8 ++----- src/config/openclawProviderPresets.ts | 1 - src/i18n/locales/ja.json | 23 +++++++++++++++++-- 10 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 882191ad7..0f815c963 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -357,7 +357,7 @@ fn json_merge_patch(target: &mut Value, patch: &Value) { let entry = target_obj.entry(key.clone()).or_insert(json!({})); json_merge_patch(entry, value); // Clean up empty container objects - if entry.as_object().map_or(false, |o| o.is_empty()) { + if entry.as_object().is_some_and(|o| o.is_empty()) { target_obj.remove(key); } } else { diff --git a/src-tauri/src/session_manager/providers/codex.rs b/src-tauri/src/session_manager/providers/codex.rs index 56a4a3cf4..0fd1e9656 100644 --- a/src-tauri/src/session_manager/providers/codex.rs +++ b/src-tauri/src/session_manager/providers/codex.rs @@ -16,10 +16,8 @@ use super::utils::{ const PROVIDER_ID: &str = "codex"; static UUID_RE: LazyLock = LazyLock::new(|| { - Regex::new( - r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", - ) - .unwrap() + Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") + .unwrap() }); pub fn scan_sessions() -> Vec { @@ -132,13 +130,10 @@ fn parse_session(path: &Path) -> Option { if last_active_at.is_none() { last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } - if summary.is_none() - && value.get("type").and_then(Value::as_str) == Some("response_item") - { + if summary.is_none() && value.get("type").and_then(Value::as_str) == Some("response_item") { if let Some(payload) = value.get("payload") { if payload.get("type").and_then(Value::as_str) == Some("message") { - let text = - payload.get("content").map(extract_text).unwrap_or_default(); + let text = payload.get("content").map(extract_text).unwrap_or_default(); if !text.trim().is_empty() { summary = Some(text); } diff --git a/src-tauri/src/session_manager/providers/utils.rs b/src-tauri/src/session_manager/providers/utils.rs index 9ac700b63..e22c5a5ee 100644 --- a/src-tauri/src/session_manager/providers/utils.rs +++ b/src-tauri/src/session_manager/providers/utils.rs @@ -27,11 +27,7 @@ pub fn read_head_tail_lines( // Read head lines from the beginning let reader = BufReader::new(file); - let head: Vec = reader - .lines() - .take(head_n) - .map_while(Result::ok) - .collect(); + let head: Vec = reader.lines().take(head_n).map_while(Result::ok).collect(); // Seek to last ~16 KB for tail lines let seek_pos = file_len.saturating_sub(16_384); diff --git a/src/components/DeepLinkImportDialog.tsx b/src/components/DeepLinkImportDialog.tsx index daf1d7327..05acafa89 100644 --- a/src/components/DeepLinkImportDialog.tsx +++ b/src/components/DeepLinkImportDialog.tsx @@ -53,15 +53,12 @@ export function DeepLinkImportDialog() { const unlistenImport = listen( "deeplink-import", async (event) => { - console.log("Deep link import event received:", event.payload); - // If config is present, merge it to get the complete configuration if (event.payload.config || event.payload.configUrl) { try { const mergedRequest = await deeplinkApi.mergeDeeplinkConfig( event.payload, ); - console.log("Config merged successfully:", mergedRequest); setRequest(mergedRequest); } catch (error) { console.error("Failed to merge config:", error); diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index bfc66ca0c..9d9c932ec 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -1243,10 +1243,7 @@ export function ProviderForm({ providerId={providerId} shouldShowApiKey={ hasApiKeyField(form.getValues("settingsConfig"), "claude") && - shouldShowApiKey( - form.getValues("settingsConfig"), - isEditMode, - ) + shouldShowApiKey(form.getValues("settingsConfig"), isEditMode) } apiKey={apiKey} onApiKeyChange={handleApiKeyChange} @@ -1529,7 +1526,11 @@ export function ProviderForm({ language="json" />

- {t(isCurrent ? "claudeConfig.fullSettingsHint" : "claudeConfig.fragmentSettingsHint")} + {t( + isCurrent + ? "claudeConfig.fullSettingsHint" + : "claudeConfig.fragmentSettingsHint", + )}

{settingsConfigErrorField} diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 1211fcb16..569b6085a 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -486,9 +486,7 @@ export const providerPresets: ProviderPreset[] = [ ANTHROPIC_AUTH_TOKEN: "", }, }, - endpointCandidates: [ - "https://api.aicoding.sh", - ], + endpointCandidates: ["https://api.aicoding.sh"], category: "third_party", isPartner: true, // 合作伙伴 partnerPromotionKey: "aicoding", // 促销信息 i18n key @@ -505,9 +503,7 @@ export const providerPresets: ProviderPreset[] = [ ANTHROPIC_AUTH_TOKEN: "", }, }, - endpointCandidates: [ - "https://crazyrouter.com", - ], + endpointCandidates: ["https://crazyrouter.com"], category: "third_party", isPartner: true, // 合作伙伴 partnerPromotionKey: "crazyrouter", // 促销信息 i18n key @@ -603,8 +599,7 @@ export const providerPresets: ProviderPreset[] = [ ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-6-v1", ANTHROPIC_DEFAULT_HAIKU_MODEL: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - ANTHROPIC_DEFAULT_SONNET_MODEL: - "global.anthropic.claude-sonnet-4-6", + ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-4-6", ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-6-v1", CLAUDE_CODE_USE_BEDROCK: "1", }, @@ -642,8 +637,7 @@ export const providerPresets: ProviderPreset[] = [ ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-6-v1", ANTHROPIC_DEFAULT_HAIKU_MODEL: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - ANTHROPIC_DEFAULT_SONNET_MODEL: - "global.anthropic.claude-sonnet-4-6", + ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-4-6", ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-6-v1", CLAUDE_CODE_USE_BEDROCK: "1", }, diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index e35fbaf0e..9bf3da7f2 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -237,9 +237,7 @@ requires_openai_auth = true`, "https://api.aicoding.sh", "gpt-5.3-codex", ), - endpointCandidates: [ - "https://api.aicoding.sh", - ], + endpointCandidates: ["https://api.aicoding.sh"], isPartner: true, partnerPromotionKey: "aicoding", icon: "aicoding", @@ -253,11 +251,9 @@ requires_openai_auth = true`, config: generateThirdPartyConfig( "crazyrouter", "https://crazyrouter.com/v1", - "gpt-5.3-codex", + "gpt-5.3-codex", ), - endpointCandidates: [ - "https://crazyrouter.com/v1", - ], + endpointCandidates: ["https://crazyrouter.com/v1"], isPartner: true, partnerPromotionKey: "crazyrouter", icon: "crazyrouter", diff --git a/src/config/geminiProviderPresets.ts b/src/config/geminiProviderPresets.ts index 61fbef290..ac88fc953 100644 --- a/src/config/geminiProviderPresets.ts +++ b/src/config/geminiProviderPresets.ts @@ -155,9 +155,7 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [ category: "third_party", isPartner: true, partnerPromotionKey: "aicoding", - endpointCandidates: [ - "https://api.aicoding.sh", - ], + endpointCandidates: ["https://api.aicoding.sh"], icon: "aicoding", iconColor: "#000000", }, @@ -177,9 +175,7 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [ category: "third_party", isPartner: true, partnerPromotionKey: "crazyrouter", - endpointCandidates: [ - "https://crazyrouter.com", - ], + endpointCandidates: ["https://crazyrouter.com"], icon: "crazyrouter", iconColor: "#000000", }, diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index c8608879d..0c7c535ce 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -1052,7 +1052,6 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ "aicodemirror/claude-opus-4-6": { alias: "Opus" }, }, }, - }, { name: "AICoding", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3ce28a6f4..c0b97460c 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1350,6 +1350,7 @@ "http429": "リクエストが多すぎます。時間をおいて再試行してください", "parseMetadataFailed": "スキルメタデータの解析に失敗しました", "getHomeDirFailed": "ユーザーのホームディレクトリを取得できません", + "noSkillsInZip": "ZIP ファイルにスキルが見つかりません(SKILL.md ファイルが必要です)", "networkError": "ネットワークエラー", "fsError": "ファイルシステムエラー", "unknownError": "不明なエラー", @@ -1360,7 +1361,8 @@ "checkRepoUrl": "リポジトリ URL とブランチ名を確認してください", "checkDiskSpace": "ディスク容量を確認してください", "checkPermission": "ディレクトリの権限を確認してください", - "uninstallFirst": "同名のスキルを先にアンインストールしてください" + "uninstallFirst": "同名のスキルを先にアンインストールしてください", + "checkZipContent": "ZIP ファイルに有効なスキルディレクトリ(SKILL.md を含む)が含まれていることを確認してください" } }, "repo": { @@ -1630,7 +1632,12 @@ "toast": { "saved": "プロキシ設定を保存しました", "saveFailed": "保存に失敗しました: {{error}}" - } + }, + "invalidPort": "無効なポートです。1024〜65535 の数値を入力してください", + "invalidAddress": "無効なアドレスです。有効な IP アドレス(例: 127.0.0.1)または localhost を入力してください", + "configSaved": "プロキシ設定を保存しました", + "configSaveFailed": "設定の保存に失敗しました", + "restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です" }, "switchFailed": "切り替えに失敗しました: {{error}}", "failover": { @@ -1660,6 +1667,7 @@ "info": "フェイルオーバーキューに複数のプロバイダーが設定されている場合、リクエストが失敗すると優先度順に試行します。プロバイダーが連続失敗のしきい値に達すると、サーキットブレーカーが開き、一時的にスキップされます。", "configSaved": "自動フェイルオーバー設定を保存しました", "configSaveFailed": "保存に失敗しました", + "validationFailed": "以下のフィールドが有効範囲外です: {{fields}}", "retrySettings": "リトライとタイムアウト設定", "failureThreshold": "失敗しきい値", "failureThresholdHint": "この回数連続で失敗するとサーキットブレーカーが開きます(推奨: 3-10)", @@ -1712,6 +1720,16 @@ "streamingIdle": "ストリーミングアイドルタイムアウト", "nonStreaming": "非ストリーミングタイムアウト" }, + "circuitBreaker": { + "failureThreshold": "失敗しきい値", + "successThreshold": "成功しきい値", + "timeoutSeconds": "タイムアウト(秒)", + "errorRateThreshold": "エラー率しきい値", + "minRequests": "最小リクエスト数", + "validationFailed": "以下のフィールドが有効範囲外です: {{fields}}", + "configSaved": "サーキットブレーカー設定を保存しました", + "saveFailed": "保存に失敗しました" + }, "universalProvider": { "title": "統合プロバイダー", "description": "統合プロバイダーは Claude、Codex、Gemini の設定を同時に管理します。変更は有効なすべてのアプリに自動的に同期されます。", @@ -1736,6 +1754,7 @@ "syncError": "同期に失敗しました", "noAppsEnabled": "有効なアプリがありません", "added": "統合プロバイダーを追加しました", + "addedAndSynced": "統合プロバイダーを追加して同期しました", "updated": "統合プロバイダーを更新しました", "deleted": "統合プロバイダーを削除しました", "addSuccess": "統合プロバイダーを追加しました", From 90cb8db16b68f3e3269ed2199e576d0fab6ff486 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 16:02:18 +0800 Subject: [PATCH 09/24] fix: add toast feedback for OMO "Fill Recommended" button silent failures --- .../providers/forms/OmoFormFields.tsx | 48 ++++++++++++++++++- src/i18n/locales/en.json | 4 ++ src/i18n/locales/ja.json | 4 ++ src/i18n/locales/zh.json | 4 ++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/components/providers/forms/OmoFormFields.tsx b/src/components/providers/forms/OmoFormFields.tsx index d0ffa02d2..52cced9cb 100644 --- a/src/components/providers/forms/OmoFormFields.tsx +++ b/src/components/providers/forms/OmoFormFields.tsx @@ -722,14 +722,23 @@ export function OmoFormFields({ return; } + let filledCount = 0; + let alreadySetCount = 0; + let unmatchedCount = 0; + const updatedAgents = { ...agents }; for (const agentDef of builtinAgentDefs) { const recommendedValue = resolveRecommendedModel(agentDef.recommended); - if (recommendedValue && !updatedAgents[agentDef.key]?.model) { + if (!recommendedValue) { + unmatchedCount++; + } else if (updatedAgents[agentDef.key]?.model) { + alreadySetCount++; + } else { updatedAgents[agentDef.key] = { ...updatedAgents[agentDef.key], model: recommendedValue, }; + filledCount++; } } onAgentsChange(updatedAgents); @@ -738,15 +747,50 @@ export function OmoFormFields({ const updatedCategories = { ...categories }; for (const catDef of OMO_BUILTIN_CATEGORIES) { const recommendedValue = resolveRecommendedModel(catDef.recommended); - if (recommendedValue && !updatedCategories[catDef.key]?.model) { + if (!recommendedValue) { + unmatchedCount++; + } else if (updatedCategories[catDef.key]?.model) { + alreadySetCount++; + } else { updatedCategories[catDef.key] = { ...updatedCategories[catDef.key], model: recommendedValue, }; + filledCount++; } } onCategoriesChange(updatedCategories); } + + if (filledCount > 0 && unmatchedCount === 0) { + toast.success( + t("omo.fillRecommendedSuccess", { + defaultValue: "Filled {{count}} recommended models", + count: filledCount, + }), + ); + } else if (filledCount > 0 && unmatchedCount > 0) { + toast.success( + t("omo.fillRecommendedPartial", { + defaultValue: + "Filled {{filled}} recommended models, {{unmatched}} unmatched", + filled: filledCount, + unmatched: unmatchedCount, + }), + ); + } else if (alreadySetCount > 0 && unmatchedCount === 0) { + toast.info( + t("omo.fillRecommendedAllSet", { + defaultValue: "All slots already have models configured", + }), + ); + } else { + toast.warning( + t("omo.fillRecommendedNoMatch", { + defaultValue: "Recommended models not found in configured providers", + }), + ); + } }; const configuredAgentCount = Object.keys(agents).length; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8ae71f854..fa5c497a2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1840,6 +1840,10 @@ "customCategories": "Custom Categories", "modelConfiguration": "Model Configuration", "fillRecommended": "Fill Recommended", + "fillRecommendedSuccess": "Filled {{count}} recommended models", + "fillRecommendedPartial": "Filled {{filled}} recommended models, {{unmatched}} unmatched", + "fillRecommendedAllSet": "All slots already have models configured", + "fillRecommendedNoMatch": "Recommended models not found in configured providers", "configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params", "enabledModelsCount": "{{count}} configured models available", "source": "from:", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index c0b97460c..d0df8ba1e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1840,6 +1840,10 @@ "customCategories": "カスタムカテゴリ", "modelConfiguration": "モデル設定", "fillRecommended": "推奨を入力", + "fillRecommendedSuccess": "推奨モデル {{count}} 件を設定しました", + "fillRecommendedPartial": "推奨モデル {{filled}} 件を設定、{{unmatched}} 件は未一致", + "fillRecommendedAllSet": "すべてのスロットにモデルが設定済みです", + "fillRecommendedNoMatch": "推奨モデルが設定済みプロバイダーに見つかりませんでした", "configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開", "enabledModelsCount": "設定済みモデル {{count}} 件", "source": "出典:", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 2a2bfe007..8563c36fe 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1840,6 +1840,10 @@ "customCategories": "自定义分类", "modelConfiguration": "模型配置", "fillRecommended": "填充推荐", + "fillRecommendedSuccess": "已填充 {{count}} 个推荐模型", + "fillRecommendedPartial": "已填充 {{filled}} 个推荐模型,{{unmatched}} 个未匹配", + "fillRecommendedAllSet": "所有槽位已有模型配置", + "fillRecommendedNoMatch": "推荐模型在已配置的供应商中未找到匹配", "configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数", "enabledModelsCount": "可选已配置模型 {{count}} 个", "source": "来源:", From 3e2c8c12a5bc6aa70adb7a491533448f6e598064 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 17:39:08 +0800 Subject: [PATCH 10/24] fix: sync OMO agent/category recommended models with upstream sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regular agents & categories: align with oh-my-opencode model-requirements.ts fallback chains (oracle→gpt-5.2, librarian→gemini-3-flash, etc.) Slim agents: derive from Regular's design philosophy — match each agent to its functional counterpart's first-choice model instead of using the outdated Slim defaults. Also fix provider/model format to pure model IDs for suffix matching compatibility. --- src/types/omo.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/types/omo.ts b/src/types/omo.ts index fa5515112..10d00680c 100644 --- a/src/types/omo.ts +++ b/src/types/omo.ts @@ -79,7 +79,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Oracle", descKey: "omo.agentDesc.oracle", tooltipKey: "omo.agentTooltip.oracle", - recommended: "gpt-5.3", + recommended: "gpt-5.2", group: "sub", }, { @@ -87,7 +87,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Librarian", descKey: "omo.agentDesc.librarian", tooltipKey: "omo.agentTooltip.librarian", - recommended: "glm-4.7", + recommended: "gemini-3-flash", group: "sub", }, { @@ -103,7 +103,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Multimodal-Looker", descKey: "omo.agentDesc.multimodalLooker", tooltipKey: "omo.agentTooltip.multimodalLooker", - recommended: "gemini-3-flash", + recommended: "kimi-k2.5", group: "sub", }, { @@ -119,7 +119,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Momus", descKey: "omo.agentDesc.momus", tooltipKey: "omo.agentTooltip.momus", - recommended: "gpt-5.3", + recommended: "gpt-5.2", group: "sub", }, { @@ -144,7 +144,7 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [ display: "Ultrabrain", descKey: "omo.categoryDesc.ultrabrain", tooltipKey: "omo.categoryTooltip.ultrabrain", - recommended: "claude-opus-4-6", + recommended: "gpt-5.3-codex", }, { key: "deep", @@ -158,35 +158,35 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [ display: "Artistry", descKey: "omo.categoryDesc.artistry", tooltipKey: "omo.categoryTooltip.artistry", - recommended: "claude-opus-4-6", + recommended: "gemini-3-pro", }, { key: "quick", display: "Quick", descKey: "omo.categoryDesc.quick", tooltipKey: "omo.categoryTooltip.quick", - recommended: "gemini-3-flash", + recommended: "claude-haiku-4-5", }, { key: "unspecified-low", display: "Unspecified Low", descKey: "omo.categoryDesc.unspecifiedLow", tooltipKey: "omo.categoryTooltip.unspecifiedLow", - recommended: "gemini-3-flash", + recommended: "claude-sonnet-4-6", }, { key: "unspecified-high", display: "Unspecified High", descKey: "omo.categoryDesc.unspecifiedHigh", tooltipKey: "omo.categoryTooltip.unspecifiedHigh", - recommended: "gpt-5.3-codex", + recommended: "claude-opus-4-6", }, { key: "writing", display: "Writing", descKey: "omo.categoryDesc.writing", tooltipKey: "omo.categoryTooltip.writing", - recommended: "claude-opus-4-6", + recommended: "gemini-3-flash", }, ]; @@ -337,7 +337,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Orchestrator", descKey: "omo.slimAgentDesc.orchestrator", tooltipKey: "omo.slimAgentTooltip.orchestrator", - recommended: "kimi-for-coding/k2p5", + recommended: "claude-opus-4-6", group: "main", }, { @@ -345,7 +345,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Oracle", descKey: "omo.slimAgentDesc.oracle", tooltipKey: "omo.slimAgentTooltip.oracle", - recommended: "openai/gpt-5.2-codex", + recommended: "gpt-5.2", group: "sub", }, { @@ -353,7 +353,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Librarian", descKey: "omo.slimAgentDesc.librarian", tooltipKey: "omo.slimAgentTooltip.librarian", - recommended: "openai/gpt-5.1-codex-mini", + recommended: "gemini-3-flash", group: "sub", }, { @@ -361,7 +361,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Explorer", descKey: "omo.slimAgentDesc.explorer", tooltipKey: "omo.slimAgentTooltip.explorer", - recommended: "openai/gpt-5.1-codex-mini", + recommended: "grok-code-fast-1", group: "sub", }, { @@ -369,7 +369,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Designer", descKey: "omo.slimAgentDesc.designer", tooltipKey: "omo.slimAgentTooltip.designer", - recommended: "kimi-for-coding/k2p5", + recommended: "gemini-3-pro", group: "sub", }, { @@ -377,7 +377,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [ display: "Fixer", descKey: "omo.slimAgentDesc.fixer", tooltipKey: "omo.slimAgentTooltip.fixer", - recommended: "openai/gpt-5.1-codex-mini", + recommended: "gpt-5.3-codex", group: "sub", }, ]; From 55e21c3c193b7ab3f95c977cfbcf78c955b2210e Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 17:51:50 +0800 Subject: [PATCH 11/24] fix: invalidate OMO Slim query cache after provider mutations OMO Slim queries (["omo-slim", ...]) were not invalidated alongside OMO queries, causing stale UI state when switching/adding/deleting OMO Slim providers. --- src/lib/query/mutations.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index d59cef6de..fd6c99fb1 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -57,6 +57,12 @@ export const useAddProviderMutation = (appId: AppId) => { await queryClient.invalidateQueries({ queryKey: ["omo", "provider-count"], }); + await queryClient.invalidateQueries({ + queryKey: ["omo-slim", "current-provider-id"], + }); + await queryClient.invalidateQueries({ + queryKey: ["omo-slim", "provider-count"], + }); } try { @@ -139,6 +145,12 @@ export const useDeleteProviderMutation = (appId: AppId) => { await queryClient.invalidateQueries({ queryKey: ["omo", "provider-count"], }); + await queryClient.invalidateQueries({ + queryKey: ["omo-slim", "current-provider-id"], + }); + await queryClient.invalidateQueries({ + queryKey: ["omo-slim", "provider-count"], + }); } try { @@ -190,6 +202,9 @@ export const useSwitchProviderMutation = (appId: AppId) => { await queryClient.invalidateQueries({ queryKey: ["omo", "current-provider-id"], }); + await queryClient.invalidateQueries({ + queryKey: ["omo-slim", "current-provider-id"], + }); } if (appId === "openclaw") { await queryClient.invalidateQueries({ From 082cb0327d1ba972ce1c3fe699ccab4dfd2b1044 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 18:08:46 +0800 Subject: [PATCH 12/24] =?UTF-8?q?fix:=20enforce=20OMO=20=E2=86=94=20OMO=20?= =?UTF-8?q?Slim=20cross-category=20mutual=20exclusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When activating an OMO provider, deactivate all OMO Slim providers in the same transaction and delete the Slim config file, and vice versa. This prevents both plugin variants from being active simultaneously. --- src-tauri/src/database/dao/providers.rs | 13 +++++++++++++ src-tauri/src/services/provider/mod.rs | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/src-tauri/src/database/dao/providers.rs b/src-tauri/src/database/dao/providers.rs index 4f4259e95..bf0a44361 100644 --- a/src-tauri/src/database/dao/providers.rs +++ b/src-tauri/src/database/dao/providers.rs @@ -375,6 +375,19 @@ impl Database { params![app_type, category], ) .map_err(|e| AppError::Database(e.to_string()))?; + // OMO ↔ OMO Slim mutually exclusive: deactivate the opposite category + let opposite = match category { + "omo" => Some("omo-slim"), + "omo-slim" => Some("omo"), + _ => None, + }; + if let Some(opp) = opposite { + tx.execute( + "UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = ?2", + params![app_type, opp], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + } let updated = tx .execute( "UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3", diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index fdd9daa51..bde2dd925 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -513,6 +513,8 @@ impl ProviderService { state, &crate::services::omo::STANDARD, )?; + // OMO ↔ OMO Slim mutually exclusive: remove Slim config + let _ = crate::services::OmoService::delete_config_file(&crate::services::omo::SLIM); return Ok(SwitchResult::default()); } @@ -522,6 +524,10 @@ impl ProviderService { .db .set_omo_provider_current(app_type.as_str(), id, "omo-slim")?; crate::services::OmoService::write_config_to_file(state, &crate::services::omo::SLIM)?; + // OMO ↔ OMO Slim mutually exclusive: remove Standard config + let _ = crate::services::OmoService::delete_config_file( + &crate::services::omo::STANDARD, + ); return Ok(SwitchResult::default()); } From 3e9815f5d27927e4b34db0a348ee0e04cd00a0b9 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 19:15:54 +0800 Subject: [PATCH 13/24] chore: add .worktrees/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 411f24951..fc211bf72 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ nul flatpak/cc-switch.deb flatpak-build/ flatpak-repo/ +.worktrees/ From e7766d4d220c10500874bc1df408c1c6e12ea589 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 26 Feb 2026 19:31:43 +0800 Subject: [PATCH 14/24] refactor: remove OMO common config two-layer merge system Each OMO provider now stores its complete configuration directly in settings_config.otherFields instead of relying on a shared OmoGlobalConfig merged at write time. This simplifies the data flow from a 4-tuple (agents, categories, otherFields, useCommonConfig) to a 3-tuple and eliminates an entire DB table, two Tauri commands, and ~1700 lines of merge/sync code across frontend and backend. Backend: - Delete database/dao/omo.rs (OmoGlobalConfig struct + get/save methods) - Remove get/set_config_snippet from settings DAO - Remove get/set_common_config_snippet Tauri commands - Replace merge_config() with build_config() in services/omo.rs - Simplify OmoVariant (remove config_key, known_keys) - Simplify import_from_local and build_local_file_data - Rewrite all OMO service tests Frontend: - Delete OmoCommonConfigEditor.tsx and OmoGlobalConfigFields.tsx - Delete src/lib/api/config.ts - Remove OmoGlobalConfig type and merge preview functions - Remove useGlobalConfig/useSaveGlobalConfig query hooks - Simplify useOmoDraftState (remove all common config state) - Replace OmoCommonConfigEditor with read-only JsonEditor preview - Clean i18n keys (zh/en/ja) --- src-tauri/src/commands/config.rs | 81 -- src-tauri/src/database/dao/mod.rs | 2 - src-tauri/src/database/dao/omo.rs | 77 -- src-tauri/src/database/dao/settings.rs | 25 - src-tauri/src/database/mod.rs | 1 - src-tauri/src/lib.rs | 2 - src-tauri/src/services/omo.rs | 257 +----- src-tauri/src/services/provider/mod.rs | 5 +- .../providers/forms/OmoCommonConfigEditor.tsx | 164 ---- .../providers/forms/OmoGlobalConfigFields.tsx | 773 ------------------ .../providers/forms/ProviderForm.tsx | 35 +- .../forms/helpers/opencodeFormUtils.ts | 10 - .../providers/forms/hooks/useOmoDraftState.ts | 157 +--- src/i18n/locales/en.json | 4 - src/i18n/locales/ja.json | 4 - src/i18n/locales/zh.json | 4 - src/lib/api/config.ts | 28 - src/lib/api/index.ts | 1 - src/lib/query/omo.ts | 58 -- src/types/omo.ts | 87 -- tests/utils/omoConfig.test.ts | 32 +- 21 files changed, 74 insertions(+), 1733 deletions(-) delete mode 100644 src-tauri/src/database/dao/omo.rs delete mode 100644 src/components/providers/forms/OmoCommonConfigEditor.tsx delete mode 100644 src/components/providers/forms/OmoGlobalConfigFields.tsx delete mode 100644 src/lib/api/config.ts diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index ca7759f4c..54c996695 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -7,7 +7,6 @@ use tauri_plugin_opener::OpenerExt; use crate::app_config::AppType; use crate::codex_config; use crate::config::{self, get_claude_settings_path, ConfigStatus}; -use crate::settings; #[tauri::command] pub async fn get_claude_config_status() -> Result { @@ -16,18 +15,6 @@ pub async fn get_claude_config_status() -> Result { use std::str::FromStr; -fn invalid_json_format_error(error: serde_json::Error) -> String { - let lang = settings::get_settings() - .language - .unwrap_or_else(|| "zh".to_string()); - - match lang.as_str() { - "en" => format!("Invalid JSON format: {error}"), - "ja" => format!("JSON形式が無効です: {error}"), - _ => format!("无效的 JSON 格式: {error}"), - } -} - #[tauri::command] pub async fn get_config_status(app: String) -> Result { match AppType::from_str(&app).map_err(|e| e.to_string())? { @@ -163,71 +150,3 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result { Ok(true) } - -#[tauri::command] -pub async fn get_common_config_snippet( - app_type: String, - state: tauri::State<'_, crate::store::AppState>, -) -> Result, String> { - state - .db - .get_config_snippet(&app_type) - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn set_common_config_snippet( - app_type: String, - snippet: String, - state: tauri::State<'_, crate::store::AppState>, -) -> Result<(), String> { - if !snippet.trim().is_empty() { - match app_type.as_str() { - "claude" | "gemini" | "omo" | "omo-slim" => { - serde_json::from_str::(&snippet) - .map_err(invalid_json_format_error)?; - } - "codex" => {} - _ => {} - } - } - - let value = if snippet.trim().is_empty() { - None - } else { - Some(snippet) - }; - - state - .db - .set_config_snippet(&app_type, value) - .map_err(|e| e.to_string())?; - - if app_type == "omo" - && state - .db - .get_current_omo_provider("opencode", "omo") - .map_err(|e| e.to_string())? - .is_some() - { - crate::services::OmoService::write_config_to_file( - state.inner(), - &crate::services::omo::STANDARD, - ) - .map_err(|e| e.to_string())?; - } - if app_type == "omo-slim" - && state - .db - .get_current_omo_provider("opencode", "omo-slim") - .map_err(|e| e.to_string())? - .is_some() - { - crate::services::OmoService::write_config_to_file( - state.inner(), - &crate::services::omo::SLIM, - ) - .map_err(|e| e.to_string())?; - } - Ok(()) -} diff --git a/src-tauri/src/database/dao/mod.rs b/src-tauri/src/database/dao/mod.rs index 53ea93dfa..81ac6a6c1 100644 --- a/src-tauri/src/database/dao/mod.rs +++ b/src-tauri/src/database/dao/mod.rs @@ -4,7 +4,6 @@ pub mod failover; pub mod mcp; -pub mod omo; pub mod prompts; pub mod providers; pub mod proxy; @@ -16,4 +15,3 @@ pub mod universal_providers; // 所有 DAO 方法都通过 Database impl 提供,无需单独导出 // 导出 FailoverQueueItem 供外部使用 pub use failover::FailoverQueueItem; -pub use omo::OmoGlobalConfig; diff --git a/src-tauri/src/database/dao/omo.rs b/src-tauri/src/database/dao/omo.rs deleted file mode 100644 index 85be1ab22..000000000 --- a/src-tauri/src/database/dao/omo.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::database::Database; -use crate::error::AppError; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OmoGlobalConfig { - pub id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub schema_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sisyphus_agent: Option, - #[serde(default)] - pub disabled_agents: Vec, - #[serde(default)] - pub disabled_mcps: Vec, - #[serde(default)] - pub disabled_hooks: Vec, - #[serde(default)] - pub disabled_skills: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub lsp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub experimental: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub background_task: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub browser_automation_engine: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub claude_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub other_fields: Option, - pub updated_at: String, -} - -impl Default for OmoGlobalConfig { - fn default() -> Self { - Self { - id: "global".to_string(), - schema_url: None, - sisyphus_agent: None, - disabled_agents: vec![], - disabled_mcps: vec![], - disabled_hooks: vec![], - disabled_skills: vec![], - lsp: None, - experimental: None, - background_task: None, - browser_automation_engine: None, - claude_code: None, - other_fields: None, - updated_at: chrono::Utc::now().to_rfc3339(), - } - } -} - -impl Database { - pub fn get_omo_global_config(&self, key: &str) -> Result { - let json_str = self.get_setting(key)?; - match json_str { - Some(s) => serde_json::from_str::(&s) - .map_err(|e| AppError::Config(format!("Failed to parse {key}: {e}"))), - None => Ok(OmoGlobalConfig::default()), - } - } - - pub fn save_omo_global_config( - &self, - key: &str, - config: &OmoGlobalConfig, - ) -> Result<(), AppError> { - let json_str = serde_json::to_string(config) - .map_err(|e| AppError::Config(format!("JSON serialization failed: {e}")))?; - self.set_setting(key, &json_str)?; - Ok(()) - } -} diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index 8056a01cb..0b51a62e7 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -38,31 +38,6 @@ impl Database { Ok(()) } - // --- Config Snippets 辅助方法 --- - - /// 获取通用配置片段 - pub fn get_config_snippet(&self, app_type: &str) -> Result, AppError> { - self.get_setting(&format!("common_config_{app_type}")) - } - - /// 设置通用配置片段 - pub fn set_config_snippet( - &self, - app_type: &str, - snippet: Option, - ) -> Result<(), AppError> { - let key = format!("common_config_{app_type}"); - if let Some(value) = snippet { - self.set_setting(&key, &value) - } else { - // 如果为 None 则删除 - let conn = lock_conn!(self.conn); - conn.execute("DELETE FROM settings WHERE key = ?1", params![key]) - .map_err(|e| AppError::Database(e.to_string()))?; - Ok(()) - } - } - // --- 全局出站代理 --- /// 全局代理 URL 的存储键名 diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 9ec7ea608..b5ac00677 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -33,7 +33,6 @@ mod tests; // DAO 类型导出供外部使用 pub use dao::FailoverQueueItem; -pub use dao::OmoGlobalConfig; use crate::config::get_app_config_dir; use crate::error::AppError; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2cefc38c..2ed14d9f9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -845,8 +845,6 @@ pub fn run() { commands::get_skills_migration_result, commands::get_app_config_path, commands::open_app_config_folder, - commands::get_common_config_snippet, - commands::set_common_config_snippet, commands::read_live_provider_settings, commands::patch_claude_live_settings, commands::get_settings, diff --git a/src-tauri/src/services/omo.rs b/src-tauri/src/services/omo.rs index b62ac26ff..49d5be92c 100644 --- a/src-tauri/src/services/omo.rs +++ b/src-tauri/src/services/omo.rs @@ -1,5 +1,4 @@ use crate::config::write_json_file; -use crate::database::OmoGlobalConfig; use crate::error::AppError; use crate::opencode_config::get_opencode_dir; use crate::store::AppState; @@ -13,12 +12,11 @@ pub struct OmoLocalFileData { pub agents: Option, pub categories: Option, pub other_fields: Option, - pub global: OmoGlobalConfig, pub file_path: String, pub last_modified: Option, } -type OmoProfileData = (Option, Option, Option, bool); +type OmoProfileData = (Option, Option, Option); // ── Variant descriptor ───────────────────────────────────────── @@ -28,9 +26,7 @@ pub struct OmoVariant { pub provider_prefix: &'static str, pub plugin_name: &'static str, pub plugin_prefix: &'static str, - pub known_keys: &'static [&'static str], pub has_categories: bool, - pub config_key: &'static str, pub label: &'static str, pub import_label: &'static str, } @@ -41,23 +37,7 @@ pub const STANDARD: OmoVariant = OmoVariant { provider_prefix: "omo-", plugin_name: "oh-my-opencode@latest", plugin_prefix: "oh-my-opencode", - known_keys: &[ - "$schema", - "agents", - "categories", - "sisyphus_agent", - "disabled_agents", - "disabled_mcps", - "disabled_hooks", - "disabled_skills", - "lsp", - "experimental", - "background_task", - "browser_automation_engine", - "claude_code", - ], has_categories: true, - config_key: "common_config_omo", label: "OMO", import_label: "Imported", }; @@ -68,18 +48,7 @@ pub const SLIM: OmoVariant = OmoVariant { provider_prefix: "omo-slim-", plugin_name: "oh-my-opencode-slim@latest", plugin_prefix: "oh-my-opencode-slim", - known_keys: &[ - "$schema", - "agents", - "sisyphus_agent", - "disabled_agents", - "disabled_mcps", - "disabled_hooks", - "lsp", - "experimental", - ], has_categories: false, - config_key: "common_config_omo_slim", label: "OMO Slim", import_label: "Imported Slim", }; @@ -135,47 +104,6 @@ impl OmoService { other } - fn extract_string_array(val: &Value) -> Vec { - val.as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default() - } - - fn merge_global_from_obj(obj: &Map, global: &mut OmoGlobalConfig) { - if let Some(v) = obj.get("$schema") { - global.schema_url = v.as_str().map(|s| s.to_string()); - } - for (key, target) in [ - ("disabled_agents", &mut global.disabled_agents), - ("disabled_mcps", &mut global.disabled_mcps), - ("disabled_hooks", &mut global.disabled_hooks), - ("disabled_skills", &mut global.disabled_skills), - ] { - if let Some(v) = obj.get(key) { - *target = Self::extract_string_array(v); - } - } - for (key, target) in [ - ("sisyphus_agent", &mut global.sisyphus_agent), - ("lsp", &mut global.lsp), - ("experimental", &mut global.experimental), - ("background_task", &mut global.background_task), - ( - "browser_automation_engine", - &mut global.browser_automation_engine, - ), - ("claude_code", &mut global.claude_code), - ] { - if let Some(v) = obj.get(key) { - *target = Some(v.clone()); - } - } - } - // ── Merge helpers ────────────────────────────────────── fn insert_opt_value(result: &mut Map, key: &str, value: &Option) { @@ -184,15 +112,6 @@ impl OmoService { } } - fn insert_string_array(result: &mut Map, key: &str, values: &[String]) { - if !values.is_empty() { - result.insert( - key.to_string(), - serde_json::to_value(values).unwrap_or(Value::Array(vec![])), - ); - } - } - fn insert_object_entries(result: &mut Map, value: Option<&Value>) { if let Some(Value::Object(map)) = value { for (k, v) in map { @@ -214,9 +133,7 @@ impl OmoService { } pub fn write_config_to_file(state: &AppState, v: &OmoVariant) -> Result<(), AppError> { - let global = state.db.get_omo_global_config(v.config_key)?; let current_omo = state.db.get_current_omo_provider("opencode", v.category)?; - let profile_data = current_omo.as_ref().map(|p| { let agents = p.settings_config.get("agents").cloned(); let categories = if v.has_categories { @@ -225,15 +142,10 @@ impl OmoService { None }; let other_fields = p.settings_config.get("otherFields").cloned(); - let use_common_config = p - .settings_config - .get("useCommonConfig") - .and_then(|val| val.as_bool()) - .unwrap_or(true); - (agents, categories, other_fields, use_common_config) + (agents, categories, other_fields) }); - let merged = Self::merge_config(v, &global, profile_data.as_ref()); + let merged = Self::build_config(v, profile_data.as_ref()); let config_path = Self::config_path(v); if let Some(parent) = config_path.parent() { @@ -241,56 +153,20 @@ impl OmoService { } write_json_file(&config_path, &merged)?; - crate::opencode_config::add_plugin(v.plugin_name)?; - log::info!("{} config written to {config_path:?}", v.label); Ok(()) } - fn merge_config( - v: &OmoVariant, - global: &OmoGlobalConfig, - profile_data: Option<&OmoProfileData>, - ) -> Value { + fn build_config(v: &OmoVariant, profile_data: Option<&OmoProfileData>) -> Value { let mut result = Map::new(); - let use_common_config = profile_data.map(|(_, _, _, uc)| *uc).unwrap_or(true); - - if use_common_config { - if let Some(url) = &global.schema_url { - result.insert("$schema".to_string(), Value::String(url.clone())); - } - - Self::insert_opt_value(&mut result, "sisyphus_agent", &global.sisyphus_agent); - Self::insert_string_array(&mut result, "disabled_agents", &global.disabled_agents); - Self::insert_string_array(&mut result, "disabled_mcps", &global.disabled_mcps); - Self::insert_string_array(&mut result, "disabled_hooks", &global.disabled_hooks); - - if v.has_categories { - Self::insert_string_array(&mut result, "disabled_skills", &global.disabled_skills); - Self::insert_opt_value(&mut result, "background_task", &global.background_task); - Self::insert_opt_value( - &mut result, - "browser_automation_engine", - &global.browser_automation_engine, - ); - Self::insert_opt_value(&mut result, "claude_code", &global.claude_code); - } - - Self::insert_opt_value(&mut result, "lsp", &global.lsp); - Self::insert_opt_value(&mut result, "experimental", &global.experimental); - - Self::insert_object_entries(&mut result, global.other_fields.as_ref()); - } - - if let Some((agents, categories, other_fields, _)) = profile_data { + if let Some((agents, categories, other_fields)) = profile_data { + Self::insert_object_entries(&mut result, other_fields.as_ref()); Self::insert_opt_value(&mut result, "agents", agents); if v.has_categories { Self::insert_opt_value(&mut result, "categories", categories); } - Self::insert_object_entries(&mut result, other_fields.as_ref()); } - Value::Object(result) } @@ -310,18 +186,12 @@ impl OmoService { settings.insert("categories".to_string(), categories.clone()); } } - settings.insert("useCommonConfig".to_string(), Value::Bool(true)); - let other = Self::extract_other_fields_with_keys(&obj, v.known_keys); + let other = Self::extract_other_fields_with_keys(&obj, &["agents", "categories"]); if !other.is_empty() { settings.insert("otherFields".to_string(), Value::Object(other)); } - let mut global = state.db.get_omo_global_config(v.config_key)?; - Self::merge_global_from_obj(&obj, &mut global); - global.updated_at = chrono::Utc::now().to_rfc3339(); - state.db.save_omo_global_config(v.config_key, &global)?; - let provider_id = format!("{}{}", v.provider_prefix, uuid::Uuid::new_v4()); let name = format!( "{} {}", @@ -384,22 +254,17 @@ impl OmoService { None }; - let other = Self::extract_other_fields_with_keys(obj, v.known_keys); + let other = Self::extract_other_fields_with_keys(obj, &["agents", "categories"]); let other_fields = if other.is_empty() { None } else { Some(Value::Object(other)) }; - let mut global = OmoGlobalConfig::default(); - Self::merge_global_from_obj(obj, &mut global); - global.other_fields = other_fields.clone(); - OmoLocalFileData { agents, categories, other_fields, - global, file_path, last_modified, } @@ -482,26 +347,24 @@ mod tests { } #[test] - fn test_merge_config_empty() { - let global = OmoGlobalConfig::default(); - let merged = OmoService::merge_config(&STANDARD, &global, None); + fn test_build_config_empty() { + let merged = OmoService::build_config(&STANDARD, None); assert!(merged.is_object()); + assert!(merged.as_object().unwrap().is_empty()); } #[test] - fn test_merge_config_with_profile() { - let global = OmoGlobalConfig { - schema_url: Some("https://example.com/schema.json".to_string()), - disabled_agents: vec!["explore".to_string()], - ..Default::default() - }; + fn test_build_config_with_profile() { let agents = Some(serde_json::json!({ "sisyphus": { "model": "claude-opus-4-5" } })); let categories = None; - let other_fields = None; - let profile_data = (agents, categories, other_fields, true); - let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data)); + let other_fields = Some(serde_json::json!({ + "$schema": "https://example.com/schema.json", + "disabled_agents": ["explore"] + })); + let profile_data = (agents, categories, other_fields); + let merged = OmoService::build_config(&STANDARD, Some(&profile_data)); let obj = merged.as_object().unwrap(); assert_eq!(obj["$schema"], "https://example.com/schema.json"); @@ -511,28 +374,7 @@ mod tests { } #[test] - fn test_merge_config_without_common_config() { - let global = OmoGlobalConfig { - schema_url: Some("https://example.com/schema.json".to_string()), - disabled_agents: vec!["explore".to_string()], - ..Default::default() - }; - let agents = Some(serde_json::json!({ - "sisyphus": { "model": "claude-opus-4-5" } - })); - let categories = None; - let other_fields = None; - let profile_data = (agents, categories, other_fields, false); - let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data)); - let obj = merged.as_object().unwrap(); - - assert!(!obj.contains_key("$schema")); - assert!(!obj.contains_key("disabled_agents")); - assert!(obj.contains_key("agents")); - } - - #[test] - fn test_build_local_file_data_keeps_unknown_top_level_fields_in_global() { + fn test_build_local_file_data_keeps_all_non_agent_category_fields_in_other() { let obj = serde_json::json!({ "$schema": "https://example.com/schema.json", "disabled_agents": ["oracle"], @@ -555,64 +397,53 @@ mod tests { None, ); + // All non-agents/categories fields should be in other_fields + let other = data.other_fields.unwrap(); + let other_obj = other.as_object().unwrap(); assert_eq!( - data.global.schema_url.as_deref(), - Some("https://example.com/schema.json") + other_obj.get("$schema").unwrap(), + "https://example.com/schema.json" ); - assert_eq!(data.global.disabled_agents, vec!["oracle".to_string()]); - assert_eq!( - data.other_fields, - Some(serde_json::json!({ - "custom_top_level": { "enabled": true } - })) + other_obj.get("disabled_agents").unwrap(), + &serde_json::json!(["oracle"]) ); - assert_eq!(data.global.other_fields, data.other_fields); + assert_eq!( + other_obj.get("custom_top_level").unwrap(), + &serde_json::json!({"enabled": true}) + ); + // agents and categories should NOT be in other_fields + assert!(!other_obj.contains_key("agents")); + assert!(!other_obj.contains_key("categories")); } #[test] - fn test_merge_config_ignores_non_object_other_fields() { - let global = OmoGlobalConfig { - other_fields: Some(serde_json::json!(["global_non_object"])), - ..Default::default() - }; + fn test_build_config_ignores_non_object_other_fields() { let agents = None; let categories = None; let other_fields = Some(serde_json::json!("profile_non_object")); - let profile_data = (agents, categories, other_fields, true); + let profile_data = (agents, categories, other_fields); - let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data)); + let merged = OmoService::build_config(&STANDARD, Some(&profile_data)); let obj = merged.as_object().unwrap(); - assert!(!obj.contains_key("0")); - assert!(!obj.contains_key("global_non_object")); assert!(!obj.contains_key("profile_non_object")); } #[test] - fn test_merge_config_slim_excludes_categories_and_extra_fields() { - let global = OmoGlobalConfig { - schema_url: Some("https://slim.schema".to_string()), - disabled_agents: vec!["oracle".to_string()], - disabled_skills: vec!["playwright".to_string()], - background_task: Some(serde_json::json!({"key": "val"})), - browser_automation_engine: Some(serde_json::json!({"provider": "pw"})), - claude_code: Some(serde_json::json!({"mcp": true})), - ..Default::default() - }; + fn test_build_config_slim_excludes_categories() { let agents = Some(serde_json::json!({"orchestrator": {"model": "k2"}})); let categories = Some(serde_json::json!({"code": {"model": "gpt"}})); - let other_fields = None; - let profile_data = (agents, categories, other_fields, true); + let other_fields = Some(serde_json::json!({ + "$schema": "https://slim.schema", + "disabled_agents": ["oracle"] + })); + let profile_data = (agents, categories, other_fields); - let merged = OmoService::merge_config(&SLIM, &global, Some(&profile_data)); + let merged = OmoService::build_config(&SLIM, Some(&profile_data)); let obj = merged.as_object().unwrap(); - // Slim should NOT include these - assert!(!obj.contains_key("disabled_skills")); - assert!(!obj.contains_key("background_task")); - assert!(!obj.contains_key("browser_automation_engine")); - assert!(!obj.contains_key("claude_code")); + // Slim should NOT include categories assert!(!obj.contains_key("categories")); // Slim SHOULD include these diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index bde2dd925..17aa759a6 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -525,9 +525,8 @@ impl ProviderService { .set_omo_provider_current(app_type.as_str(), id, "omo-slim")?; crate::services::OmoService::write_config_to_file(state, &crate::services::omo::SLIM)?; // OMO ↔ OMO Slim mutually exclusive: remove Standard config - let _ = crate::services::OmoService::delete_config_file( - &crate::services::omo::STANDARD, - ); + let _ = + crate::services::OmoService::delete_config_file(&crate::services::omo::STANDARD); return Ok(SwitchResult::default()); } diff --git a/src/components/providers/forms/OmoCommonConfigEditor.tsx b/src/components/providers/forms/OmoCommonConfigEditor.tsx deleted file mode 100644 index 9f0a4a3d9..000000000 --- a/src/components/providers/forms/OmoCommonConfigEditor.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { useEffect, useState } from "react"; -import { FullScreenPanel } from "@/components/common/FullScreenPanel"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { Save, FolderInput, Loader2 } from "lucide-react"; -import JsonEditor from "@/components/JsonEditor"; -import { - OmoGlobalConfigFields, - type OmoGlobalConfigFieldsRef, -} from "./OmoGlobalConfigFields"; -import type { OmoGlobalConfig } from "@/types/omo"; - -interface OmoCommonConfigEditorProps { - previewValue: string; - useCommonConfig: boolean; - onCommonConfigToggle: (checked: boolean) => void; - isModalOpen: boolean; - onEditClick: () => void; - onModalClose: () => void; - onSave: () => Promise; - isSaving: boolean; - onGlobalConfigStateChange: (config: OmoGlobalConfig) => void; - globalConfigRef: React.RefObject; - fieldsKey: number; - isSlim?: boolean; -} - -export function OmoCommonConfigEditor({ - previewValue, - useCommonConfig, - onCommonConfigToggle, - isModalOpen, - onEditClick, - onModalClose, - onSave, - isSaving, - onGlobalConfigStateChange, - globalConfigRef, - fieldsKey, - isSlim = false, -}: OmoCommonConfigEditorProps) { - const { t } = useTranslation(); - const [isDarkMode, setIsDarkMode] = useState(false); - const [isImporting, setIsImporting] = useState(false); - useEffect(() => { - const syncDarkMode = () => - setIsDarkMode(document.documentElement.classList.contains("dark")); - syncDarkMode(); - const observer = new MutationObserver(syncDarkMode); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); - return () => observer.disconnect(); - }, []); - const handleImportLocal = async () => { - if (!globalConfigRef.current) return; - setIsImporting(true); - try { - await globalConfigRef.current.importFromLocal(); - } finally { - setIsImporting(false); - } - }; - return ( - <> -
-
- -
- -
-
-
- -
- {}} - darkMode={isDarkMode} - rows={14} - showValidation={false} - language="json" - /> -
- - - - - - } - > -
-

- {t("omo.commonConfigHint", { - defaultValue: - "OMO common config will be merged into all OMO configs that enable it", - })} -

- } - onStateChange={onGlobalConfigStateChange} - hideSaveButtons - isSlim={isSlim} - /> -
-
- - ); -} diff --git a/src/components/providers/forms/OmoGlobalConfigFields.tsx b/src/components/providers/forms/OmoGlobalConfigFields.tsx deleted file mode 100644 index 3e8e1ab9d..000000000 --- a/src/components/providers/forms/OmoGlobalConfigFields.tsx +++ /dev/null @@ -1,773 +0,0 @@ -import { - useState, - useEffect, - useCallback, - forwardRef, - useImperativeHandle, -} from "react"; -import { useTranslation } from "react-i18next"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Save, - Loader2, - X, - FolderInput, - RotateCcw, - ChevronsUpDown, -} from "lucide-react"; -import { cn } from "@/lib/utils"; -import { toast } from "sonner"; -import type { OmoGlobalConfig } from "@/types/omo"; -import { - OMO_DISABLEABLE_AGENTS, - OMO_DISABLEABLE_MCPS, - OMO_DISABLEABLE_HOOKS, - OMO_DISABLEABLE_SKILLS, - OMO_DEFAULT_SCHEMA_URL, - OMO_SISYPHUS_AGENT_PLACEHOLDER, - OMO_LSP_PLACEHOLDER, - OMO_EXPERIMENTAL_PLACEHOLDER, - OMO_BACKGROUND_TASK_PLACEHOLDER, - OMO_BROWSER_AUTOMATION_PLACEHOLDER, - OMO_CLAUDE_CODE_PLACEHOLDER, - OMO_SLIM_DISABLEABLE_AGENTS, - OMO_SLIM_DISABLEABLE_MCPS, - OMO_SLIM_DISABLEABLE_HOOKS, - OMO_SLIM_DEFAULT_SCHEMA_URL, -} from "@/types/omo"; -import { - useOmoGlobalConfig, - useSaveOmoGlobalConfig, - useReadOmoLocalFile, - useOmoSlimGlobalConfig, - useSaveOmoSlimGlobalConfig, - useReadOmoSlimLocalFile, -} from "@/lib/query/omo"; - -interface PresetOption { - readonly value: string; - readonly label: string; -} - -export interface OmoGlobalConfigFieldsRef { - buildCurrentConfig: () => OmoGlobalConfig; - buildCurrentConfigStrict: () => OmoGlobalConfig; - importFromLocal: () => Promise; -} - -interface OmoGlobalConfigFieldsProps { - onStateChange?: (config: OmoGlobalConfig) => void; - hideSaveButtons?: boolean; - isSlim?: boolean; -} - -type OmoAdvancedFieldKey = - | "lspStr" - | "experimentalStr" - | "backgroundTaskStr" - | "browserStr" - | "claudeCodeStr"; - -const OMO_ADVANCED_JSON_FIELDS: ReadonlyArray<{ - key: OmoAdvancedFieldKey; - labelKey: string; - defaultLabel: string; - placeholder: string; - minHeight: string; -}> = [ - { - key: "lspStr", - labelKey: "omo.advancedLsp", - defaultLabel: "LSP Config", - placeholder: OMO_LSP_PLACEHOLDER, - minHeight: "200px", - }, - { - key: "experimentalStr", - labelKey: "omo.advancedExperimental", - defaultLabel: "Experimental Features", - placeholder: OMO_EXPERIMENTAL_PLACEHOLDER, - minHeight: "120px", - }, - { - key: "backgroundTaskStr", - labelKey: "omo.advancedBackgroundTask", - defaultLabel: "Background Tasks", - placeholder: OMO_BACKGROUND_TASK_PLACEHOLDER, - minHeight: "250px", - }, - { - key: "browserStr", - labelKey: "omo.advancedBrowserAutomation", - defaultLabel: "Browser Automation", - placeholder: OMO_BROWSER_AUTOMATION_PLACEHOLDER, - minHeight: "80px", - }, - { - key: "claudeCodeStr", - labelKey: "omo.advancedClaudeCode", - defaultLabel: "Claude Code", - placeholder: OMO_CLAUDE_CODE_PLACEHOLDER, - minHeight: "180px", - }, -]; - -const OMO_SLIM_ADVANCED_KEYS: ReadonlySet = new Set([ - "lspStr", - "experimentalStr", -]); - -function TagListEditor({ - label, - values, - onChange, - placeholder, - presets, -}: { - label: string; - values: string[]; - onChange: (values: string[]) => void; - placeholder?: string; - presets?: readonly PresetOption[]; -}) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const [search, setSearch] = useState(""); - - const toggleValue = (v: string) => { - if (values.includes(v)) { - onChange(values.filter((x) => x !== v)); - } else { - onChange([...values, v]); - } - }; - const customValue = search.trim(); - const canAddCustom = customValue.length > 0 && !values.includes(customValue); - const triggerText = - values.length === 0 - ? placeholder || t("omo.selectPlaceholder", { defaultValue: "Select..." }) - : values.length === 1 - ? values[0] - : `${values[0]} +${values.length - 1}`; - - const availablePresets = presets?.filter( - (p) => - !search.trim() || - p.label.toLowerCase().includes(search.toLowerCase()) || - p.value.toLowerCase().includes(search.toLowerCase()), - ); - - return ( -
-
- - {values.length > 0 && ( - - )} -
- {values.length > 0 && ( -
- {values.map((v, i) => ( - - {v} - - - ))} -
- )} - - - - - -
- setSearch(e.target.value)} - onKeyDown={(e) => { - e.stopPropagation(); - if (e.key === "Enter" && canAddCustom) { - e.preventDefault(); - onChange([...values, customValue]); - setSearch(""); - } - }} - placeholder={ - placeholder || - t("omo.searchOrType", { - defaultValue: "Search or type custom value...", - }) - } - className="h-7 text-sm" - autoFocus - /> -
- {canAddCustom && ( - - )} -
- {availablePresets && availablePresets.length > 0 ? ( - availablePresets.map((p) => { - const checked = values.includes(p.value); - return ( - e.preventDefault()} - onCheckedChange={() => toggleValue(p.value)} - className="text-sm" - > - {p.label} - - ); - }) - ) : ( -
- {t("omo.noMatches", { defaultValue: "No matches" })} -
- )} -
-
-
-
- ); -} - -function JsonTextareaField({ - label, - value, - onChange, - placeholder, - minHeight, -}: { - label: string; - value: string; - onChange: (value: string) => void; - placeholder?: string; - minHeight?: string; -}) { - return ( -
- -