From 9745af59c1d4d1d0ff139d864118508fbcc15544 Mon Sep 17 00:00:00 2001 From: hqhq1025 <1506751656@qq.com> Date: Mon, 20 Apr 2026 01:06:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(desktop):=20simplify=20provider=20card?= =?UTF-8?q?s=20in=20Settings=20=E2=86=92=20API=20=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse each provider card to one header row (label, masked key, optional base URL). The "Active" badge or a compact "set active" button sits on the right; the model selector is hidden on non-active providers and rendered as a click-to-edit chip on the active card. Per-row actions (Test connection, Re-enter key, Delete) move into a ··· overflow menu, and the duplicated dashed "Add provider" button at the bottom of the list is removed. Signed-off-by: hqhq1025 <1506751656@qq.com> --- .changeset/settings-provider-card-cleanup.md | 13 + .../src/renderer/src/components/Settings.tsx | 252 +++++++++++++----- packages/i18n/src/locales/en.json | 8 +- packages/i18n/src/locales/zh-CN.json | 8 +- 4 files changed, 206 insertions(+), 75 deletions(-) create mode 100644 .changeset/settings-provider-card-cleanup.md diff --git a/.changeset/settings-provider-card-cleanup.md b/.changeset/settings-provider-card-cleanup.md new file mode 100644 index 0000000..0f2abe5 --- /dev/null +++ b/.changeset/settings-provider-card-cleanup.md @@ -0,0 +1,13 @@ +--- +'@open-codesign/desktop': patch +--- + +feat(desktop): simplify provider cards in Settings → API 服务 + +Each provider card now collapses to a single header row (label, masked key, +optional base URL) with the active badge or a compact "set active" button on +the right. The model selector is hidden on non-active providers (they don't +drive any generation) and shown as a click-to-edit chip on the active card. +Per-row actions move into a `···` overflow menu (Test connection on the +active card, Re-enter key, Delete with inline confirm), and the duplicated +dashed "Add provider" button at the bottom of the list is removed. diff --git a/apps/desktop/src/renderer/src/components/Settings.tsx b/apps/desktop/src/renderer/src/components/Settings.tsx index c86cc53..381cd21 100644 --- a/apps/desktop/src/renderer/src/components/Settings.tsx +++ b/apps/desktop/src/renderer/src/components/Settings.tsx @@ -18,14 +18,15 @@ import { Cpu, FolderOpen, Globe, + KeyRound, Loader2, + MoreHorizontal, Palette, Plus, RotateCcw, Sliders, Trash2, X, - Zap, } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import type { AppPaths, Preferences, ProviderRow } from '../../../preload/index'; @@ -507,6 +508,127 @@ function AddProviderModal({ ); } +function ProviderOverflowMenu({ + isActive, + hasError, + onTestConnection, + onReEnterKey, + onDelete, + label, +}: { + isActive: boolean; + hasError: boolean; + onTestConnection: () => void; + onReEnterKey: () => void; + onDelete: () => void; + label: string; +}) { + const t = useT(); + const [open, setOpen] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + const wrapRef = useRef(null); + + useEffect(() => { + if (!open) return; + function onDocClick(e: MouseEvent) { + if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) { + setOpen(false); + setConfirmDelete(false); + } + } + document.addEventListener('mousedown', onDocClick); + return () => document.removeEventListener('mousedown', onDocClick); + }, [open]); + + function close() { + setOpen(false); + setConfirmDelete(false); + } + + const itemClass = + 'w-full flex items-center gap-2 px-2.5 py-1.5 text-left text-[var(--text-xs)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors'; + + return ( +
+ + {open && ( +
+ {isActive && !hasError && ( + + )} + + {confirmDelete ? ( +
+ + +
+ ) : ( + + )} +
+ )} +
+ ); +} + function ProviderCard({ row, config, @@ -521,8 +643,8 @@ function ProviderCard({ onReEnterKey: (p: SupportedOnboardingProvider) => void; }) { const t = useT(); + const pushToast = useCodesignStore((s) => s.pushToast); const label = SHORTLIST[row.provider]?.label ?? row.provider; - const [confirmDelete, setConfirmDelete] = useState(false); const hasError = row.error !== undefined; const stateClass = hasError @@ -531,27 +653,35 @@ function ProviderCard({ ? 'border-[var(--color-border)] border-l-[var(--size-accent-stripe)] border-l-[var(--color-accent)] bg-[var(--color-accent-tint)]' : 'border-[var(--color-border)] bg-[var(--color-surface)]'; + async function handleTestConnection() { + if (!window.codesign) return; + const res = await window.codesign.connection.testActive(); + if (res.ok) { + pushToast({ variant: 'success', title: t('settings.providers.toast.connectionOk') }); + } else { + pushToast({ + variant: 'error', + title: t('settings.providers.toast.connectionFailed'), + description: res.hint || res.message, + }); + } + } + return (
-
-
+
+
{label} - {row.isActive && !hasError && ( - - {t('settings.providers.active')} - - )} - {hasError && ( + {hasError ? ( {t('settings.providers.decryptionFailed')} - )} - {!hasError && ( + ) : ( {row.maskedKey} @@ -564,7 +694,12 @@ function ProviderCard({ )}
-
+
+ {row.isActive && !hasError && ( + + {t('settings.providers.active')} + + )} {!row.isActive && !hasError && ( - -
- ) : ( - - )} + onReEnterKey(row.provider)} + onDelete={() => onDelete(row.provider)} + label={label} + />
@@ -637,29 +750,20 @@ function ActiveModelSelector({ const pushToast = useCodesignStore((s) => s.pushToast); const [primary, setPrimary] = useState(config.modelPrimary ?? sl.defaultPrimary); - const saveTimeout = useRef | null>(null); + const [editing, setEditing] = useState(false); useEffect(() => { setPrimary(config.modelPrimary ?? sl.defaultPrimary); }, [config.modelPrimary, sl.defaultPrimary]); - useEffect(() => { - return () => { - if (saveTimeout.current !== null) { - clearTimeout(saveTimeout.current); - saveTimeout.current = null; - } - }; - }, []); - - async function save(p: string) { + async function save(next: string) { if (!window.codesign) return; try { - const next = await window.codesign.settings.setActiveProvider({ + const updated = await window.codesign.settings.setActiveProvider({ provider, - modelPrimary: p, + modelPrimary: next, }); - setConfig(next); + setConfig(updated); } catch (err) { pushToast({ variant: 'error', @@ -669,18 +773,28 @@ function ActiveModelSelector({ } } - function handlePrimaryChange(v: string) { + function handleChange(v: string) { setPrimary(v); - if (saveTimeout.current !== null) clearTimeout(saveTimeout.current); - saveTimeout.current = setTimeout(() => void save(v), 400); + setEditing(false); + void save(v); } return ( -
-

- {t('settings.providers.primary')} -

- +
+ + {editing ? ( + + ) : ( + + )}
); } @@ -820,14 +934,6 @@ function ModelsTab() { onReEnterKey={setReEnterProvider} /> ))} -
)}
diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index a619c69..97d7524 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -114,6 +114,10 @@ "setActive": "Set active", "reEnterKey": "Re-enter key", "confirm": "Confirm", + "delete": "Delete", + "testConnection": "Test connection", + "moreActions": "More actions", + "editModel": "Change model", "deleteAria": "Delete {{label}} provider", "primary": "Primary", "fast": "Fast", @@ -140,7 +144,9 @@ "switchedTo": "Switched to {{label}}", "switchFailed": "Switch failed", "saved": "Provider saved", - "modelSaveFailed": "Failed to save model selection" + "modelSaveFailed": "Failed to save model selection", + "connectionOk": "Connection OK", + "connectionFailed": "Connection failed" } }, "appearance": { diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json index dc68bc1..b4b1ab2 100644 --- a/packages/i18n/src/locales/zh-CN.json +++ b/packages/i18n/src/locales/zh-CN.json @@ -113,6 +113,10 @@ "setActive": "设为当前", "reEnterKey": "重新输入 Key", "confirm": "确认", + "delete": "删除", + "testConnection": "测试连接", + "moreActions": "更多操作", + "editModel": "更改模型", "deleteAria": "删除 {{label}} 服务", "primary": "主力", "fast": "快速", @@ -139,7 +143,9 @@ "switchedTo": "已切换到 {{label}}", "switchFailed": "切换失败", "saved": "服务已保存", - "modelSaveFailed": "保存模型选择失败" + "modelSaveFailed": "保存模型选择失败", + "connectionOk": "连接正常", + "connectionFailed": "连接失败" } }, "appearance": { From 12bfda1c4fbd648ed6fe8cf4642ce987afa32937 Mon Sep 17 00:00:00 2001 From: hqhq1025 <1506751656@qq.com> Date: Mon, 20 Apr 2026 01:11:40 +0800 Subject: [PATCH 2/2] fix(desktop): catch test-connection errors and roll back chip on save failure Codex flagged two Major issues in #116: - handleTestConnection awaited testActive() without try/catch, leaking bridge errors as unhandled promise rejections. - Model chip optimistically set new value before save completed; on save failure the UI showed an unsaved model. Now save() returns boolean and handleChange rolls back primary to its previous value when save fails. Signed-off-by: hqhq1025 <1506751656@qq.com> --- .../src/renderer/src/components/Settings.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/Settings.tsx b/apps/desktop/src/renderer/src/components/Settings.tsx index 381cd21..0974aa1 100644 --- a/apps/desktop/src/renderer/src/components/Settings.tsx +++ b/apps/desktop/src/renderer/src/components/Settings.tsx @@ -655,14 +655,22 @@ function ProviderCard({ async function handleTestConnection() { if (!window.codesign) return; - const res = await window.codesign.connection.testActive(); - if (res.ok) { - pushToast({ variant: 'success', title: t('settings.providers.toast.connectionOk') }); - } else { + try { + const res = await window.codesign.connection.testActive(); + if (res.ok) { + pushToast({ variant: 'success', title: t('settings.providers.toast.connectionOk') }); + } else { + pushToast({ + variant: 'error', + title: t('settings.providers.toast.connectionFailed'), + description: res.hint || res.message, + }); + } + } catch (err) { pushToast({ variant: 'error', title: t('settings.providers.toast.connectionFailed'), - description: res.hint || res.message, + description: err instanceof Error ? err.message : t('settings.common.unknownError'), }); } } @@ -756,27 +764,32 @@ function ActiveModelSelector({ setPrimary(config.modelPrimary ?? sl.defaultPrimary); }, [config.modelPrimary, sl.defaultPrimary]); - async function save(next: string) { - if (!window.codesign) return; + async function save(next: string): Promise { + if (!window.codesign) return false; try { const updated = await window.codesign.settings.setActiveProvider({ provider, modelPrimary: next, }); setConfig(updated); + return true; } catch (err) { pushToast({ variant: 'error', title: t('settings.providers.toast.modelSaveFailed'), description: err instanceof Error ? err.message : t('settings.common.unknownError'), }); + return false; } } function handleChange(v: string) { + const prev = primary; setPrimary(v); setEditing(false); - void save(v); + void save(v).then((ok) => { + if (!ok) setPrimary(prev); + }); } return (