From e88562be987457455f64e6d15662042c9286a9a1 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 10 Oct 2025 20:52:16 +0800 Subject: [PATCH] - feat(mcp): unify notifications via onNotify in form and wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor(mcp): remove HTML5 required to avoid native popups - refactor(ui): propagate onNotify from App → McpPanel → McpFormModal → McpWizardModal - feat(settings): use onNotify for export and file-selection feedback - fix(ui): notify link-open failures via onNotify; remove unused appType prop from ProviderList - chore: format codebase and ensure typecheck passes --- src/App.tsx | 2 +- src/components/AddProviderModal.tsx | 7 ++++++- src/components/EditProviderModal.tsx | 10 +++++++-- src/components/ProviderList.tsx | 8 ++++--- src/components/SettingsModal.tsx | 24 ++++++++++++++++++--- src/components/mcp/McpFormModal.tsx | 30 +++++++++++++++++++++------ src/components/mcp/McpPanel.tsx | 8 +++++-- src/components/mcp/McpWizardModal.tsx | 12 +++++++---- src/i18n/locales/en.json | 10 +++++++++ src/i18n/locales/zh.json | 10 +++++++++ 10 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7b96058..74a793c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -354,7 +354,6 @@ function App() { onSwitch={handleSwitchProvider} onDelete={handleDeleteProvider} onEdit={setEditingProviderId} - appType={activeApp} onNotify={showNotification} /> @@ -392,6 +391,7 @@ function App() { setIsSettingsOpen(false)} onImportSuccess={handleImportSuccess} + onNotify={showNotification} /> )} diff --git a/src/components/AddProviderModal.tsx b/src/components/AddProviderModal.tsx index 8d92753..3b443ec 100644 --- a/src/components/AddProviderModal.tsx +++ b/src/components/AddProviderModal.tsx @@ -17,10 +17,15 @@ const AddProviderModal: React.FC = ({ }) => { const { t } = useTranslation(); + const title = + appType === "claude" + ? t("provider.addClaudeProvider") + : t("provider.addCodexProvider"); + return ( = ({ onClose, }) => { const { t } = useTranslation(); - const [effectiveProvider, setEffectiveProvider] = useState(provider); + const [effectiveProvider, setEffectiveProvider] = + useState(provider); // 若为当前应用且正在编辑“当前供应商”,则优先读取 live 配置作为初始值(Claude/Codex 均适用) useEffect(() => { @@ -51,10 +52,15 @@ const EditProviderModal: React.FC = ({ }); }; + const title = + appType === "claude" + ? t("provider.editClaudeProvider") + : t("provider.editCodexProvider"); + return ( void; onDelete: (id: string) => void; onEdit: (id: string) => void; - appType?: AppType; onNotify?: ( message: string, type: "success" | "error", @@ -26,7 +24,6 @@ const ProviderList: React.FC = ({ onSwitch, onDelete, onEdit, - appType, onNotify, }) => { const { t, i18n } = useTranslation(); @@ -55,6 +52,11 @@ const ProviderList: React.FC = ({ await window.api.openExternal(url); } catch (error) { console.error(t("console.openLinkFailed"), error); + onNotify?.( + `${t("console.openLinkFailed")}: ${String(error)}`, + "error", + 4000, + ); } }; diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 4ee1529..c1b4a1d 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -24,11 +24,17 @@ import { isLinux } from "../lib/platform"; interface SettingsModalProps { onClose: () => void; onImportSuccess?: () => void | Promise; + onNotify?: ( + message: string, + type: "success" | "error", + duration?: number, + ) => void; } export default function SettingsModal({ onClose, onImportSuccess, + onNotify, }: SettingsModalProps) { const { t, i18n } = useTranslation(); @@ -387,11 +393,19 @@ export default function SettingsModal({ const result = await window.api.exportConfigToFile(filePath); if (result.success) { - alert(`${t("settings.configExported")}\n${result.filePath}`); + onNotify?.( + `${t("settings.configExported")}\n${result.filePath}`, + "success", + 4000, + ); } } catch (error) { console.error(t("settings.exportFailedError"), error); - alert(`${t("settings.exportFailed")}: ${error}`); + onNotify?.( + `${t("settings.exportFailed")}: ${String(error)}`, + "error", + 5000, + ); } }; @@ -406,7 +420,11 @@ export default function SettingsModal({ } } catch (error) { console.error(t("settings.selectFileFailed") + ":", error); - alert(`${t("settings.selectFileFailed")}: ${error}`); + onNotify?.( + `${t("settings.selectFileFailed")}: ${String(error)}`, + "error", + 5000, + ); } }; diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index ec5965d..5a2522b 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -5,13 +5,20 @@ import { McpServer } from "../../types"; import { buttonStyles, inputStyles } from "../../lib/styles"; import McpWizardModal from "./McpWizardModal"; import { extractErrorMessage } from "../../utils/errorUtils"; +import { AppType } from "../../lib/tauri-api"; interface McpFormModalProps { + appType: AppType; editingId?: string; initialData?: McpServer; onSave: (id: string, server: McpServer) => Promise; onClose: () => void; existingIds?: string[]; + onNotify?: ( + message: string, + type: "success" | "error", + duration?: number, + ) => void; } /** @@ -35,11 +42,13 @@ const validateJson = (text: string): string => { * 仅包含:标题(必填)、描述(可选)、JSON 配置(可选,带格式校验) */ const McpFormModal: React.FC = ({ + appType, editingId, initialData, onSave, onClose, existingIds = [], + onNotify, }) => { const { t } = useTranslation(); const [formId, setFormId] = useState(editingId || ""); @@ -111,7 +120,7 @@ const McpFormModal: React.FC = ({ const handleSubmit = async () => { if (!formId.trim()) { - alert(t("mcp.error.idRequired")); + onNotify?.(t("mcp.error.idRequired"), "error", 3000); return; } @@ -125,7 +134,7 @@ const McpFormModal: React.FC = ({ const currentJsonError = validateJson(formJson); setJsonError(currentJsonError); if (currentJsonError) { - alert(t("mcp.error.jsonInvalid")); + onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000); return; } @@ -138,11 +147,11 @@ const McpFormModal: React.FC = ({ // 前置必填校验,避免后端拒绝后才提示 if (server?.type === "stdio" && !server?.command?.trim()) { - alert(t("mcp.error.commandRequired")); + onNotify?.(t("mcp.error.commandRequired"), "error", 3000); return; } if (server?.type === "http" && !server?.url?.trim()) { - alert(t("mcp.wizard.urlRequired")); + onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000); return; } } else { @@ -170,12 +179,20 @@ const McpFormModal: React.FC = ({ // 提取后端错误信息(支持 string / {message} / tauri payload) const detail = extractErrorMessage(error); const msg = detail || t("mcp.error.saveFailed"); - alert(msg); + onNotify?.(msg, "error", detail ? 6000 : 4000); } finally { setSaving(false); } }; + const getFormTitle = () => { + if (appType === "claude") { + return isEditing ? t("mcp.editClaudeServer") : t("mcp.addClaudeServer"); + } else { + return isEditing ? t("mcp.editCodexServer") : t("mcp.addCodexServer"); + } + }; + return (
{/* Backdrop */} @@ -189,7 +206,7 @@ const McpFormModal: React.FC = ({ {/* Header */}

- {isEditing ? t("mcp.editServer") : t("mcp.addServer")} + {getFormTitle()}

); diff --git a/src/components/mcp/McpPanel.tsx b/src/components/mcp/McpPanel.tsx index 5fd21b1..712a54c 100644 --- a/src/components/mcp/McpPanel.tsx +++ b/src/components/mcp/McpPanel.tsx @@ -170,6 +170,9 @@ const McpPanel: React.FC = ({ onClose, onNotify, appType }) => { const serverEntries = useMemo(() => Object.entries(servers), [servers]); + const panelTitle = + appType === "claude" ? t("mcp.claudeTitle") : t("mcp.codexTitle"); + return (
{/* Backdrop */} @@ -183,8 +186,7 @@ const McpPanel: React.FC = ({ onClose, onNotify, appType }) => { {/* Header */}

- {t("mcp.title")} ·{" "} - {t(appType === "claude" ? "apps.claude" : "apps.codex")} + {panelTitle}

@@ -298,11 +300,13 @@ const McpPanel: React.FC = ({ onClose, onNotify, appType }) => { {/* Form Modal */} {isFormOpen && ( )} diff --git a/src/components/mcp/McpWizardModal.tsx b/src/components/mcp/McpWizardModal.tsx index 2536f3e..c76ec8b 100644 --- a/src/components/mcp/McpWizardModal.tsx +++ b/src/components/mcp/McpWizardModal.tsx @@ -8,6 +8,11 @@ interface McpWizardModalProps { isOpen: boolean; onClose: () => void; onApply: (json: string) => void; + onNotify?: ( + message: string, + type: "success" | "error", + duration?: number, + ) => void; } /** @@ -66,6 +71,7 @@ const McpWizardModal: React.FC = ({ isOpen, onClose, onApply, + onNotify, }) => { const { t } = useTranslation(); const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio"); @@ -124,11 +130,11 @@ const McpWizardModal: React.FC = ({ const handleApply = () => { if (wizardType === "stdio" && !wizardCommand.trim()) { - alert(t("mcp.error.commandRequired")); + onNotify?.(t("mcp.error.commandRequired"), "error", 3000); return; } if (wizardType === "http" && !wizardUrl.trim()) { - alert(t("mcp.wizard.urlRequired")); + onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000); return; } @@ -256,7 +262,6 @@ const McpWizardModal: React.FC = ({ onChange={(e) => setWizardCommand(e.target.value)} onKeyDown={handleKeyDown} placeholder={t("mcp.wizard.commandPlaceholder")} - required className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" />
@@ -321,7 +326,6 @@ const McpWizardModal: React.FC = ({ onChange={(e) => setWizardUrl(e.target.value)} onKeyDown={handleKeyDown} placeholder={t("mcp.wizard.urlPlaceholder")} - required className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" />
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b744281..3b099b3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -55,6 +55,10 @@ "editProvider": "Edit Provider", "deleteProvider": "Delete Provider", "addNewProvider": "Add New Provider", + "addClaudeProvider": "Add Claude Code Provider", + "addCodexProvider": "Add Codex Provider", + "editClaudeProvider": "Edit Claude Code Provider", + "editCodexProvider": "Edit Codex Provider", "configError": "Configuration Error", "notConfigured": "Not configured for official website", "applyToClaudePlugin": "Apply to Claude plugin", @@ -254,6 +258,8 @@ }, "mcp": { "title": "MCP Management", + "claudeTitle": "Claude Code MCP Management", + "codexTitle": "Codex MCP Management", "userLevelPath": "User-level MCP path", "serverList": "Servers", "loading": "Loading...", @@ -262,6 +268,10 @@ "add": "Add MCP", "addServer": "Add MCP", "editServer": "Edit MCP", + "addClaudeServer": "Add Claude Code MCP", + "editClaudeServer": "Edit Claude Code MCP", + "addCodexServer": "Add Codex MCP", + "editCodexServer": "Edit Codex MCP", "configPath": "Config Path", "serverCount": "{{count}} MCP server(s) configured", "template": { diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 12f9e1a..c676a97 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -55,6 +55,10 @@ "editProvider": "编辑供应商", "deleteProvider": "删除供应商", "addNewProvider": "添加新供应商", + "addClaudeProvider": "添加 Claude Code 供应商", + "addCodexProvider": "添加 Codex 供应商", + "editClaudeProvider": "编辑 Claude Code 供应商", + "editCodexProvider": "编辑 Codex 供应商", "configError": "配置错误", "notConfigured": "未配置官网地址", "applyToClaudePlugin": "应用到 Claude 插件", @@ -254,6 +258,8 @@ }, "mcp": { "title": "MCP 管理", + "claudeTitle": "Claude Code MCP 管理", + "codexTitle": "Codex MCP 管理", "userLevelPath": "用户级 MCP 配置路径", "serverList": "服务器列表", "loading": "加载中...", @@ -262,6 +268,10 @@ "add": "添加 MCP", "addServer": "新增 MCP", "editServer": "编辑 MCP", + "addClaudeServer": "新增 Claude Code MCP", + "editClaudeServer": "编辑 Claude Code MCP", + "addCodexServer": "新增 Codex MCP", + "editCodexServer": "编辑 Codex MCP", "configPath": "配置路径", "serverCount": "已配置 {{count}} 个 MCP 服务器", "template": {