feat(mcp): add overwrite warning and improve sync option visibility

- Move sync checkbox to footer alongside save/cancel buttons for better visibility
- Add real-time conflict detection to check for duplicate MCP IDs on other side
- Display amber warning icon when sync would overwrite existing config
- Add i18n keys for overwrite warning (zh: "将覆盖 {{target}} 中的同名配置")
- Update UI layout to use justify-between for better spacing
This commit is contained in:
Jason
2025-10-14 10:22:57 +08:00
parent a2aa5f8434
commit 5427ae04e4
3 changed files with 80 additions and 45 deletions

View File

@@ -1,6 +1,6 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { X, Save, AlertCircle, ChevronDown, ChevronUp } from "lucide-react"; import { X, Save, AlertCircle, ChevronDown, ChevronUp, AlertTriangle } from "lucide-react";
import { McpServer, McpServerSpec } from "../../types"; import { McpServer, McpServerSpec } from "../../types";
import { import {
mcpPresets, mcpPresets,
@@ -118,16 +118,40 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
const [isWizardOpen, setIsWizardOpen] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false);
const [idError, setIdError] = useState(""); const [idError, setIdError] = useState("");
const [syncOtherSide, setSyncOtherSide] = useState(false); const [syncOtherSide, setSyncOtherSide] = useState(false);
const [otherSideHasConflict, setOtherSideHasConflict] = useState(false);
// 判断是否使用 TOML 格式 // 判断是否使用 TOML 格式
const useToml = appType === "codex"; const useToml = appType === "codex";
const syncTargetLabel = const syncTargetLabel =
appType === "claude" ? t("apps.codex") : t("apps.claude"); appType === "claude" ? t("apps.codex") : t("apps.claude");
const otherAppType: AppType = appType === "claude" ? "codex" : "claude";
const syncCheckboxId = useMemo( const syncCheckboxId = useMemo(
() => `sync-other-side-${appType}`, () => `sync-other-side-${appType}`,
[appType], [appType],
); );
// 检测另一侧是否有同名 MCP
useEffect(() => {
const checkOtherSide = async () => {
const currentId = formId.trim();
if (!currentId) {
setOtherSideHasConflict(false);
return;
}
try {
const otherConfig = await window.api.getMcpConfig(otherAppType);
const hasConflict = Object.keys(otherConfig.servers || {}).includes(currentId);
setOtherSideHasConflict(hasConflict);
} catch (error) {
console.error("检查另一侧 MCP 配置失败:", error);
setOtherSideHasConflict(false);
}
};
checkOtherSide();
}, [formId, otherAppType]);
const wizardInitialSpec = useMemo(() => { const wizardInitialSpec = useMemo(() => {
const fallback = initialData?.server; const fallback = initialData?.server;
if (!formConfig.trim()) { if (!formConfig.trim()) {
@@ -666,32 +690,40 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</div> </div>
)} )}
</div> </div>
</div>
{/* Footer */}
<div className="flex-shrink-0 flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
{/* 双端同步选项 */} {/* 双端同步选项 */}
<div className="mt-4 flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900/40"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<input <input
id={syncCheckboxId} id={syncCheckboxId}
type="checkbox" type="checkbox"
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800" className="h-4 w-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800"
checked={syncOtherSide} checked={syncOtherSide}
onChange={(event) => setSyncOtherSide(event.target.checked)} onChange={(event) => setSyncOtherSide(event.target.checked)}
/> />
<label <label
htmlFor={syncCheckboxId} htmlFor={syncCheckboxId}
className="text-sm text-gray-700 dark:text-gray-300" className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
title={t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
> >
<span className="font-medium">
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })} {t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
</span>
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
{t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
</span>
</label> </label>
</div> </div>
{syncOtherSide && otherSideHasConflict && (
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
<AlertTriangle size={14} />
<span className="text-xs font-medium">
{t("mcp.form.willOverwriteWarning", { target: syncTargetLabel })}
</span>
</div>
)}
</div> </div>
{/* Footer */} {/* 操作按钮 */}
<div className="flex-shrink-0 flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800"> <div className="flex items-center gap-3">
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium" className="px-4 py-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium"
@@ -712,6 +744,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</button> </button>
</div> </div>
</div> </div>
</div>
{/* Wizard Modal */} {/* Wizard Modal */}
<McpWizardModal <McpWizardModal

View File

@@ -300,7 +300,8 @@
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]", "tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
"useWizard": "Config Wizard", "useWizard": "Config Wizard",
"syncOtherSide": "Mirror to {{target}}", "syncOtherSide": "Mirror to {{target}}",
"syncOtherSideHint": "Apply the same settings to {{target}}; existing entries with the same id will be overwritten." "syncOtherSideHint": "Apply the same settings to {{target}}; existing entries with the same id will be overwritten.",
"willOverwriteWarning": "Will overwrite existing config in {{target}}"
}, },
"wizard": { "wizard": {
"title": "MCP Configuration Wizard", "title": "MCP Configuration Wizard",

View File

@@ -300,7 +300,8 @@
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]", "tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
"useWizard": "配置向导", "useWizard": "配置向导",
"syncOtherSide": "同步到 {{target}}", "syncOtherSide": "同步到 {{target}}",
"syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖" "syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖",
"willOverwriteWarning": "将覆盖 {{target}} 中的同名配置"
}, },
"wizard": { "wizard": {
"title": "MCP 配置向导", "title": "MCP 配置向导",