diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index c424d513..26115b88 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -187,6 +187,7 @@ export type ThreadStreamState = { id: string; content: string; rawParams: RawToolParamsObj; + mcpServerName: string | undefined; }; interrupt: Promise<() => void>; } | { @@ -448,7 +449,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') { - this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params }) + + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params, mcpServerName: lastMessage.mcpServerName }) } } @@ -541,13 +543,17 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else return - const { name, id, rawParams } = lastMsg + const { name, id, rawParams, mcpServerName } = lastMsg const errorMessage = this.toolErrMsgs.rejected - this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams, mcpServerName }) this._setStreamState(threadId, undefined) } + private _computeMCPServerOfToolName = (toolName: string) => { + return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName + } + async abortRunning(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -556,13 +562,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (this.streamState[threadId]?.isRunning === 'LLM') { const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) }) } // add tool that's running else if (this.streamState[threadId]?.isRunning === 'tool') { - const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo + const { toolName, toolParams, id, content: content_, rawParams, mcpServerName } = this.streamState[threadId].toolInfo const content = content_ || this.toolErrMsgs.interrupted - this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null }) + this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null, mcpServerName }) } // reject the tool for the user if relevant else if (this.streamState[threadId]?.isRunning === 'awaiting_user') { @@ -600,6 +606,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { threadId: string, toolName: ToolName, toolId: string, + mcpServerName: string | undefined, opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { @@ -625,7 +632,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, }) + this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, mcpServerName }) return {} } // once validated, add checkpoint for edit @@ -638,7 +645,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (approvalType) { const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType] // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) - this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) if (!autoApprove) { return { awaitingUserApproval: true } } @@ -655,7 +662,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool // this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const + const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName } as const this._updateLatestTool(threadId, runningTool) @@ -665,7 +672,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { try { // set stream state - this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } }) + this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams, mcpServerName } }) if (isBuiltInTool) { const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) @@ -695,7 +702,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here const errorMessage = getErrorMessage(error) - this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) return {} } @@ -723,12 +730,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } catch (error) { const errorMessage = this.toolErrMsgs.errWhenStringifying(error) - this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) return {} } // 5. add to history and keep going - this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) return {} }; @@ -763,7 +770,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) + const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, callThisToolFirst.mcpServerName, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) if (interrupted) { this._setStreamState(threadId, undefined) this._addUserCheckpoint({ threadId }) @@ -872,7 +879,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { error } = llmRes const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) }) this._setStreamState(threadId, { isRunning: undefined, error }) this._addUserCheckpoint({ threadId }) @@ -889,7 +896,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // call tool if there is one if (toolCall) { - const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams }) + const mcpTools = this._mcpService.getMCPTools() + const mcpTool = mcpTools?.find(t => t.name === toolCall.name) + + const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, mcpTool?.mcpServerName, { preapproved: false, unvalidatedToolParams: toolCall.rawParams }) if (interrupted) { this._setStreamState(threadId, undefined) return diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 3b935267..82c00afb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1414,14 +1414,36 @@ const titleOfBuiltinToolName = { } as const satisfies Record -const getTitle = (toolMessage: Pick): React.ReactNode => { +const getTitle = (toolMessage: Pick): React.ReactNode => { const t = toolMessage - if (!builtinToolNames.includes(t.name as BuiltinToolName)) return t.name // good measure - const toolName = t.name as BuiltinToolName - if (t.type === 'success') return titleOfBuiltinToolName[toolName].done - if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running - return titleOfBuiltinToolName[toolName].proposed + // non-built-in title + if (!builtinToolNames.includes(t.name as BuiltinToolName)) { + + // descriptor of Running or Ran etc + const descriptor = + t.type === 'success' ? 'Ran' + : t.type === 'running_now' ? 'Running' + : t.type === 'tool_request' ? 'Requested' + : t.type === 'rejected' ? 'Canceled' + : t.type === 'invalid_params' ? 'Canceled' + : t.type === 'tool_error' ? 'Canceled' + : 'Ran' + + const title = `${descriptor} ${t.name}` + + if (t.type === 'running_now' || t.type === 'tool_request') + return loadingTitleWrapper(title) + return title + } + + // built-in title + else { + const toolName = t.name as BuiltinToolName + if (t.type === 'success') return titleOfBuiltinToolName[toolName].done + if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running + return titleOfBuiltinToolName[toolName].proposed + } } @@ -1700,9 +1722,9 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName, threadId }: -const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => { +const InvalidTool = ({ toolName, message, mcpServerName }: { toolName: ToolName, message: string, mcpServerName: string | undefined }) => { const accessor = useAccessor() - const title = getTitle({ name: toolName, type: 'invalid_params' }) + const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName }) const desc1 = 'Invalid parameters' const icon = null const isError = true @@ -1716,9 +1738,9 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin return } -const CanceledTool = ({ toolName }: { toolName: ToolName }) => { +const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => { const accessor = useAccessor() - const title = getTitle({ name: toolName, type: 'rejected' }) + const title = getTitle({ name: toolName, type: 'rejected', mcpServerName }) const desc1 = '' const icon = null const isRejected = true @@ -1839,7 +1861,7 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { const desc1 = toolMessage.name const icon = null - if (toolMessage.type === 'tool_request') return null // do not show past requests + if (toolMessage.type === 'running_now') return null // do not show running const isError = false @@ -1847,16 +1869,17 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } - if (toolMessage.type === 'success') { + componentParams.info = `${toolMessage.mcpServerName} MCP server` + + if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') { const { result } = toolMessage componentParams.children = - + } @@ -2529,7 +2552,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me else if (role === 'interrupted_streaming_tool') { return
- +
} @@ -2809,7 +2832,7 @@ const CommandBarInChat = () => { const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { - if (!isABuiltinToolName( toolCallSoFar.name)) return null + if (!isABuiltinToolName(toolCallSoFar.name)) return null const accessor = useAccessor() diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 2c6e7da2..44dc307e 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -13,6 +13,7 @@ export type ToolMessage = { content: string; // give this result to LLM (string of value) id: string; rawParams: RawToolParamsObj; + mcpServerName: string | undefined; // the server name at the time of the call } & ( // in order of events: | { type: 'invalid_params', result: null, name: T, } @@ -29,6 +30,7 @@ export type ToolMessage = { export type DecorativeCanceledTool = { role: 'interrupted_streaming_tool'; name: ToolName; + mcpServerName: string | undefined; // the server name at the time of the call } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index baa1b585..52676f1d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -18,7 +18,7 @@ import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMe import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; -import { availableTools, InternalToolInfo, builtinTools, isABuiltinToolName } from '../../common/prompt/prompts.js'; +import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { BuiltinToolParamName } from '../../common/toolsServiceTypes.js'; @@ -221,21 +221,26 @@ const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | u // convert LLM tool call to our tool format -const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { - if (!isABuiltinToolName(name)) return null - const rawParams: RawToolParamsObj = {} +const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { let input: unknown - try { - input = JSON.parse(toolParamsStr) - } - catch (e) { - return null - } + try { input = JSON.parse(toolParamsStr) } + catch (e) { return null } + if (input === null) return null if (typeof input !== 'object') return null - for (const paramName in builtinTools[name].params) { - rawParams[paramName as BuiltinToolParamName] = (input as any)[paramName] - } + + const rawParams: RawToolParamsObj = input + return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } +} + + +const rawToolCallObjOfAnthropicParams = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => { + const { id, name, input } = toolBlock + + if (input === null) return null + if (typeof input !== 'object') return null + + const rawParams: RawToolParamsObj = input return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } } @@ -339,7 +344,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isABuiltinToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, + toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId }, }) } @@ -348,7 +353,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) + const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } @@ -425,17 +430,7 @@ const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] return anthropicTools } -const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => { - const { id, name, input } = toolBlock - if (!isABuiltinToolName(name)) return null - const rawParams: RawToolParamsObj = {} - if (input === null) return null - if (typeof input !== 'object') return null - for (const paramName in builtinTools[name].params) { - rawParams[paramName as BuiltinToolParamName] = (input as any)[paramName] - } - return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } -} + // ------------ ANTHROPIC ------------ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => { @@ -496,7 +491,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag onText({ fullText, fullReasoning, - toolCall: isABuiltinToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, + toolCall: { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' }, }) } // there are no events for tool_use, it comes in at the end @@ -546,7 +541,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag stream.on('finalMessage', (response) => { const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') const tools = response.content.filter(c => c.type === 'tool_use') - const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0]) + const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0]) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj }) }) @@ -791,7 +786,7 @@ const sendGeminiChat = async ({ onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isABuiltinToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, + toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId }, }) } @@ -800,7 +795,7 @@ const sendGeminiChat = async ({ onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id - const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) + const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); }