feat: enhance MCP server management and system settings

- Added functionality to save MCP server configurations as templates in the MCP Manager.
- Implemented new hooks for managing system settings including Chinese response, Windows platform, and Codex CLI enhancements.
- Updated API calls to support fetching and toggling system settings.
- Introduced UI components for displaying and managing response language settings and system status.
- Enhanced error handling and notifications for server deletion and template saving actions.
- Updated localization files for new settings and descriptions in English and Chinese.
This commit is contained in:
catlog22
2026-02-07 21:17:18 +08:00
parent d29527ae16
commit 2094c1085b
52 changed files with 2061 additions and 602 deletions

View File

@@ -20,6 +20,7 @@ import {
Trash2,
ChevronDown,
ChevronUp,
BookmarkPlus,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
@@ -29,20 +30,21 @@ import { McpServerDialog } from '@/components/mcp/McpServerDialog';
import { CliModeToggle, type CliMode } from '@/components/mcp/CliModeToggle';
import { CodexMcpEditableCard } from '@/components/mcp/CodexMcpEditableCard';
import { CcwToolsMcpCard } from '@/components/mcp/CcwToolsMcpCard';
import { McpTemplatesSection } from '@/components/mcp/McpTemplatesSection';
import { McpTemplatesSection, TemplateSaveDialog } from '@/components/mcp/McpTemplatesSection';
import { RecommendedMcpSection } from '@/components/mcp/RecommendedMcpSection';
import { WindowsCompatibilityWarning } from '@/components/mcp/WindowsCompatibilityWarning';
import { CrossCliCopyButton } from '@/components/mcp/CrossCliCopyButton';
import { AllProjectsTable } from '@/components/mcp/AllProjectsTable';
import { OtherProjectsSection } from '@/components/mcp/OtherProjectsSection';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { useMcpServers, useMcpServerMutations } from '@/hooks';
import { useMcpServers, useMcpServerMutations, useNotifications } from '@/hooks';
import {
fetchCodexMcpServers,
fetchCcwMcpConfig,
updateCcwConfig,
codexRemoveServer,
codexToggleServer,
saveMcpTemplate,
type McpServer,
type CcwMcpConfig,
} from '@/lib/api';
@@ -57,9 +59,10 @@ interface McpServerCardProps {
onToggle: (serverName: string, enabled: boolean) => void;
onEdit: (server: McpServer) => void;
onDelete: (server: McpServer) => void;
onSaveAsTemplate: (server: McpServer) => void;
}
function McpServerCard({ server, isExpanded, onToggleExpand, onToggle, onEdit, onDelete }: McpServerCardProps) {
function McpServerCard({ server, isExpanded, onToggleExpand, onToggle, onEdit, onDelete, onSaveAsTemplate }: McpServerCardProps) {
const { formatMessage } = useIntl();
return (
@@ -115,6 +118,18 @@ function McpServerCard({ server, isExpanded, onToggleExpand, onToggle, onEdit, o
>
{server.enabled ? <Power className="w-4 h-4 text-green-600" /> : <PowerOff className="w-4 h-4" />}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
onSaveAsTemplate(server);
}}
title={formatMessage({ id: 'mcp.templates.actions.saveAsTemplate' })}
>
<BookmarkPlus className="w-4 h-4 text-primary" />
</Button>
<Button
variant="ghost"
size="sm"
@@ -206,6 +221,10 @@ export function McpManagerPage() {
const [editingServer, setEditingServer] = useState<McpServer | undefined>(undefined);
const [cliMode, setCliMode] = useState<CliMode>('claude');
const [codexExpandedServers, setCodexExpandedServers] = useState<Set<string>>(new Set());
const [saveTemplateDialogOpen, setSaveTemplateDialogOpen] = useState(false);
const [serverToSaveAsTemplate, setServerToSaveAsTemplate] = useState<McpServer | undefined>(undefined);
const notifications = useNotifications();
const {
servers,
@@ -269,9 +288,22 @@ export function McpManagerPage() {
toggleServer(serverName, enabled);
};
const handleDelete = (server: McpServer) => {
const handleDelete = async (server: McpServer) => {
if (confirm(formatMessage({ id: 'mcp.deleteConfirm' }, { name: server.name }))) {
deleteServer(server.name, server.scope);
try {
await deleteServer(server.name, server.scope);
notifications.success(
formatMessage({ id: 'mcp.actions.delete' }),
server.name
);
refetch();
} catch (error) {
console.error('Failed to delete MCP server:', error);
notifications.error(
formatMessage({ id: 'mcp.actions.delete' }),
error instanceof Error ? error.message : String(error)
);
}
}
};
@@ -339,19 +371,64 @@ export function McpManagerPage() {
setDialogOpen(true);
};
const handleSaveAsTemplate = (serverName: string, config: { command: string; args: string[] }) => {
// This would open a dialog to save current server as template
// For now, just log it
console.log('Save as template:', serverName, config);
const handleSaveServerAsTemplate = (server: McpServer) => {
setServerToSaveAsTemplate(server);
setSaveTemplateDialogOpen(true);
};
const handleSaveAsTemplate = async (
name: string,
category: string,
description: string,
serverConfig: { command: string; args: string[]; env: Record<string, string> },
) => {
try {
const result = await saveMcpTemplate({
name,
description: description || undefined,
category: category || 'custom',
serverConfig: {
command: serverConfig.command,
args: serverConfig.args.length > 0 ? serverConfig.args : undefined,
env: Object.keys(serverConfig.env).length > 0 ? serverConfig.env : undefined,
},
});
if (result.success) {
notifications.success(
formatMessage({ id: 'mcp.templates.feedback.saveSuccess' }),
name
);
setSaveTemplateDialogOpen(false);
setServerToSaveAsTemplate(undefined);
} else {
notifications.error(
formatMessage({ id: 'mcp.templates.feedback.saveError' }),
result.error || ''
);
}
} catch (error) {
notifications.error(
formatMessage({ id: 'mcp.templates.feedback.saveError' }),
error instanceof Error ? error.message : String(error)
);
}
};
// Codex MCP handlers
const handleCodexRemove = async (serverName: string) => {
try {
await codexRemoveServer(serverName);
notifications.success(
formatMessage({ id: 'mcp.actions.delete' }),
serverName
);
codexQuery.refetch();
} catch (error) {
console.error('Failed to remove Codex MCP server:', error);
notifications.error(
formatMessage({ id: 'mcp.actions.delete' }),
error instanceof Error ? error.message : String(error)
);
}
};
@@ -592,6 +669,7 @@ export function McpManagerPage() {
onToggle={handleToggle}
onEdit={handleEdit}
onDelete={handleDelete}
onSaveAsTemplate={handleSaveServerAsTemplate}
/>
)
))}
@@ -646,6 +724,20 @@ export function McpManagerPage() {
onSave={handleDialogSave}
/>
)}
{/* Save as Template Dialog */}
<TemplateSaveDialog
open={saveTemplateDialogOpen}
onClose={() => {
setSaveTemplateDialogOpen(false);
setServerToSaveAsTemplate(undefined);
}}
onSave={handleSaveAsTemplate}
defaultName={serverToSaveAsTemplate?.name}
defaultCommand={serverToSaveAsTemplate?.command}
defaultArgs={serverToSaveAsTemplate?.args}
defaultEnv={serverToSaveAsTemplate?.env as Record<string, string>}
/>
</div>
);
}

View File

@@ -18,6 +18,10 @@ import {
ChevronUp,
Languages,
Plus,
MessageSquareText,
Monitor,
Terminal,
AlertTriangle,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
@@ -29,6 +33,17 @@ import { useConfigStore, selectCliTools, selectDefaultCliTool, selectUserPrefere
import type { CliToolConfig, UserPreferences } from '@/types/store';
import { cn } from '@/lib/utils';
import { LanguageSwitcher } from '@/components/layout/LanguageSwitcher';
import {
useChineseResponseStatus,
useToggleChineseResponse,
useWindowsPlatformStatus,
useToggleWindowsPlatform,
useCodexCliEnhancementStatus,
useToggleCodexCliEnhancement,
useRefreshCodexCliEnhancement,
useCcwInstallStatus,
useCliToolStatus,
} from '@/hooks/useSystemSettings';
// ========== CLI Tool Card Component ==========
@@ -37,6 +52,7 @@ interface CliToolCardProps {
config: CliToolConfig;
isDefault: boolean;
isExpanded: boolean;
toolAvailable?: boolean;
onToggleExpand: () => void;
onToggleEnabled: () => void;
onSetDefault: () => void;
@@ -51,6 +67,7 @@ function CliToolCard({
config,
isDefault,
isExpanded,
toolAvailable,
onToggleExpand,
onToggleEnabled,
onSetDefault,
@@ -125,6 +142,12 @@ function CliToolCard({
<Badge variant="default" className="text-xs">{formatMessage({ id: 'settings.cliTools.default' })}</Badge>
)}
<Badge variant="outline" className="text-xs">{config.type}</Badge>
{toolAvailable !== undefined && (
<span className={cn(
'inline-block w-2 h-2 rounded-full',
toolAvailable ? 'bg-green-500' : 'bg-red-400'
)} title={toolAvailable ? 'Available' : 'Unavailable'} />
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5">
{config.primaryModel}
@@ -345,6 +368,266 @@ function CliToolCard({
);
}
// ========== Response Language Section ==========
function ResponseLanguageSection() {
const { formatMessage } = useIntl();
const { data: chineseStatus, isLoading: chineseLoading } = useChineseResponseStatus();
const { toggle: toggleChinese, isPending: chineseToggling } = useToggleChineseResponse();
const { data: windowsStatus, isLoading: windowsLoading } = useWindowsPlatformStatus();
const { toggle: toggleWindows, isPending: windowsToggling } = useToggleWindowsPlatform();
const { data: cliEnhStatus, isLoading: cliEnhLoading } = useCodexCliEnhancementStatus();
const { toggle: toggleCliEnh, isPending: cliEnhToggling } = useToggleCodexCliEnhancement();
const { refresh: refreshCliEnh, isPending: refreshing } = useRefreshCodexCliEnhancement();
return (
<Card className="p-6">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
<MessageSquareText className="w-5 h-5" />
{formatMessage({ id: 'settings.sections.responseLanguage' })}
</h2>
<div className="grid gap-4 md:grid-cols-2">
{/* Chinese Response - Claude */}
<div className="rounded-lg border border-border p-4 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{formatMessage({ id: 'settings.responseLanguage.chineseClaude' })}</span>
<Badge variant="default" className="text-xs">Claude</Badge>
</div>
<Button
variant={chineseStatus?.claudeEnabled ? 'default' : 'outline'}
size="sm"
className="h-7"
disabled={chineseLoading || chineseToggling}
onClick={() => toggleChinese(!chineseStatus?.claudeEnabled, 'claude')}
>
{chineseStatus?.claudeEnabled
? formatMessage({ id: 'settings.responseLanguage.enabled' })
: formatMessage({ id: 'settings.responseLanguage.disabled' })}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.responseLanguage.chineseClaudeDesc' })}
</p>
</div>
{/* Chinese Response - Codex */}
<div className="rounded-lg border border-border p-4 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{formatMessage({ id: 'settings.responseLanguage.chineseCodex' })}</span>
<Badge variant="secondary" className="text-xs">Codex</Badge>
</div>
<Button
variant={chineseStatus?.codexEnabled ? 'default' : 'outline'}
size="sm"
className="h-7"
disabled={chineseLoading || chineseToggling}
onClick={() => toggleChinese(!chineseStatus?.codexEnabled, 'codex')}
>
{chineseStatus?.codexEnabled
? formatMessage({ id: 'settings.responseLanguage.enabled' })
: formatMessage({ id: 'settings.responseLanguage.disabled' })}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.responseLanguage.chineseCodexDesc' })}
</p>
{chineseStatus?.codexNeedsMigration && (
<p className="text-xs text-yellow-500">
<AlertTriangle className="w-3 h-3 inline mr-1" />
{formatMessage({ id: 'settings.responseLanguage.migrationWarning' })}
</p>
)}
</div>
{/* Windows Platform */}
<div className="rounded-lg border border-border p-4 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Monitor className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">{formatMessage({ id: 'settings.responseLanguage.windowsPlatform' })}</span>
</div>
<Button
variant={windowsStatus?.enabled ? 'default' : 'outline'}
size="sm"
className="h-7"
disabled={windowsLoading || windowsToggling}
onClick={() => toggleWindows(!windowsStatus?.enabled)}
>
{windowsStatus?.enabled
? formatMessage({ id: 'settings.responseLanguage.enabled' })
: formatMessage({ id: 'settings.responseLanguage.disabled' })}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.responseLanguage.windowsPlatformDesc' })}
</p>
</div>
{/* CLI Enhancement - Codex */}
<div className="rounded-lg border border-border p-4 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">{formatMessage({ id: 'settings.responseLanguage.cliEnhancement' })}</span>
<Badge variant="secondary" className="text-xs">Codex</Badge>
</div>
<div className="flex items-center gap-1">
{cliEnhStatus?.enabled && (
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
disabled={cliEnhLoading || refreshing}
onClick={() => refreshCliEnh()}
title={formatMessage({ id: 'settings.responseLanguage.refreshConfig' })}
>
<RefreshCw className={cn('w-3.5 h-3.5', refreshing && 'animate-spin')} />
</Button>
)}
<Button
variant={cliEnhStatus?.enabled ? 'default' : 'outline'}
size="sm"
className="h-7"
disabled={cliEnhLoading || cliEnhToggling}
onClick={() => toggleCliEnh(!cliEnhStatus?.enabled)}
>
{cliEnhStatus?.enabled
? formatMessage({ id: 'settings.responseLanguage.enabled' })
: formatMessage({ id: 'settings.responseLanguage.disabled' })}
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.responseLanguage.cliEnhancementDesc' })}
</p>
{cliEnhStatus?.enabled && (
<p className="text-xs text-muted-foreground/70">
{formatMessage({ id: 'settings.responseLanguage.cliEnhancementHint' })}
</p>
)}
</div>
</div>
</Card>
);
}
// ========== System Status Section ==========
function SystemStatusSection() {
const { formatMessage } = useIntl();
const { data: ccwInstall, isLoading: installLoading } = useCcwInstallStatus();
// Don't show if installed or still loading
if (installLoading || ccwInstall?.installed) return null;
return (
<Card className="p-6 border-yellow-500/50">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
<AlertTriangle className="w-5 h-5 text-yellow-500" />
{formatMessage({ id: 'settings.systemStatus.title' })}
</h2>
{/* CCW Install Status */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{formatMessage({ id: 'settings.systemStatus.ccwInstall' })}</span>
<Badge variant="outline" className="text-yellow-500 border-yellow-500/50">
{formatMessage({ id: 'settings.systemStatus.incomplete' })}
</Badge>
</div>
{ccwInstall && ccwInstall.missingFiles.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.systemStatus.missingFiles' })}:
</p>
<ul className="text-xs text-muted-foreground/70 list-disc list-inside">
{ccwInstall.missingFiles.slice(0, 5).map((file) => (
<li key={file}>{file}</li>
))}
{ccwInstall.missingFiles.length > 5 && (
<li>+{ccwInstall.missingFiles.length - 5} more...</li>
)}
</ul>
<div className="bg-muted/50 rounded-md p-3 mt-2">
<p className="text-xs font-medium mb-1">
{formatMessage({ id: 'settings.systemStatus.runToFix' })}:
</p>
<code className="text-xs bg-background px-2 py-1 rounded block font-mono">
ccw install
</code>
</div>
</div>
)}
</div>
</Card>
);
}
// ========== CLI Tools with Status Enhancement ==========
interface CliToolsWithStatusProps {
cliTools: Record<string, CliToolConfig>;
defaultCliTool: string;
expandedTools: Set<string>;
onToggleExpand: (toolId: string) => void;
onToggleEnabled: (toolId: string) => void;
onSetDefault: (toolId: string) => void;
onUpdateModel: (toolId: string, field: 'primaryModel' | 'secondaryModel', value: string) => void;
onUpdateTags: (toolId: string, tags: string[]) => void;
onUpdateAvailableModels: (toolId: string, models: string[]) => void;
onUpdateSettingsFile: (toolId: string, settingsFile: string | undefined) => void;
formatMessage: ReturnType<typeof useIntl>['formatMessage'];
}
function CliToolsWithStatus({
cliTools,
defaultCliTool,
expandedTools,
onToggleExpand,
onToggleEnabled,
onSetDefault,
onUpdateModel,
onUpdateTags,
onUpdateAvailableModels,
onUpdateSettingsFile,
formatMessage,
}: CliToolsWithStatusProps) {
const { data: toolStatus } = useCliToolStatus();
return (
<>
<p className="text-sm text-muted-foreground mb-4">
{formatMessage({ id: 'settings.cliTools.description' })} <strong className="text-foreground">{defaultCliTool}</strong>
</p>
<div className="space-y-3">
{Object.entries(cliTools).map(([toolId, config]) => {
const status = toolStatus?.[toolId];
return (
<CliToolCard
key={toolId}
toolId={toolId}
config={config}
isDefault={toolId === defaultCliTool}
isExpanded={expandedTools.has(toolId)}
toolAvailable={status?.available}
onToggleExpand={() => onToggleExpand(toolId)}
onToggleEnabled={() => onToggleEnabled(toolId)}
onSetDefault={() => onSetDefault(toolId)}
onUpdateModel={(field, value) => onUpdateModel(toolId, field, value)}
onUpdateTags={(tags) => onUpdateTags(toolId, tags)}
onUpdateAvailableModels={(models) => onUpdateAvailableModels(toolId, models)}
onUpdateSettingsFile={(settingsFile) => onUpdateSettingsFile(toolId, settingsFile)}
/>
);
})}
</div>
</>
);
}
// ========== Main Page Component ==========
export function SettingsPage() {
@@ -466,33 +749,31 @@ export function SettingsPage() {
</div>
</Card>
{/* Response Language Settings */}
<ResponseLanguageSection />
{/* System Status */}
<SystemStatusSection />
{/* CLI Tools Configuration */}
<Card className="p-6">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
<Cpu className="w-5 h-5" />
{formatMessage({ id: 'settings.sections.cliTools' })}
</h2>
<p className="text-sm text-muted-foreground mb-4">
{formatMessage({ id: 'settings.cliTools.description' })} <strong className="text-foreground">{defaultCliTool}</strong>
</p>
<div className="space-y-3">
{Object.entries(cliTools).map(([toolId, config]) => (
<CliToolCard
key={toolId}
toolId={toolId}
config={config}
isDefault={toolId === defaultCliTool}
isExpanded={expandedTools.has(toolId)}
onToggleExpand={() => toggleToolExpand(toolId)}
onToggleEnabled={() => handleToggleToolEnabled(toolId)}
onSetDefault={() => handleSetDefaultTool(toolId)}
onUpdateModel={(field, value) => handleUpdateModel(toolId, field, value)}
onUpdateTags={(tags) => handleUpdateTags(toolId, tags)}
onUpdateAvailableModels={(models) => handleUpdateAvailableModels(toolId, models)}
onUpdateSettingsFile={(settingsFile) => handleUpdateSettingsFile(toolId, settingsFile)}
/>
))}
</div>
<CliToolsWithStatus
cliTools={cliTools}
defaultCliTool={defaultCliTool}
expandedTools={expandedTools}
onToggleExpand={toggleToolExpand}
onToggleEnabled={handleToggleToolEnabled}
onSetDefault={handleSetDefaultTool}
onUpdateModel={handleUpdateModel}
onUpdateTags={handleUpdateTags}
onUpdateAvailableModels={handleUpdateAvailableModels}
onUpdateSettingsFile={handleUpdateSettingsFile}
formatMessage={formatMessage}
/>
</Card>
{/* Data Refresh Settings */}

View File

@@ -38,7 +38,7 @@ import {
AlertDialogAction,
AlertDialogCancel,
} from '@/components/ui';
import { SkillCard, SkillDetailPanel } from '@/components/shared';
import { SkillCard, SkillDetailPanel, SkillCreateDialog } from '@/components/shared';
import { useSkills, useSkillMutations } from '@/hooks';
import { fetchSkillDetail } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -118,6 +118,9 @@ export function SkillsManagerPage() {
const [confirmDisable, setConfirmDisable] = useState<{ skill: Skill; enable: boolean } | null>(null);
const [locationFilter, setLocationFilter] = useState<'project' | 'user'>('project');
// Skill create dialog state
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
// Skill detail panel state
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
const [isDetailLoading, setIsDetailLoading] = useState(false);
@@ -243,7 +246,7 @@ export function SkillsManagerPage() {
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
{formatMessage({ id: 'skills.actions.install' })}
</Button>
@@ -470,6 +473,13 @@ export function SkillsManagerPage() {
onClose={handleCloseDetailPanel}
isLoading={isDetailLoading}
/>
{/* Skill Create Dialog */}
<SkillCreateDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
onCreated={() => refetch()}
/>
</div>
);
}