mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-08 16:10:09 +00:00
update , add mcp server stdio and sse
This commit is contained in:
@@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { CircleStop, History, NotebookPen, Plus, SquareSlash } from 'lucide-react'
|
||||
import { CircleStop, History, NotebookPen, Plus, Server, SquareSlash } from 'lucide-react'
|
||||
import { App, Notice } from 'obsidian'
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -20,6 +20,7 @@ import { APPLY_VIEW_TYPE } from '../../constants'
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useDiffStrategy } from '../../contexts/DiffStrategyContext'
|
||||
import { useLLM } from '../../contexts/LLMContext'
|
||||
import { useMcpHub } from '../../contexts/McpHubContext'
|
||||
import { useRAG } from '../../contexts/RAGContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import {
|
||||
@@ -49,19 +50,22 @@ import {
|
||||
import { readTFileContent } from '../../utils/obsidian'
|
||||
import { openSettingsModalWithError } from '../../utils/open-settings-modal'
|
||||
import { PromptGenerator, addLineNumbers } from '../../utils/prompt-generator'
|
||||
import { fetchUrlsContent, webSearch } from '../../utils/web-search'
|
||||
// Removed empty line above, added one below for group separation
|
||||
import { fetchUrlsContent, onEnt, webSearch } from '../../utils/web-search'
|
||||
|
||||
import { ModeSelect } from './chat-input/ModeSelect'
|
||||
import { ModeSelect } from './chat-input/ModeSelect' // Start of new group
|
||||
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
|
||||
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatHistory } from './ChatHistoryView'
|
||||
import CommandsView from './CommandsView'
|
||||
import CustomModeView from './CustomModeView'
|
||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||
import McpHubView from './McpHubView' // Moved after MarkdownReasoningBlock
|
||||
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||
import ReactMarkdown from './ReactMarkdown'
|
||||
import ShortcutInfo from './ShortcutInfo'
|
||||
import SimilaritySearchResults from './SimilaritySearchResults'
|
||||
|
||||
// Add an empty line here
|
||||
const getNewInputMessage = (app: App, defaultMention: string): ChatUserMessage => {
|
||||
const mentionables: Mentionable[] = [];
|
||||
@@ -103,6 +107,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
const { settings, setSettings } = useSettings()
|
||||
const { getRAGEngine } = useRAG()
|
||||
const diffStrategy = useDiffStrategy()
|
||||
const { getMcpHub } = useMcpHub()
|
||||
const { customModeList, customModePrompts } = useCustomModes()
|
||||
|
||||
const {
|
||||
@@ -115,9 +120,9 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
const { streamResponse, chatModel } = useLLM()
|
||||
|
||||
const promptGenerator = useMemo(() => {
|
||||
// @ts-expect-error
|
||||
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList)
|
||||
}, [getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList])
|
||||
// @ts-expect-error TODO: Review PromptGenerator constructor parameters and types
|
||||
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList, getMcpHub)
|
||||
}, [getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList, getMcpHub])
|
||||
|
||||
const [inputMessage, setInputMessage] = useState<ChatUserMessage>(() => {
|
||||
const newMessage = getNewInputMessage(app, settings.defaultMention)
|
||||
@@ -166,7 +171,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode'>('chat')
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp'>('chat')
|
||||
|
||||
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -190,6 +196,11 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
return () => scrollContainer.removeEventListener('scroll', handleScroll)
|
||||
}, [chatMessages])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
onEnt(`switch_tab/${tab}`)
|
||||
}, [tab])
|
||||
|
||||
const handleCreateCommand = (serializedNodes: BaseSerializedNode[]) => {
|
||||
setSelectedSerializedNodes(serializedNodes)
|
||||
setTab('commands')
|
||||
@@ -217,7 +228,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
abortActiveStreams()
|
||||
const conversation = await getChatMessagesById(conversationId)
|
||||
if (!conversation) {
|
||||
throw new Error(t('chat.errors.conversationNotFound'))
|
||||
throw new Error(String(t('chat.errors.conversationNotFound')))
|
||||
}
|
||||
setCurrentConversationId(conversationId)
|
||||
setChatMessages(conversation)
|
||||
@@ -228,8 +239,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
type: 'idle',
|
||||
})
|
||||
} catch (error) {
|
||||
new Notice(t('chat.errors.failedToLoadConversation'))
|
||||
console.error(t('chat.errors.failedToLoadConversation'), error)
|
||||
new Notice(String(t('chat.errors.failedToLoadConversation')))
|
||||
console.error(String(t('chat.errors.failedToLoadConversation')), error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +287,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
try {
|
||||
const abortController = new AbortController()
|
||||
activeStreamAbortControllersRef.current.push(abortController)
|
||||
|
||||
onEnt('chat-submit')
|
||||
const { requestMessages, compiledMessages } =
|
||||
await promptGenerator.generateRequestMessages({
|
||||
messages: newChatHistory,
|
||||
@@ -705,6 +716,42 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'use_mcp_tool') {
|
||||
const mcpHub = await getMcpHub()
|
||||
if (!mcpHub) {
|
||||
throw new Error('MCP hub not found')
|
||||
}
|
||||
const toolResult = await mcpHub.callTool(toolArgs.server_name, toolArgs.tool_name, toolArgs.parameters)
|
||||
const toolResultPretty =
|
||||
(toolResult?.isError ? "Error:\n" : "") +
|
||||
toolResult?.content
|
||||
.map((item) => {
|
||||
if (item.type === "text") {
|
||||
return item.text
|
||||
}
|
||||
if (item.type === "resource") {
|
||||
const { blob: _, ...rest } = item.resource
|
||||
return JSON.stringify(rest, null, 2)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n") || "(No response)"
|
||||
|
||||
const formattedContent = `[use_mcp_tool for '${toolArgs.server_name}'] Result:\n${toolResultPretty}\n`;
|
||||
return {
|
||||
type: 'use_mcp_tool',
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Applied,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes', error)
|
||||
@@ -723,6 +770,21 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
} : message,
|
||||
);
|
||||
}
|
||||
if (result.returnMsg) {
|
||||
newChatMessages.push({
|
||||
id: uuidv4(),
|
||||
role: 'assistant',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
isToolResult: true,
|
||||
content: `<tool_result>${result.returnMsg.promptContent}</tool_result>`,
|
||||
reasoningContent: '',
|
||||
metadata: {
|
||||
usage: undefined,
|
||||
model: undefined,
|
||||
},
|
||||
})
|
||||
console.log('Updated chat messages:', newChatMessages);
|
||||
}
|
||||
setChatMessages(newChatMessages);
|
||||
|
||||
if (result.returnMsg) {
|
||||
@@ -953,6 +1015,18 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<NotebookPen size={18} color={tab === 'custom-mode' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'mcp') {
|
||||
setTab('chat')
|
||||
} else {
|
||||
setTab('mcp')
|
||||
}
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<Server size={18} color={tab === 'mcp' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* main view */}
|
||||
@@ -1071,10 +1145,14 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
selectedSerializedNodes={selectedSerializedNodes}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
) : tab === 'custom-mode' ? (
|
||||
<div className="infio-chat-commands">
|
||||
<CustomModeView />
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<McpHubView />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CustomMode, GroupEntry, ToolGroup } from '../../database/json/custom-mo
|
||||
import { useCustomModes } from '../../hooks/use-custom-mode';
|
||||
import { t } from '../../lang/helpers';
|
||||
import { PreviewView, PreviewViewState } from '../../PreviewView';
|
||||
import { modes as buildinModes } from '../../utils/modes';
|
||||
import { defaultModes as buildinModes } from '../../utils/modes';
|
||||
import { openOrCreateMarkdownFile } from '../../utils/obsidian';
|
||||
import { PromptGenerator, getFullLanguageName } from '../../utils/prompt-generator';
|
||||
|
||||
@@ -30,7 +30,7 @@ const CustomModeView = () => {
|
||||
const diffStrategy = useDiffStrategy()
|
||||
|
||||
const promptGenerator = useMemo(() => {
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error PromptGenerator constructor parameter types need to be reviewed
|
||||
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList)
|
||||
}, [app, settings, diffStrategy, customModePrompts, customModeList])
|
||||
|
||||
@@ -76,7 +76,7 @@ const CustomModeView = () => {
|
||||
setModeName(newMode.name);
|
||||
setRoleDefinition(newMode.roleDefinition);
|
||||
setCustomInstructions(newMode.customInstructions || '');
|
||||
setSelectedTools(newMode.groups as GroupEntry[]);
|
||||
setSelectedTools(newMode.groups);
|
||||
setCustomModeId('');
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ const CustomModeView = () => {
|
||||
setModeName(builtinMode.slug);
|
||||
setRoleDefinition(builtinMode.roleDefinition);
|
||||
setCustomInstructions(builtinMode.customInstructions || '');
|
||||
setSelectedTools(builtinMode.groups as GroupEntry[]);
|
||||
setSelectedTools(builtinMode.groups);
|
||||
setCustomModeId(''); // Built-in modes don't have custom IDs
|
||||
} else {
|
||||
setIsBuiltinMode(false);
|
||||
@@ -387,7 +387,7 @@ const CustomModeView = () => {
|
||||
type: PREVIEW_VIEW_TYPE,
|
||||
active: true,
|
||||
state: {
|
||||
content: systemPrompt.content as string,
|
||||
content: typeof systemPrompt.content === 'string' ? systemPrompt.content : '',
|
||||
title: `${modeName} system prompt`,
|
||||
} satisfies PreviewViewState,
|
||||
})
|
||||
|
||||
88
src/components/chat-view/Markdown/MarkdownToolResult.tsx
Normal file
88
src/components/chat-view/Markdown/MarkdownToolResult.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ChevronDown, ChevronRight, CheckCheck } from 'lucide-react'
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
|
||||
import { t } from '../../../lang/helpers'
|
||||
|
||||
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
|
||||
|
||||
const processContent = (content: string): { serverName: string; processedContent: string } => {
|
||||
const lines = content.split('\n');
|
||||
const firstLine = lines[0];
|
||||
|
||||
// 提取 serverName
|
||||
const serverNameMatch = firstLine.match(/\[use_mcp_tool for '([^']+)'\]/);
|
||||
const serverName = serverNameMatch ? serverNameMatch[1] : '';
|
||||
|
||||
// 移除第一行并重新组合内容
|
||||
const processedContent = lines.slice(1).join('\n');
|
||||
|
||||
return { serverName, processedContent };
|
||||
};
|
||||
|
||||
export default function MarkdownToolResult({
|
||||
content,
|
||||
}: PropsWithChildren<{
|
||||
content: string
|
||||
}>) {
|
||||
const { isDarkMode } = useDarkModeContext()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
const { serverName, processedContent } = React.useMemo(() => processContent(content), [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
||||
}
|
||||
}, [processedContent])
|
||||
|
||||
return (
|
||||
processedContent && (
|
||||
<div
|
||||
className={`infio-chat-code-block has-filename infio-reasoning-block`}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
<CheckCheck size={10} className="infio-chat-code-block-header-icon" />
|
||||
response from tool
|
||||
<span className="infio-mcp-tool-server-name">{serverName}</span>
|
||||
</div>
|
||||
<button
|
||||
className="clickable-icon infio-chat-list-dropdown"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="infio-reasoning-content-wrapper"
|
||||
>
|
||||
<MemoizedSyntaxHighlighterWrapper
|
||||
isDarkMode={isDarkMode}
|
||||
language="markdown"
|
||||
hasFilename={true}
|
||||
wrapLines={true}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{processedContent}
|
||||
</MemoizedSyntaxHighlighterWrapper>
|
||||
</div>
|
||||
<style>
|
||||
{`
|
||||
.infio-mcp-tool-server-name {
|
||||
color: var(--text-accent);
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
109
src/components/chat-view/Markdown/UseMcpToolBlock.tsx
Normal file
109
src/components/chat-view/Markdown/UseMcpToolBlock.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Server } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import { useSettings } from "../../../contexts/SettingsContext"
|
||||
import { t } from '../../../lang/helpers'
|
||||
import { ApplyStatus, SearchWebToolArgs } from "../../../types/apply"
|
||||
|
||||
export default function UseMcpToolBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
serverName,
|
||||
toolName,
|
||||
parameters,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: SearchWebToolArgs) => void
|
||||
serverName: string,
|
||||
toolName: string,
|
||||
parameters: Record<string, unknown>,
|
||||
finish: boolean
|
||||
}) {
|
||||
|
||||
const { settings } = useSettings()
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
onApply({
|
||||
type: 'use_mcp_tool',
|
||||
server_name: serverName,
|
||||
tool_name: toolName,
|
||||
parameters: parameters,
|
||||
})
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`infio-chat-code-block has-filename`
|
||||
}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
<Server size={14} className="infio-chat-code-block-header-icon" />
|
||||
use mcp tool from
|
||||
<span className="infio-mcp-tool-server-name">{serverName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="infio-reasoning-content-wrapper"
|
||||
>
|
||||
<div className="infio-mcp-tool-row">
|
||||
<div className="infio-mcp-tool-row-header">
|
||||
<div className="infio-mcp-tool-name-section">
|
||||
<span className="infio-mcp-tool-name">{toolName}</span>
|
||||
</div>
|
||||
</div>
|
||||
参数: <div className="infio-mcp-tool-parameters">
|
||||
<pre className="infio-json-pre"><code>{JSON.stringify(parameters, null, 2)}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
.infio-mcp-tool-row {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-primary);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
.infio-mcp-tool-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.infio-mcp-tool-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
}
|
||||
.infio-mcp-tool-server-name {
|
||||
color: var(--text-accent);
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
}
|
||||
.infio-mcp-tool-parameters {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
.infio-json-pre {
|
||||
background: #282c34;
|
||||
color: #d4d4d4;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
755
src/components/chat-view/McpHubView.tsx
Normal file
755
src/components/chat-view/McpHubView.tsx
Normal file
@@ -0,0 +1,755 @@
|
||||
import { AlertTriangle, ChevronDown, ChevronRight, FileText, Folder, Power, RotateCcw, Trash2, Wrench } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useMcpHub } from '../../contexts/McpHubContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import { McpErrorEntry, McpResource, McpResourceTemplate, McpServer, McpTool } from '../../core/mcp/type'
|
||||
import { t } from '../../lang/helpers'
|
||||
|
||||
const McpHubView = () => {
|
||||
const { settings, setSettings } = useSettings()
|
||||
const { getMcpHub } = useMcpHub()
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [expandedServers, setExpandedServers] = useState<Record<string, boolean>>({});
|
||||
const [activeServerDetailTab, setActiveServerDetailTab] = useState<Record<string, 'tools' | 'resources' | 'errors'>>({});
|
||||
|
||||
const fetchServers = async () => {
|
||||
const hub = await getMcpHub()
|
||||
console.log('Fetching MCP Servers from hub:', hub)
|
||||
if (hub) {
|
||||
const serversData = hub.getAllServers()
|
||||
console.log('Fetched MCP Servers:', serversData)
|
||||
setMcpServers(serversData)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers()
|
||||
}, [getMcpHub])
|
||||
|
||||
const switchMcp = React.useCallback(() => {
|
||||
setSettings({
|
||||
...settings,
|
||||
mcpEnabled: !settings.mcpEnabled,
|
||||
})
|
||||
}, [settings, setSettings])
|
||||
|
||||
// const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// setSearchTerm(e.target.value)
|
||||
// }
|
||||
|
||||
const handleRestart = async (serverName: string) => {
|
||||
const hub = await getMcpHub();
|
||||
if (hub) {
|
||||
await hub.restartConnection(serverName, "global")
|
||||
const updatedServers = hub.getAllServers()
|
||||
setMcpServers(updatedServers)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggle = async (serverName: string, disabled: boolean) => {
|
||||
const hub = await getMcpHub();
|
||||
if (hub) {
|
||||
await hub.toggleServerDisabled(serverName, !disabled)
|
||||
const updatedServers = hub.getAllServers()
|
||||
setMcpServers(updatedServers)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (serverName: string) => {
|
||||
const hub = await getMcpHub();
|
||||
if (hub) {
|
||||
if (confirm(`确定要删除服务器 "${serverName}" 吗?`)) {
|
||||
await hub.deleteServer(serverName, "global")
|
||||
const updatedServers = hub.getAllServers()
|
||||
setMcpServers(updatedServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const toggleServerExpansion = (serverKey: string) => {
|
||||
setExpandedServers(prev => ({ ...prev, [serverKey]: !prev[serverKey] }));
|
||||
if (!expandedServers[serverKey] && !activeServerDetailTab[serverKey]) {
|
||||
setActiveServerDetailTab(prev => ({ ...prev, [serverKey]: 'tools' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDetailTabChange = (serverKey: string, tab: 'tools' | 'resources' | 'errors') => {
|
||||
setActiveServerDetailTab(prev => ({ ...prev, [serverKey]: tab }));
|
||||
};
|
||||
|
||||
const ToolRow = ({ tool }: { tool: McpTool }) => {
|
||||
return (
|
||||
<div className="infio-mcp-tool-row">
|
||||
<div className="infio-mcp-tool-row-header">
|
||||
<div className="infio-mcp-tool-name-section">
|
||||
<span className="infio-mcp-tool-name">{tool.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
{tool.description && (
|
||||
<p className="infio-mcp-item-description">{tool.description}</p>
|
||||
)}
|
||||
{(tool.inputSchema && (() => {
|
||||
const schema = tool.inputSchema;
|
||||
const properties = schema && typeof schema === 'object' && 'properties' in schema ? schema.properties : undefined;
|
||||
const required = schema && typeof schema === 'object' && 'required' in schema ? schema.required : undefined;
|
||||
|
||||
if (properties && typeof properties === 'object' && Object.keys(properties).length > 0) {
|
||||
return (
|
||||
<div className="infio-mcp-tool-parameters">
|
||||
<h5 className="infio-mcp-parameters-title">{t('parameters')}</h5>
|
||||
{Object.entries(properties).map(
|
||||
([paramName, paramSchemaUntyped]) => {
|
||||
const paramSchema = paramSchemaUntyped && typeof paramSchemaUntyped === 'object' ? paramSchemaUntyped : {};
|
||||
const paramDescription = 'description' in paramSchema && typeof paramSchema.description === 'string' ? paramSchema.description : undefined;
|
||||
const isRequired = required && Array.isArray(required) && required.includes(paramName);
|
||||
return (
|
||||
<div key={paramName} className="infio-mcp-parameter-item">
|
||||
<code className="infio-mcp-parameter-name">
|
||||
{paramName}
|
||||
{isRequired && <span className="infio-mcp-parameter-required">*</span>}
|
||||
</code>
|
||||
<span className="infio-mcp-parameter-description">
|
||||
{paramDescription || t('mcpHub.tool.noDescription')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})())}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceRow = ({ resource }: { resource: McpResource | McpResourceTemplate }) => (
|
||||
<div className="infio-mcp-resource-row">
|
||||
<div className="infio-mcp-resource-header">
|
||||
<FileText size={16} className="infio-mcp-resource-icon" />
|
||||
<strong>{'uri' in resource ? resource.uri : resource.uriTemplate}</strong>
|
||||
</div>
|
||||
{resource.description && <p className="infio-mcp-item-description">{resource.description}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ErrorRow = ({ error }: { error: McpErrorEntry }) => (
|
||||
<div className="infio-mcp-error-row">
|
||||
<div className="infio-mcp-error-header">
|
||||
<AlertTriangle size={16} className="infio-mcp-error-icon" />
|
||||
<p style={{ color: error.level === 'error' ? 'var(--text-error)' : error.level === 'warn' ? 'var(--text-warning)' : 'var(--text-normal)' }}>
|
||||
{error.message}
|
||||
</p>
|
||||
</div>
|
||||
<p className="infio-mcp-item-timestamp">{new Date(error.timestamp).toLocaleString()}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="infio-mcp-hub-container">
|
||||
{/* Header Section */}
|
||||
<div className="infio-mcp-hub-header">
|
||||
<h2 className="infio-mcp-hub-title">MCP 服务器</h2>
|
||||
</div>
|
||||
|
||||
{/* MCP Settings */}
|
||||
<div className="infio-mcp-settings-section">
|
||||
<div className="infio-mcp-setting-item">
|
||||
<label className="infio-mcp-setting-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.mcpEnabled}
|
||||
onChange={switchMcp}
|
||||
className="infio-mcp-setting-checkbox"
|
||||
/>
|
||||
<span className="infio-mcp-setting-text">启用 MCP 服务器</span>
|
||||
</label>
|
||||
<p className="infio-mcp-setting-description">
|
||||
开启后 Roo 可用已连接 MCP 服务器的工具,能力更强。不用这些工具时建议关闭,节省 API Token 费用。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Servers List */}
|
||||
{settings.mcpEnabled && (
|
||||
<div className="infio-mcp-hub-list">
|
||||
{mcpServers.length === 0 ? (
|
||||
<div className="infio-mcp-hub-empty">
|
||||
<p>{t('mcpHub.noServersFound')}</p>
|
||||
</div>
|
||||
) : (
|
||||
mcpServers.map(server => {
|
||||
const serverKey = `${server.name}-${server.source || 'global'}`;
|
||||
const isExpanded = !!expandedServers[serverKey];
|
||||
const currentDetailTab = activeServerDetailTab[serverKey] || 'tools';
|
||||
|
||||
return (
|
||||
<div key={serverKey} className={`infio-mcp-hub-item ${server.disabled ? 'disabled' : ''}`}>
|
||||
<div className={`infio-mcp-hub-item-header ${server.disabled ? 'disabled' : ''}`}>
|
||||
<div className="infio-mcp-hub-item-info" onClick={() => toggleServerExpansion(serverKey)}>
|
||||
<div className="infio-mcp-hub-expander">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</div>
|
||||
<span className={`infio-mcp-hub-status-indicator ${server.status === 'connected' ? 'connected' : server.status === 'connecting' ? 'connecting' : 'disconnected'} ${server.disabled ? 'disabled' : ''}`}></span>
|
||||
<h3 className="infio-mcp-hub-name">{server.name}</h3>
|
||||
{/* <span className="infio-mcp-hub-source-badge">{server.source}</span> */}
|
||||
</div>
|
||||
|
||||
<div className="infio-mcp-hub-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
className={`infio-section-btn ${server.disabled ? 'disabled' : 'enabled'}`}
|
||||
onClick={() => handleToggle(server.name, server.disabled)}
|
||||
title={server.disabled ? '启用服务器' : '禁用服务器'}
|
||||
>
|
||||
<Power size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="infio-section-btn"
|
||||
onClick={() => handleRestart(server.name)}
|
||||
title="重启服务器"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="infio-section-btn"
|
||||
onClick={() => handleDelete(server.name)}
|
||||
title="删除服务器"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="infio-mcp-hub-status-info">
|
||||
<span className="infio-mcp-status-text">
|
||||
状态: <span className={`status-value ${server.status}`}>{server.status}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && server.status === 'connected' && (
|
||||
<div className="infio-mcp-server-details-expanded">
|
||||
<div className="infio-mcp-tabs">
|
||||
{(['tools', 'resources', 'errors'] as const).map(tabName => {
|
||||
const count = tabName === 'tools'
|
||||
? server.tools?.length || 0
|
||||
: tabName === 'resources'
|
||||
? (server.resources?.length || 0) + (server.resourceTemplates?.length || 0)
|
||||
: server.errorHistory?.length || 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tabName}
|
||||
className={`infio-mcp-tab-button ${currentDetailTab === tabName ? 'active' : ''}`}
|
||||
onClick={(e) => { e.stopPropagation(); handleDetailTabChange(serverKey, tabName); }}
|
||||
>
|
||||
{tabName === 'tools' && <Wrench size={14} />}
|
||||
{tabName === 'resources' && <Folder size={14} />}
|
||||
{tabName === 'errors' && <AlertTriangle size={14} />}
|
||||
{t(`${tabName}`)} ({count})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="infio-mcp-tab-content">
|
||||
{currentDetailTab === 'tools' && (
|
||||
<div className="infio-mcp-tools-list">
|
||||
{(server.tools && server.tools.length > 0) ? server.tools.map(tool => <ToolRow key={tool.name} tool={tool} />) : <p className="infio-mcp-empty-message">{t('mcpHub.noTools')}</p>}
|
||||
</div>
|
||||
)}
|
||||
{currentDetailTab === 'resources' && (
|
||||
<div className="infio-mcp-resources-list">
|
||||
{((server.resources && server.resources.length > 0) || (server.resourceTemplates && server.resourceTemplates.length > 0))
|
||||
? [...(server.resources || []), ...(server.resourceTemplates || [])].map(res => <ResourceRow key={'uri' in res ? res.uri : res.uriTemplate} resource={res} />)
|
||||
: <p className="infio-mcp-empty-message">{t('mcpHub.noResources')}</p>}
|
||||
</div>
|
||||
)}
|
||||
{currentDetailTab === 'errors' && (
|
||||
<div className="infio-mcp-errors-list">
|
||||
{(server.errorHistory && server.errorHistory.length > 0)
|
||||
? [...server.errorHistory].sort((a, b) => b.timestamp - a.timestamp).map((err, idx) => <ErrorRow key={`${err.timestamp}-${idx}`} error={err} />)
|
||||
: <p className="infio-mcp-empty-message">{t('mcpHub.noErrors')}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && server.status !== 'connected' && (
|
||||
<div className="infio-mcp-server-details-expanded">
|
||||
<p className="infio-mcp-server-error-message">
|
||||
{t('mcpHub.serverNotConnectedError')}
|
||||
{server.error && <pre>{server.error}</pre>}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.infio-mcp-hub-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
color: var(--text-normal);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.infio-mcp-hub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* Settings Section */
|
||||
.infio-mcp-settings-section {
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.infio-mcp-setting-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.infio-mcp-setting-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-mcp-setting-checkbox {
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-mcp-setting-text {
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.infio-mcp-setting-description {
|
||||
margin: 8px 0 0 24px;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.infio-mcp-search-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infio-mcp-search-input {
|
||||
background-color: var(--background-primary) !important;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-normal);
|
||||
padding: var(--size-4-2);
|
||||
font-size: var(--font-ui-small);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.infio-mcp-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
/* Server Item Styles */
|
||||
.infio-mcp-hub-item {
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-header:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-header.disabled {
|
||||
opacity: 0.6;
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-header.disabled:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-header.disabled .infio-mcp-hub-name,
|
||||
.infio-mcp-hub-item-header.disabled .infio-mcp-hub-expander {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-header.disabled .infio-mcp-hub-source-badge {
|
||||
background-color: var(--text-faint);
|
||||
color: var(--background-primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-expander {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator.connected {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator.connecting {
|
||||
background-color: #f59e0b;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator.disconnected {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator.disabled.connected {
|
||||
background-color: #10b981;
|
||||
opacity: 0.4;
|
||||
filter: saturate(0.6);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator.disabled.connecting {
|
||||
background-color: #f59e0b;
|
||||
opacity: 0.4;
|
||||
filter: saturate(0.6);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-indicator.disabled.disconnected {
|
||||
background-color: #ef4444;
|
||||
opacity: 0.4;
|
||||
filter: saturate(0.6);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-source-badge {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infio-section-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-muted);
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.infio-section-btn:hover {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.infio-section-btn.enabled {
|
||||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.infio-section-btn.disabled {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-status-info {
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item.disabled .infio-mcp-hub-status-info {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.status-value.connected {
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-value.connecting {
|
||||
color: #f59e0b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-value.disconnected {
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item.disabled .status-value.connected {
|
||||
color: #10b981;
|
||||
opacity: 0.5;
|
||||
filter: saturate(0.6);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item.disabled .status-value.connecting {
|
||||
color: #f59e0b;
|
||||
opacity: 0.5;
|
||||
filter: saturate(0.6);
|
||||
}
|
||||
|
||||
.infio-mcp-hub-item.disabled .status-value.disconnected {
|
||||
color: #ef4444;
|
||||
opacity: 0.5;
|
||||
filter: saturate(0.6);
|
||||
}
|
||||
|
||||
/* Expanded Content Styles */
|
||||
.infio-mcp-server-details-expanded {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-mcp-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.infio-mcp-tab-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infio-mcp-tab-button:hover {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-mcp-tab-button.active {
|
||||
color: var(--interactive-accent);
|
||||
border-bottom-color: var(--interactive-accent);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.infio-mcp-tab-content {
|
||||
background-color: var(--background-primary);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.infio-mcp-empty-message {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Tool/Resource/Error Row Styles */
|
||||
.infio-mcp-tool-row, .infio-mcp-resource-row, .infio-mcp-error-row {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-primary);
|
||||
border-radius: var(--radius-s);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-mcp-tool-row:last-child,
|
||||
.infio-mcp-resource-row:last-child,
|
||||
.infio-mcp-error-row:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.infio-mcp-tool-row-header, .infio-mcp-resource-header, .infio-mcp-error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-mcp-tool-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.infio-mcp-item-description {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
/* Tool Parameters */
|
||||
.infio-mcp-tool-parameters {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.infio-mcp-parameters-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.infio-mcp-parameter-item {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.infio-mcp-parameter-name {
|
||||
display: inline-block;
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-accent);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.infio-mcp-parameter-required {
|
||||
color: var(--text-error);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.infio-mcp-parameter-description {
|
||||
display: block;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Error Messages */
|
||||
.infio-mcp-server-error-message {
|
||||
background-color: var(--background-modifier-error);
|
||||
border-left: 3px solid var(--text-error);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.infio-mcp-server-error-message pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.infio-mcp-item-timestamp {
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.infio-mcp-hub-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpHubView
|
||||
@@ -18,7 +18,9 @@ import MarkdownSearchAndReplace from './Markdown/MarkdownSearchAndReplace'
|
||||
import MarkdownSearchWebBlock from './Markdown/MarkdownSearchWebBlock'
|
||||
import MarkdownSemanticSearchFilesBlock from './Markdown/MarkdownSemanticSearchFilesBlock'
|
||||
import MarkdownSwitchModeBlock from './Markdown/MarkdownSwitchModeBlock'
|
||||
import MarkdownToolResult from './Markdown/MarkdownToolResult'
|
||||
import MarkdownWithIcons from './Markdown/MarkdownWithIcon'
|
||||
import UseMcpToolBlock from './Markdown/UseMcpToolBlock'
|
||||
|
||||
function ReactMarkdown({
|
||||
applyStatus,
|
||||
@@ -178,6 +180,21 @@ function ReactMarkdown({
|
||||
urls={block.urls}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'use_mcp_tool' ? (
|
||||
<UseMcpToolBlock
|
||||
key={"use-mcp-tool-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
serverName={block.server_name}
|
||||
toolName={block.tool_name}
|
||||
parameters={block.parameters}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'tool_result' ? (
|
||||
<MarkdownToolResult
|
||||
key={"tool-result-" + index}
|
||||
content={block.content}
|
||||
/>
|
||||
) : (
|
||||
<Markdown key={"markdown-" + index} className="infio-markdown">
|
||||
{block.content}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useSettings } from '../../../contexts/SettingsContext'
|
||||
import { useCustomModes } from '../../../hooks/use-custom-mode'
|
||||
import { modes } from '../../../utils/modes'
|
||||
import { defaultModes } from '../../../utils/modes'
|
||||
import { onEnt } from '../../../utils/web-search'
|
||||
|
||||
export function ModeSelect() {
|
||||
const { settings, setSettings } = useSettings()
|
||||
@@ -13,9 +14,10 @@ export function ModeSelect() {
|
||||
|
||||
const { customModeList } = useCustomModes()
|
||||
|
||||
const allModes = useMemo(() => [...modes, ...customModeList], [customModeList])
|
||||
const allModes = useMemo(() => [...defaultModes, ...customModeList], [customModeList])
|
||||
|
||||
useEffect(() => {
|
||||
onEnt(`switch_mode/${settings.mode}`)
|
||||
setMode(settings.mode)
|
||||
}, [settings.mode])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user