feat: add mcp dialog

This commit is contained in:
LaZzyMan 2026-02-13 14:38:54 +08:00
parent 51fdf3c16a
commit f64f08d8a1
17 changed files with 1453 additions and 9 deletions

View file

@ -0,0 +1,183 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../../semantic-colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { t } from '../../../../i18n/index.js';
import type { ServerDetailStepProps } from '../types.js';
import {
getStatusColor,
getStatusIcon,
formatServerCommand,
} from '../utils.js';
type ServerAction = 'view-tools' | 'reconnect' | 'disable';
export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
server,
onViewTools,
onReconnect,
onDisable,
onBack,
}) => {
const [selectedAction, setSelectedAction] =
useState<ServerAction>('view-tools');
const statusColor = server ? getStatusColor(server.status) : 'gray';
const actions = [
{
key: 'view-tools',
get label() {
return t('View tools');
},
value: 'view-tools' as const,
},
{
key: 'reconnect',
get label() {
return t('Reconnect');
},
value: 'reconnect' as const,
},
{
key: 'disable',
get label() {
return t('Disable');
},
value: 'disable' as const,
},
];
useKeypress(
(key) => {
if (key.name === 'escape') {
onBack();
} else if (key.name === 'return') {
switch (selectedAction) {
case 'view-tools':
onViewTools();
break;
case 'reconnect':
onReconnect?.();
break;
case 'disable':
onDisable?.();
break;
default:
break;
}
}
},
{ isActive: true },
);
if (!server) {
return (
<Box>
<Text color={theme.status.error}>{t('No server selected')}</Text>
</Box>
);
}
return (
<Box flexDirection="column" gap={1}>
{/* 服务器详情 */}
<Box flexDirection="column">
<Box>
<Text color={theme.text.primary}>{t('Status:')}</Text>
<Box marginLeft={2}>
<Text
color={
statusColor === 'green'
? theme.status.success
: statusColor === 'yellow'
? theme.status.warning
: theme.status.error
}
>
{getStatusIcon(server.status)} {t(server.status)}
</Text>
</Box>
</Box>
<Box marginTop={1}>
<Text color={theme.text.primary}>{t('Command:')}</Text>
<Box marginLeft={2}>
<Text wrap="truncate">{formatServerCommand(server)}</Text>
</Box>
</Box>
{server.config.cwd && (
<Box marginTop={1}>
<Text color={theme.text.primary}>{t('Working Directory:')}</Text>
<Box marginLeft={2}>
<Text wrap="truncate">{server.config.cwd}</Text>
</Box>
</Box>
)}
<Box marginTop={1}>
<Text color={theme.text.primary}>{t('Capabilities:')}</Text>
<Box marginLeft={2}>
<Text>
{server.toolCount > 0 ? t('tools') : ''}
{server.toolCount > 0 && server.promptCount > 0 ? ', ' : ''}
{server.promptCount > 0 ? t('prompts') : ''}
</Text>
</Box>
</Box>
<Box marginTop={1}>
<Text color={theme.text.primary}>{t('Tools:')}</Text>
<Box marginLeft={2}>
<Text>
{server.toolCount}{' '}
{server.toolCount === 1 ? t('tool') : t('tools')}
</Text>
</Box>
</Box>
{server.errorMessage && (
<Box marginTop={1}>
<Text color={theme.status.error}>{t('Error:')}</Text>
<Box marginLeft={2}>
<Text color={theme.status.error} wrap="wrap">
{server.errorMessage}
</Text>
</Box>
</Box>
)}
</Box>
{/* 操作列表 */}
<Box marginTop={1}>
<RadioButtonSelect<ServerAction>
items={actions}
onHighlight={(value: ServerAction) => setSelectedAction(value)}
onSelect={(value: ServerAction) => {
switch (value) {
case 'view-tools':
onViewTools();
break;
case 'reconnect':
onReconnect?.();
break;
case 'disable':
onDisable?.();
break;
default:
break;
}
}}
/>
</Box>
</Box>
);
};