From 2deee1097ba70d806eaf2a2947da0d28e7236e74 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 8 May 2026 16:02:58 +0800 Subject: [PATCH] refactor(claude-desktop): align provider form UI with Claude Code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename field labels: "Gateway Base URL" → "API Endpoint", "Bearer Token" → "API Key" - Change layout from 2-column grid to vertical sections matching Claude Code - Reuse shared EndpointField component with format-aware amber hint box - Replace native with vendor-grouped ModelDropdown (OpenCode pattern) - Move Fetch/Add buttons to section headers with compact sm styling - Extract ModelDropdown to shared module, deduplicate from OpenCodeFormFields - Extract renderActionButtons helper to eliminate proxy/direct button duplication - Remove dead i18n keys (gatewayBaseUrl, bearerToken) from all 3 locales --- .../forms/ClaudeDesktopProviderForm.tsx | 315 +++++++++--------- .../providers/forms/OpenCodeFormFields.tsx | 62 +--- .../providers/forms/shared/ModelDropdown.tsx | 53 +++ .../providers/forms/shared/index.ts | 1 + src/i18n/locales/en.json | 2 - src/i18n/locales/ja.json | 2 - src/i18n/locales/zh.json | 2 - 7 files changed, 215 insertions(+), 222 deletions(-) create mode 100644 src/components/providers/forms/shared/ModelDropdown.tsx diff --git a/src/components/providers/forms/ClaudeDesktopProviderForm.tsx b/src/components/providers/forms/ClaudeDesktopProviderForm.tsx index b81528ac8..435321cdc 100644 --- a/src/components/providers/forms/ClaudeDesktopProviderForm.tsx +++ b/src/components/providers/forms/ClaudeDesktopProviderForm.tsx @@ -3,7 +3,14 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { ChevronDown, ChevronRight, Loader2, Plus, Trash2 } from "lucide-react"; +import { + ChevronDown, + ChevronRight, + Download, + Loader2, + Plus, + Trash2, +} from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -30,6 +37,8 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { BasicFormFields } from "./BasicFormFields"; +import { EndpointField } from "./shared/EndpointField"; +import { ModelDropdown } from "./shared/ModelDropdown"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { ClaudeApiFormat, @@ -391,6 +400,36 @@ export function ClaudeDesktopProviderForm({ }); }; + const renderActionButtons = (onAdd: () => void, addLabel: string) => ( +
+ + +
+ ); + return (
-
-
- - setBaseUrl(event.target.value)} - placeholder="https://api.example.com" - /> -
-
- - setApiKey(event.target.value)} - type="password" - placeholder="sk-..." - /> -
+
+ + setApiKey(event.target.value)} + type="password" + placeholder="sk-..." + />
+ setBaseUrl(v)} + placeholder={t("providerForm.apiEndpointPlaceholder")} + hint={ + needsModelMapping && apiFormat === "openai_responses" + ? t("providerForm.apiHintResponses") + : needsModelMapping && apiFormat === "openai_chat" + ? t("providerForm.apiHintOAI") + : needsModelMapping && apiFormat === "gemini_native" + ? t("providerForm.apiHintGeminiNative") + : t("providerForm.apiHint") + } + showManageButton={false} + /> +
@@ -460,68 +499,45 @@ export function ClaudeDesktopProviderForm({ {needsModelMapping && (
-
-
- - -
-
- -
+
+ +
- - {fetchedModels.map((model) => ( - -
@@ -530,6 +546,14 @@ export function ClaudeDesktopProviderForm({ defaultValue: "模型映射", })} + {renderActionButtons( + () => + setRoutes((current) => [ + ...current, + nextRouteRow(current, defaultProxyRouteRows), + ]), + t("claudeDesktop.addRoute", { defaultValue: "添加路由" }), + )}

{t("claudeDesktop.routeMapHint", { @@ -574,14 +598,22 @@ export function ClaudeDesktopProviderForm({ } placeholder="claude-sonnet-4-6" /> - - updateRoute(index, { model: event.target.value }) - } - list="claude-desktop-upstream-models" - placeholder="kimi-k2 / deepseek-chat" - /> +

+ + updateRoute(index, { model: event.target.value }) + } + placeholder="kimi-k2 / deepseek-chat" + className="flex-1" + /> + {fetchedModels.length > 0 && ( + updateRoute(index, { model: id })} + /> + )} +
@@ -613,20 +645,6 @@ export function ClaudeDesktopProviderForm({
))}
- -
)} @@ -664,33 +682,27 @@ export function ClaudeDesktopProviderForm({
-

+

{t("claudeDesktop.directModelListHint", { defaultValue: "仅当供应商的 /v1/models 不可用或没有返回 Claude Desktop 可识别的 claude-* 模型名时填写;这些模型名会原样发送给供应商。", })}

- + {renderActionButtons( + () => + setRoutes((current) => [ + ...current, + { + route: "", + model: "", + displayName: "", + supports1m: false, + }, + ]), + t("claudeDesktop.addModel", { defaultValue: "添加模型" }), + )}
- - {fetchedModels.map((model) => ( - - {routes.length > 0 ? (
{routes.map((route, index) => ( @@ -698,14 +710,24 @@ export function ClaudeDesktopProviderForm({ key={`${route.route}-${index}`} className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_92px_36px]" > - - updateRoute(index, { route: event.target.value }) - } - list="claude-desktop-direct-models" - placeholder="claude-deepseek-chat" - /> +
+ + updateRoute(index, { route: event.target.value }) + } + placeholder="claude-deepseek-chat" + className="flex-1" + /> + {fetchedModels.length > 0 && ( + + updateRoute(index, { route: id }) + } + /> + )} +
diff --git a/src/components/providers/forms/OpenCodeFormFields.tsx b/src/components/providers/forms/OpenCodeFormFields.tsx index 717b166d0..70ba3307a 100644 --- a/src/components/providers/forms/OpenCodeFormFields.tsx +++ b/src/components/providers/forms/OpenCodeFormFields.tsx @@ -11,23 +11,8 @@ import { SelectValue, } from "@/components/ui/select"; import { toast } from "sonner"; -import { - ChevronDown, - Download, - Plus, - Trash2, - ChevronRight, - Loader2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { ApiKeySection } from "./shared"; +import { Download, Plus, Trash2, ChevronRight, Loader2 } from "lucide-react"; +import { ApiKeySection, ModelDropdown } from "./shared"; import { fetchModelsForConfig, showFetchModelsError, @@ -157,49 +142,6 @@ function ModelOptionKeyInput({ ); } -/** Dropdown button to select from fetched models */ -function ModelDropdown({ - models, - onSelect, -}: { - models: FetchedModel[]; - onSelect: (id: string) => void; -}) { - const grouped: Record = {}; - for (const model of models) { - const vendor = model.ownedBy || "Other"; - if (!grouped[vendor]) grouped[vendor] = []; - grouped[vendor].push(model); - } - const vendors = Object.keys(grouped).sort(); - - return ( - - - - - - {vendors.map((vendor, vi) => ( -
- {vi > 0 && } - {vendor} - {grouped[vendor].map((m) => ( - onSelect(m.id)}> - {m.id} - - ))} -
- ))} -
-
- ); -} - interface OpenCodeFormFieldsProps { // NPM Package npm: string; diff --git a/src/components/providers/forms/shared/ModelDropdown.tsx b/src/components/providers/forms/shared/ModelDropdown.tsx new file mode 100644 index 000000000..ec5d64b84 --- /dev/null +++ b/src/components/providers/forms/shared/ModelDropdown.tsx @@ -0,0 +1,53 @@ +import { ChevronDown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import type { FetchedModel } from "@/lib/api/model-fetch"; + +export function ModelDropdown({ + models, + onSelect, +}: { + models: FetchedModel[]; + onSelect: (id: string) => void; +}) { + const grouped: Record = {}; + for (const model of models) { + const vendor = model.ownedBy || "Other"; + if (!grouped[vendor]) grouped[vendor] = []; + grouped[vendor].push(model); + } + const vendors = Object.keys(grouped).sort(); + + return ( + + + + + + {vendors.map((vendor, vi) => ( +
+ {vi > 0 && } + {vendor} + {grouped[vendor].map((m) => ( + onSelect(m.id)}> + {m.id} + + ))} +
+ ))} +
+
+ ); +} diff --git a/src/components/providers/forms/shared/index.ts b/src/components/providers/forms/shared/index.ts index e5be59876..e1359c466 100644 --- a/src/components/providers/forms/shared/index.ts +++ b/src/components/providers/forms/shared/index.ts @@ -1,3 +1,4 @@ export { ApiKeySection } from "./ApiKeySection"; export { EndpointField } from "./EndpointField"; +export { ModelDropdown } from "./ModelDropdown"; export { ModelInputWithFetch } from "./ModelInputWithFetch"; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 396aac0a2..89b49109b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -177,8 +177,6 @@ "mode": "Model handling", "modeDirect": "Direct", "modeProxy": "Model mapping", - "gatewayBaseUrl": "Gateway Base URL", - "bearerToken": "Bearer Token", "modelMappingToggle": "Needs model mapping", "modelMappingOffHint": "Use this when the provider already exposes and accepts claude-* / anthropic/claude-* model IDs through Anthropic Messages. Claude Desktop connects to the provider directly.", "modelMappingOnHint": "Claude Desktop only sees claude-* route names. CC Switch local routing maps them to the real upstream models, so local routing must stay running.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e8e93b69e..32b38eee8 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -177,8 +177,6 @@ "mode": "モデル処理方式", "modeDirect": "直結", "modeProxy": "モデルマッピング", - "gatewayBaseUrl": "Gateway Base URL", - "bearerToken": "Bearer Token", "modelMappingToggle": "モデルマッピングが必要", "modelMappingOffHint": "プロバイダーが Anthropic Messages で claude-* / anthropic/claude-* のモデル ID を公開し、そのまま受け付ける場合に使います。Claude Desktop はプロバイダーへ直接接続します。", "modelMappingOnHint": "Claude Desktop には claude-* のルート名だけを見せ、CC Switch のローカルルーティングが実際の上流モデルへマッピングします。ローカルルーティングを起動したままにしてください。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 638ff59e8..70af32e4e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -177,8 +177,6 @@ "mode": "模型处理方式", "modeDirect": "直连", "modeProxy": "模型映射", - "gatewayBaseUrl": "Gateway Base URL", - "bearerToken": "Bearer Token", "modelMappingToggle": "需要模型映射", "modelMappingOffHint": "适合供应商已经暴露并接受 claude-* / anthropic/claude-* 模型名的 Anthropic Messages API;请求会由 Claude Desktop 直连供应商。", "modelMappingOnHint": "Claude Desktop 只看到 claude-* 路由名,CC Switch 本地路由会映射到真实上游模型;需要保持本地路由运行。",