mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user