- feat(mcp): unify notifications via onNotify in form and wizard
- 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
This commit is contained in:
@@ -354,7 +354,6 @@ function App() {
|
||||
onSwitch={handleSwitchProvider}
|
||||
onDelete={handleDeleteProvider}
|
||||
onEdit={setEditingProviderId}
|
||||
appType={activeApp}
|
||||
onNotify={showNotification}
|
||||
/>
|
||||
</div>
|
||||
@@ -392,6 +391,7 @@ function App() {
|
||||
<SettingsModal
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
onImportSuccess={handleImportSuccess}
|
||||
onNotify={showNotification}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,10 +17,15 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title =
|
||||
appType === "claude"
|
||||
? t("provider.addClaudeProvider")
|
||||
: t("provider.addCodexProvider");
|
||||
|
||||
return (
|
||||
<ProviderForm
|
||||
appType={appType}
|
||||
title={t("provider.addNewProvider")}
|
||||
title={title}
|
||||
submitText={t("common.add")}
|
||||
showPresets={true}
|
||||
onSubmit={onAdd}
|
||||
|
||||
@@ -18,7 +18,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [effectiveProvider, setEffectiveProvider] = useState<Provider>(provider);
|
||||
const [effectiveProvider, setEffectiveProvider] =
|
||||
useState<Provider>(provider);
|
||||
|
||||
// 若为当前应用且正在编辑“当前供应商”,则优先读取 live 配置作为初始值(Claude/Codex 均适用)
|
||||
useEffect(() => {
|
||||
@@ -51,10 +52,15 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const title =
|
||||
appType === "claude"
|
||||
? t("provider.editClaudeProvider")
|
||||
: t("provider.editCodexProvider");
|
||||
|
||||
return (
|
||||
<ProviderForm
|
||||
appType={appType}
|
||||
title={t("common.edit")}
|
||||
title={title}
|
||||
submitText={t("common.save")}
|
||||
initialData={effectiveProvider}
|
||||
showPresets={false}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Provider } from "../types";
|
||||
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
|
||||
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
// 不再在列表中显示分类徽章,避免造成困惑
|
||||
|
||||
interface ProviderListProps {
|
||||
@@ -12,7 +11,6 @@ interface ProviderListProps {
|
||||
onSwitch: (id: string) => 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<ProviderListProps> = ({
|
||||
onSwitch,
|
||||
onDelete,
|
||||
onEdit,
|
||||
appType,
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
@@ -55,6 +52,11 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
await window.api.openExternal(url);
|
||||
} catch (error) {
|
||||
console.error(t("console.openLinkFailed"), error);
|
||||
onNotify?.(
|
||||
`${t("console.openLinkFailed")}: ${String(error)}`,
|
||||
"error",
|
||||
4000,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,11 +24,17 @@ import { isLinux } from "../lib/platform";
|
||||
interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
onImportSuccess?: () => void | Promise<void>;
|
||||
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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<void>;
|
||||
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<McpFormModalProps> = ({
|
||||
appType,
|
||||
editingId,
|
||||
initialData,
|
||||
onSave,
|
||||
onClose,
|
||||
existingIds = [],
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [formId, setFormId] = useState(editingId || "");
|
||||
@@ -111,7 +120,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
|
||||
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<McpFormModalProps> = ({
|
||||
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<McpFormModalProps> = ({
|
||||
|
||||
// 前置必填校验,避免后端拒绝后才提示
|
||||
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<McpFormModalProps> = ({
|
||||
// 提取后端错误信息(支持 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 (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
@@ -189,7 +206,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{isEditing ? t("mcp.editServer") : t("mcp.addServer")}
|
||||
{getFormTitle()}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -289,6 +306,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
isOpen={isWizardOpen}
|
||||
onClose={() => setIsWizardOpen(false)}
|
||||
onApply={handleWizardApply}
|
||||
onNotify={onNotify}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -170,6 +170,9 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
||||
|
||||
const serverEntries = useMemo(() => Object.entries(servers), [servers]);
|
||||
|
||||
const panelTitle =
|
||||
appType === "claude" ? t("mcp.claudeTitle") : t("mcp.codexTitle");
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
@@ -183,8 +186,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{t("mcp.title")} ·{" "}
|
||||
{t(appType === "claude" ? "apps.claude" : "apps.codex")}
|
||||
{panelTitle}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -298,11 +300,13 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
||||
{/* Form Modal */}
|
||||
{isFormOpen && (
|
||||
<McpFormModal
|
||||
appType={appType}
|
||||
editingId={editingId || undefined}
|
||||
initialData={editingId ? servers[editingId] : undefined}
|
||||
existingIds={Object.keys(servers)}
|
||||
onSave={handleSave}
|
||||
onClose={handleCloseForm}
|
||||
onNotify={onNotify}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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<McpWizardModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onApply,
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio");
|
||||
@@ -124,11 +130,11 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
||||
|
||||
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<McpWizardModalProps> = ({
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -321,7 +326,6 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user