mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Merge pull request #630 from voideditor/mcp
bjoaquinc/mcp - MCP Implementation
This commit is contained in:
commit
c12e9ca9f8
30 changed files with 1942 additions and 377 deletions
|
|
@ -8,3 +8,5 @@ Look for services and built-in functions that you might need to use to solve the
|
||||||
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
|
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
|
||||||
|
|
||||||
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
|
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
|
||||||
|
|
||||||
|
Never modify files outside src/vs/workbench/contrib/void without consulting with the user first.
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,9 @@ export default tseslint.config(
|
||||||
'@xterm/xterm',
|
'@xterm/xterm',
|
||||||
'yauzl',
|
'yauzl',
|
||||||
'yazl',
|
'yazl',
|
||||||
'zlib'
|
'zlib',
|
||||||
|
// Void added this
|
||||||
|
'@modelcontextprotocol/sdk/**'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -17,7 +17,7 @@
|
||||||
"@microsoft/1ds-core-js": "^3.2.13",
|
"@microsoft/1ds-core-js": "^3.2.13",
|
||||||
"@microsoft/1ds-post-js": "^3.2.13",
|
"@microsoft/1ds-post-js": "^3.2.13",
|
||||||
"@mistralai/mistralai": "^1.6.0",
|
"@mistralai/mistralai": "^1.6.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@vscode/deviceid": "^0.1.1",
|
"@vscode/deviceid": "^0.1.1",
|
||||||
|
|
@ -2616,9 +2616,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.10.2",
|
"version": "1.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz",
|
||||||
"integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
|
"integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
"@microsoft/1ds-core-js": "^3.2.13",
|
"@microsoft/1ds-core-js": "^3.2.13",
|
||||||
"@microsoft/1ds-post-js": "^3.2.13",
|
"@microsoft/1ds-post-js": "^3.2.13",
|
||||||
"@mistralai/mistralai": "^1.6.0",
|
"@mistralai/mistralai": "^1.6.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@vscode/deviceid": "^0.1.1",
|
"@vscode/deviceid": "^0.1.1",
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda
|
||||||
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
|
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
|
||||||
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
||||||
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
|
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
|
||||||
|
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main VS Code application. There will only ever be one instance,
|
* The main VS Code application. There will only ever be one instance,
|
||||||
|
|
@ -1243,6 +1244,9 @@ export class CodeApplication extends Disposable {
|
||||||
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||||
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
|
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
|
||||||
|
|
||||||
|
const mcpChannel = new MCPChannel();
|
||||||
|
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);
|
||||||
|
|
||||||
// Extension Host Debug Broadcasting
|
// Extension Host Debug Broadcasting
|
||||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||||
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||||
import { URI } from '../../../../base/common/uri.js';
|
import { URI } from '../../../../base/common/uri.js';
|
||||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||||
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
|
import { chat_userMessageContent, isABuiltinToolName } from '../common/prompt/prompts.js';
|
||||||
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js';
|
||||||
import { IToolsService } from './toolsService.js';
|
import { IToolsService } from './toolsService.js';
|
||||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||||
|
|
@ -37,6 +37,8 @@ import { deepClone } from '../../../../base/common/objects.js';
|
||||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||||
|
import { IMCPService } from '../common/mcpService.js';
|
||||||
|
import { RawMCPToolCall } from '../common/mcpServiceTypes.js';
|
||||||
|
|
||||||
|
|
||||||
// related to retrying when LLM message has error
|
// related to retrying when LLM message has error
|
||||||
|
|
@ -181,10 +183,11 @@ export type ThreadStreamState = {
|
||||||
llmInfo?: undefined;
|
llmInfo?: undefined;
|
||||||
toolInfo: {
|
toolInfo: {
|
||||||
toolName: ToolName;
|
toolName: ToolName;
|
||||||
toolParams: ToolCallParams[ToolName];
|
toolParams: ToolCallParams<ToolName>;
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
rawParams: RawToolParamsObj;
|
rawParams: RawToolParamsObj;
|
||||||
|
mcpServerName: string | undefined;
|
||||||
};
|
};
|
||||||
interrupt: Promise<() => void>;
|
interrupt: Promise<() => void>;
|
||||||
} | {
|
} | {
|
||||||
|
|
@ -323,6 +326,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||||
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
|
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
|
||||||
@IFileService private readonly _fileService: IFileService,
|
@IFileService private readonly _fileService: IFileService,
|
||||||
|
@IMCPService private readonly _mcpService: IMCPService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||||
|
|
@ -445,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 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') {
|
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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -532,19 +537,23 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
|
|
||||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||||
|
|
||||||
let params: ToolCallParams[ToolName]
|
let params: ToolCallParams<ToolName>
|
||||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||||
params = lastMsg.params
|
params = lastMsg.params
|
||||||
}
|
}
|
||||||
else return
|
else return
|
||||||
|
|
||||||
const { name, id, rawParams } = lastMsg
|
const { name, id, rawParams, mcpServerName } = lastMsg
|
||||||
|
|
||||||
const errorMessage = this.toolErrMsgs.rejected
|
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)
|
this._setStreamState(threadId, undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _computeMCPServerOfToolName = (toolName: string) => {
|
||||||
|
return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName
|
||||||
|
}
|
||||||
|
|
||||||
async abortRunning(threadId: string) {
|
async abortRunning(threadId: string) {
|
||||||
const thread = this.state.allThreads[threadId]
|
const thread = this.state.allThreads[threadId]
|
||||||
if (!thread) return // should never happen
|
if (!thread) return // should never happen
|
||||||
|
|
@ -553,13 +562,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
if (this.streamState[threadId]?.isRunning === 'LLM') {
|
if (this.streamState[threadId]?.isRunning === 'LLM') {
|
||||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
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
|
// add tool that's running
|
||||||
else if (this.streamState[threadId]?.isRunning === 'tool') {
|
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
|
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
|
// reject the tool for the user if relevant
|
||||||
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
|
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
|
||||||
|
|
@ -597,36 +606,46 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
threadId: string,
|
threadId: string,
|
||||||
toolName: ToolName,
|
toolName: ToolName,
|
||||||
toolId: string,
|
toolId: string,
|
||||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
mcpServerName: string | undefined,
|
||||||
|
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams<ToolName> } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||||
|
|
||||||
// compute these below
|
// compute these below
|
||||||
let toolParams: ToolCallParams[ToolName]
|
let toolParams: ToolCallParams<ToolName>
|
||||||
let toolResult: Awaited<ToolResultType[typeof toolName]>
|
let toolResult: ToolResult<ToolName>
|
||||||
let toolResultStr: string
|
let toolResultStr: string
|
||||||
|
|
||||||
|
// Check if it's a built-in tool
|
||||||
|
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||||
|
|
||||||
|
|
||||||
if (!opts.preapproved) { // skip this if pre-approved
|
if (!opts.preapproved) { // skip this if pre-approved
|
||||||
// 1. validate tool params
|
// 1. validate tool params
|
||||||
try {
|
try {
|
||||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
if (isBuiltInTool) {
|
||||||
toolParams = params
|
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||||
} catch (error) {
|
toolParams = params
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toolParams = opts.unvalidatedToolParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
const errorMessage = getErrorMessage(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 {}
|
return {}
|
||||||
}
|
}
|
||||||
// once validated, add checkpoint for edit
|
// once validated, add checkpoint for edit
|
||||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['edit_file']).uri }) }
|
||||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
|
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['rewrite_file']).uri }) }
|
||||||
|
|
||||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||||
|
|
||||||
|
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
|
||||||
const approvalType = approvalTypeOfToolName[toolName]
|
|
||||||
if (approvalType) {
|
if (approvalType) {
|
||||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove[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)
|
// 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) {
|
if (!autoApprove) {
|
||||||
return { awaitingUserApproval: true }
|
return { awaitingUserApproval: true }
|
||||||
}
|
}
|
||||||
|
|
@ -638,9 +657,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 3. call the tool
|
// 3. call the tool
|
||||||
// this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
// 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)
|
this._updateLatestTool(threadId, runningTool)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -650,13 +672,28 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// set stream state
|
// 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 } })
|
||||||
|
|
||||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
if (isBuiltInTool) {
|
||||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||||
resolveInterruptor(interruptor)
|
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||||
|
resolveInterruptor(interruptor)
|
||||||
|
|
||||||
toolResult = await result
|
toolResult = await result
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const mcpTools = this._mcpService.getMCPTools()
|
||||||
|
const mcpTool = mcpTools?.find(t => t.name === toolName)
|
||||||
|
if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) }
|
||||||
|
|
||||||
|
resolveInterruptor(() => { })
|
||||||
|
|
||||||
|
toolResult = (await this._mcpService.callMCPTool({
|
||||||
|
serverName: mcpTool.mcpServerName ?? 'unknown_mcp_server',
|
||||||
|
toolName: toolName,
|
||||||
|
params: toolParams
|
||||||
|
})).result
|
||||||
|
}
|
||||||
|
|
||||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||||
}
|
}
|
||||||
|
|
@ -665,21 +702,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error)
|
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 {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. stringify the result to give to the LLM
|
// 4. stringify the result to give to the LLM
|
||||||
try {
|
try {
|
||||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
if (isBuiltInTool) {
|
||||||
|
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||||
|
}
|
||||||
|
// For MCP tools, handle the result based on its type
|
||||||
|
else {
|
||||||
|
toolResultStr = this._mcpService.stringifyResult(toolResult as RawMCPToolCall)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = this.toolErrMsgs.errWhenStringifying(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 {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. add to history and keep going
|
// 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 {}
|
return {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -714,7 +757,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
|
|
||||||
// before enter loop, call tool
|
// before enter loop, call tool
|
||||||
if (callThisToolFirst) {
|
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) {
|
if (interrupted) {
|
||||||
this._setStreamState(threadId, undefined)
|
this._setStreamState(threadId, undefined)
|
||||||
this._addUserCheckpoint({ threadId })
|
this._addUserCheckpoint({ threadId })
|
||||||
|
|
@ -823,7 +866,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
const { error } = llmRes
|
const { error } = llmRes
|
||||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
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._setStreamState(threadId, { isRunning: undefined, error })
|
||||||
this._addUserCheckpoint({ threadId })
|
this._addUserCheckpoint({ threadId })
|
||||||
|
|
@ -840,7 +883,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
|
|
||||||
// call tool if there is one
|
// call tool if there is one
|
||||||
if (toolCall) {
|
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) {
|
if (interrupted) {
|
||||||
this._setStreamState(threadId, undefined)
|
this._setStreamState(threadId, undefined)
|
||||||
return
|
return
|
||||||
|
|
@ -1300,7 +1346,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||||
}
|
}
|
||||||
// URIs of files that have been read
|
// URIs of files that have been read
|
||||||
else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') {
|
else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') {
|
||||||
const params = m.params as ToolCallParams['read_file']
|
const params = m.params as BuiltinToolCallParams['read_file']
|
||||||
addURI(params.uri)
|
addURI(params.uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
|
||||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||||
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
|
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||||
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
import { reParsedToolXMLString, chat_systemMessage } from '../common/prompt/prompts.js';
|
||||||
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||||
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
|
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
|
||||||
|
|
@ -16,6 +16,8 @@ import { ITerminalToolService } from './terminalToolService.js';
|
||||||
import { IVoidModelService } from '../common/voidModelService.js';
|
import { IVoidModelService } from '../common/voidModelService.js';
|
||||||
import { URI } from '../../../../base/common/uri.js';
|
import { URI } from '../../../../base/common/uri.js';
|
||||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||||
|
import { ToolName } from '../common/toolsServiceTypes.js';
|
||||||
|
import { IMCPService } from '../common/mcpService.js';
|
||||||
|
|
||||||
export const EMPTY_MESSAGE = '(empty message)'
|
export const EMPTY_MESSAGE = '(empty message)'
|
||||||
|
|
||||||
|
|
@ -455,8 +457,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
|
||||||
return { text: c.text }
|
return { text: c.text }
|
||||||
}
|
}
|
||||||
else if (c.type === 'tool_use') {
|
else if (c.type === 'tool_use') {
|
||||||
latestToolName = c.name as ToolName
|
latestToolName = c.name
|
||||||
return { functionCall: { id: c.id, name: c.name as ToolName, args: c.input } }
|
return { functionCall: { id: c.id, name: c.name, args: c.input } }
|
||||||
}
|
}
|
||||||
else return null
|
else return null
|
||||||
}).filter(m => !!m)
|
}).filter(m => !!m)
|
||||||
|
|
@ -538,6 +540,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||||
|
@IMCPService private readonly mcpService: IMCPService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
@ -587,8 +590,10 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||||
|
|
||||||
const includeXMLToolDefinitions = !specialToolFormat
|
const includeXMLToolDefinitions = !specialToolFormat
|
||||||
|
|
||||||
|
const mcpTools = this.mcpService.getMCPTools()
|
||||||
|
|
||||||
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
||||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions })
|
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, mcpTools, includeXMLToolDefinitions })
|
||||||
return systemMessage
|
return systemMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd
|
||||||
import { URI } from '../../../../../../../base/common/uri.js';
|
import { URI } from '../../../../../../../base/common/uri.js';
|
||||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js';
|
||||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||||
import { PastThreadsList } from './SidebarThreadSelector.js';
|
import { PastThreadsList } from './SidebarThreadSelector.js';
|
||||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||||
|
|
@ -24,11 +24,11 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||||
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
|
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
|
||||||
import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
|
import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
|
||||||
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
||||||
import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js';
|
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js';
|
||||||
import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js';
|
import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js';
|
||||||
import { IsRunningType } from '../../../chatThreadService.js';
|
import { IsRunningType } from '../../../chatThreadService.js';
|
||||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||||
import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js';
|
||||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||||
import ErrorBoundary from './ErrorBoundary.js';
|
import ErrorBoundary from './ErrorBoundary.js';
|
||||||
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
||||||
|
|
@ -36,6 +36,7 @@ import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
||||||
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
|
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -907,11 +908,14 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
||||||
const desc1OnClick = () => voidOpenFileFn(params.uri, accessor)
|
const desc1OnClick = () => voidOpenFileFn(params.uri, accessor)
|
||||||
const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, }
|
const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, }
|
||||||
|
|
||||||
|
|
||||||
|
const editToolType = toolMessage.name === 'edit_file' ? 'diff' : 'rewrite'
|
||||||
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
|
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
|
||||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||||
<EditToolChildren
|
<EditToolChildren
|
||||||
uri={params.uri}
|
uri={params.uri}
|
||||||
code={content}
|
code={content}
|
||||||
|
type={editToolType}
|
||||||
/>
|
/>
|
||||||
</ToolChildrenWrapper>
|
</ToolChildrenWrapper>
|
||||||
// JumpToFileButton removed in favor of FileLinkText
|
// JumpToFileButton removed in favor of FileLinkText
|
||||||
|
|
@ -936,6 +940,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
||||||
<EditToolChildren
|
<EditToolChildren
|
||||||
uri={params.uri}
|
uri={params.uri}
|
||||||
code={content}
|
code={content}
|
||||||
|
type={editToolType}
|
||||||
/>
|
/>
|
||||||
</ToolChildrenWrapper>
|
</ToolChildrenWrapper>
|
||||||
|
|
||||||
|
|
@ -1388,7 +1393,7 @@ const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleOfToolName = {
|
const titleOfBuiltinToolName = {
|
||||||
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
|
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
|
||||||
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||||
'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') },
|
'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') },
|
||||||
|
|
@ -1406,21 +1411,42 @@ const titleOfToolName = {
|
||||||
|
|
||||||
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
|
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
|
||||||
'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') },
|
'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') },
|
||||||
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
|
} as const satisfies Record<BuiltinToolName, { done: any, proposed: any, running: any }>
|
||||||
|
|
||||||
|
|
||||||
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
|
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type' | 'mcpServerName'>): React.ReactNode => {
|
||||||
const t = toolMessage
|
const t = toolMessage
|
||||||
if (!toolNames.includes(t.name as ToolName)) return t.name // good measure
|
|
||||||
|
|
||||||
const toolName = t.name as ToolName
|
// non-built-in title
|
||||||
if (t.type === 'success') return titleOfToolName[toolName].done
|
if (!builtinToolNames.includes(t.name as BuiltinToolName)) {
|
||||||
if (t.type === 'running_now') return titleOfToolName[toolName].running
|
// descriptor of Running or Ran etc
|
||||||
return titleOfToolName[toolName].proposed
|
const descriptor =
|
||||||
|
t.type === 'success' ? 'Called'
|
||||||
|
: t.type === 'running_now' ? 'Calling'
|
||||||
|
: t.type === 'tool_request' ? 'Call'
|
||||||
|
: t.type === 'rejected' ? 'Call'
|
||||||
|
: t.type === 'invalid_params' ? 'Call'
|
||||||
|
: t.type === 'tool_error' ? 'Call'
|
||||||
|
: 'Call'
|
||||||
|
|
||||||
|
|
||||||
|
const title = `${descriptor} ${toolMessage.mcpServerName || 'MCP'}`
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
|
const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
|
||||||
desc1: React.ReactNode,
|
desc1: React.ReactNode,
|
||||||
desc1Info?: string,
|
desc1Info?: string,
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -1431,95 +1457,95 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
|
||||||
|
|
||||||
const x = {
|
const x = {
|
||||||
'read_file': () => {
|
'read_file': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['read_file']
|
const toolParams = _toolParams as BuiltinToolCallParams['read_file']
|
||||||
return {
|
return {
|
||||||
desc1: getBasename(toolParams.uri.fsPath),
|
desc1: getBasename(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
'ls_dir': () => {
|
'ls_dir': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['ls_dir']
|
const toolParams = _toolParams as BuiltinToolCallParams['ls_dir']
|
||||||
return {
|
return {
|
||||||
desc1: getFolderName(toolParams.uri.fsPath),
|
desc1: getFolderName(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
'search_pathnames_only': () => {
|
'search_pathnames_only': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
|
const toolParams = _toolParams as BuiltinToolCallParams['search_pathnames_only']
|
||||||
return {
|
return {
|
||||||
desc1: `"${toolParams.query}"`,
|
desc1: `"${toolParams.query}"`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'search_for_files': () => {
|
'search_for_files': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['search_for_files']
|
const toolParams = _toolParams as BuiltinToolCallParams['search_for_files']
|
||||||
return {
|
return {
|
||||||
desc1: `"${toolParams.query}"`,
|
desc1: `"${toolParams.query}"`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'search_in_file': () => {
|
'search_in_file': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['search_in_file'];
|
const toolParams = _toolParams as BuiltinToolCallParams['search_in_file'];
|
||||||
return {
|
return {
|
||||||
desc1: `"${toolParams.query}"`,
|
desc1: `"${toolParams.query}"`,
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
'create_file_or_folder': () => {
|
'create_file_or_folder': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
|
const toolParams = _toolParams as BuiltinToolCallParams['create_file_or_folder']
|
||||||
return {
|
return {
|
||||||
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'delete_file_or_folder': () => {
|
'delete_file_or_folder': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['delete_file_or_folder']
|
const toolParams = _toolParams as BuiltinToolCallParams['delete_file_or_folder']
|
||||||
return {
|
return {
|
||||||
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'rewrite_file': () => {
|
'rewrite_file': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['rewrite_file']
|
const toolParams = _toolParams as BuiltinToolCallParams['rewrite_file']
|
||||||
return {
|
return {
|
||||||
desc1: getBasename(toolParams.uri.fsPath),
|
desc1: getBasename(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'edit_file': () => {
|
'edit_file': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['edit_file']
|
const toolParams = _toolParams as BuiltinToolCallParams['edit_file']
|
||||||
return {
|
return {
|
||||||
desc1: getBasename(toolParams.uri.fsPath),
|
desc1: getBasename(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'run_command': () => {
|
'run_command': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['run_command']
|
const toolParams = _toolParams as BuiltinToolCallParams['run_command']
|
||||||
return {
|
return {
|
||||||
desc1: `"${toolParams.command}"`,
|
desc1: `"${toolParams.command}"`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'run_persistent_command': () => {
|
'run_persistent_command': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['run_persistent_command']
|
const toolParams = _toolParams as BuiltinToolCallParams['run_persistent_command']
|
||||||
return {
|
return {
|
||||||
desc1: `"${toolParams.command}"`,
|
desc1: `"${toolParams.command}"`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'open_persistent_terminal': () => {
|
'open_persistent_terminal': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['open_persistent_terminal']
|
const toolParams = _toolParams as BuiltinToolCallParams['open_persistent_terminal']
|
||||||
return { desc1: '' }
|
return { desc1: '' }
|
||||||
},
|
},
|
||||||
'kill_persistent_terminal': () => {
|
'kill_persistent_terminal': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal']
|
const toolParams = _toolParams as BuiltinToolCallParams['kill_persistent_terminal']
|
||||||
return { desc1: toolParams.persistentTerminalId }
|
return { desc1: toolParams.persistentTerminalId }
|
||||||
},
|
},
|
||||||
'get_dir_tree': () => {
|
'get_dir_tree': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['get_dir_tree']
|
const toolParams = _toolParams as BuiltinToolCallParams['get_dir_tree']
|
||||||
return {
|
return {
|
||||||
desc1: getFolderName(toolParams.uri.fsPath) ?? '/',
|
desc1: getFolderName(toolParams.uri.fsPath) ?? '/',
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'read_lint_errors': () => {
|
'read_lint_errors': () => {
|
||||||
const toolParams = _toolParams as ToolCallParams['read_lint_errors']
|
const toolParams = _toolParams as BuiltinToolCallParams['read_lint_errors']
|
||||||
return {
|
return {
|
||||||
desc1: getBasename(toolParams.uri.fsPath),
|
desc1: getBasename(toolParams.uri.fsPath),
|
||||||
desc1Info: getRelative(toolParams.uri, accessor),
|
desc1Info: getRelative(toolParams.uri, accessor),
|
||||||
|
|
@ -1590,9 +1616,9 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
const approvalType = approvalTypeOfToolName[toolName]
|
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
|
||||||
const approvalToggle = approvalType ? <div key={approvalType} className="flex items-center ml-2 gap-x-1">
|
const approvalToggle = approvalType ? <div key={approvalType} className="flex items-center ml-2 gap-x-1">
|
||||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc='Auto-approve' />
|
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||||
</div> : null
|
</div> : null
|
||||||
|
|
||||||
return <div className="flex gap-2 mx-0.5 items-center">
|
return <div className="flex gap-2 mx-0.5 items-center">
|
||||||
|
|
@ -1604,7 +1630,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
||||||
|
|
||||||
export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||||
return <div className={`${className ? className : ''} cursor-default select-none`}>
|
return <div className={`${className ? className : ''} cursor-default select-none`}>
|
||||||
<div className='px-2 min-w-full'>
|
<div className='px-2 min-w-full overflow-hidden'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1633,12 +1659,18 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => {
|
const EditToolChildren = ({ uri, code, type }: { uri: URI | undefined, code: string, type: 'diff' | 'rewrite' }) => {
|
||||||
|
|
||||||
|
const content = type === 'diff' ?
|
||||||
|
<VoidDiffEditor uri={uri} searchReplaceBlocks={code} />
|
||||||
|
: <ChatMarkdownRender string={`\`\`\`\n${code}\n\`\`\``} codeURI={uri} chatMessageLocation={undefined} />
|
||||||
|
|
||||||
return <div className='!select-text cursor-auto'>
|
return <div className='!select-text cursor-auto'>
|
||||||
<SmallProseWrapper>
|
<SmallProseWrapper>
|
||||||
<ChatMarkdownRender string={code} codeURI={uri} chatMessageLocation={undefined} />
|
{content}
|
||||||
</SmallProseWrapper>
|
</SmallProseWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1689,9 +1721,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 accessor = useAccessor()
|
||||||
const title = getTitle({ name: toolName, type: 'invalid_params' })
|
const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName })
|
||||||
const desc1 = 'Invalid parameters'
|
const desc1 = 'Invalid parameters'
|
||||||
const icon = null
|
const icon = null
|
||||||
const isError = true
|
const isError = true
|
||||||
|
|
@ -1705,9 +1737,9 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin
|
||||||
return <ToolHeaderWrapper {...componentParams} />
|
return <ToolHeaderWrapper {...componentParams} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
|
const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => {
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
const title = getTitle({ name: toolName, type: 'rejected' })
|
const title = getTitle({ name: toolName, type: 'rejected', mcpServerName })
|
||||||
const desc1 = ''
|
const desc1 = ''
|
||||||
const icon = null
|
const icon = null
|
||||||
const isRejected = true
|
const isRejected = true
|
||||||
|
|
@ -1819,9 +1851,61 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WrapperProps<T extends ToolName> = { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }
|
||||||
|
const MCPToolWrapper = ({ toolMessage }: WrapperProps<string>) => {
|
||||||
|
const accessor = useAccessor()
|
||||||
|
const mcpService = accessor.get('IMCPService')
|
||||||
|
|
||||||
type ResultWrapper<T extends ToolName> = (props: { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode
|
const title = getTitle(toolMessage)
|
||||||
const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>, } } = {
|
const desc1 = toolMessage.name
|
||||||
|
const icon = null
|
||||||
|
|
||||||
|
|
||||||
|
if (toolMessage.type === 'running_now') return null // do not show running
|
||||||
|
|
||||||
|
const isError = false
|
||||||
|
const isRejected = toolMessage.type === 'rejected'
|
||||||
|
const { rawParams, params } = toolMessage
|
||||||
|
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, }
|
||||||
|
|
||||||
|
const paramsStr = JSON.stringify(params, null, 2)
|
||||||
|
componentParams.desc2 = <CopyButton codeStr={paramsStr} toolTipName={`Copy inputs: ${paramsStr}`} />
|
||||||
|
|
||||||
|
componentParams.info = !toolMessage.mcpServerName ? 'MCP tool not found' : undefined
|
||||||
|
|
||||||
|
// Add copy inputs button in desc2
|
||||||
|
|
||||||
|
|
||||||
|
if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') {
|
||||||
|
const { result } = toolMessage
|
||||||
|
const resultStr = result ? mcpService.stringifyResult(result) : 'null'
|
||||||
|
componentParams.children = <ToolChildrenWrapper>
|
||||||
|
<SmallProseWrapper>
|
||||||
|
<ChatMarkdownRender
|
||||||
|
string={`\`\`\`json\n${resultStr}\n\`\`\``}
|
||||||
|
chatMessageLocation={undefined}
|
||||||
|
isApplyEnabled={false}
|
||||||
|
isLinkDetectionEnabled={true}
|
||||||
|
/>
|
||||||
|
</SmallProseWrapper>
|
||||||
|
</ToolChildrenWrapper>
|
||||||
|
}
|
||||||
|
else if (toolMessage.type === 'tool_error') {
|
||||||
|
const { result } = toolMessage
|
||||||
|
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||||
|
<CodeChildren>
|
||||||
|
{result}
|
||||||
|
</CodeChildren>
|
||||||
|
</BottomChildren>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ToolHeaderWrapper {...componentParams} />
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResultWrapper<T extends ToolName> = (props: WrapperProps<T>) => React.ReactNode
|
||||||
|
|
||||||
|
const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper<T>, } } = {
|
||||||
'read_file': {
|
'read_file': {
|
||||||
resultWrapper: ({ toolMessage }) => {
|
resultWrapper: ({ toolMessage }) => {
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
|
|
@ -2257,12 +2341,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
||||||
},
|
},
|
||||||
'rewrite_file': {
|
'rewrite_file': {
|
||||||
resultWrapper: (params) => {
|
resultWrapper: (params) => {
|
||||||
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.newContent}${'\n```'}`} />
|
return <EditTool {...params} content={params.toolMessage.params.newContent} />
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'edit_file': {
|
'edit_file': {
|
||||||
resultWrapper: (params) => {
|
resultWrapper: (params) => {
|
||||||
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.searchReplaceBlocks}${'\n```'}`} />
|
return <EditTool {...params} content={params.toolMessage.params.searchReplaceBlocks} />
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -2442,11 +2526,15 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
||||||
|
|
||||||
if (chatMessage.type === 'invalid_params') {
|
if (chatMessage.type === 'invalid_params') {
|
||||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||||
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} />
|
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} mcpServerName={chatMessage.mcpServerName} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper<ToolName>
|
const toolName = chatMessage.name
|
||||||
|
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||||
|
const ToolResultWrapper = isBuiltInTool ? builtinToolNameToComponent[toolName]?.resultWrapper as ResultWrapper<ToolName>
|
||||||
|
: MCPToolWrapper as ResultWrapper<ToolName>
|
||||||
|
|
||||||
if (ToolResultWrapper)
|
if (ToolResultWrapper)
|
||||||
return <>
|
return <>
|
||||||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||||
|
|
@ -2466,7 +2554,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
||||||
|
|
||||||
else if (role === 'interrupted_streaming_tool') {
|
else if (role === 'interrupted_streaming_tool') {
|
||||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||||
<CanceledTool toolName={chatMessage.name} />
|
<CanceledTool toolName={chatMessage.name} mcpServerName={chatMessage.mcpServerName} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2746,12 +2834,13 @@ const CommandBarInChat = () => {
|
||||||
|
|
||||||
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
|
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
|
||||||
|
|
||||||
|
if (!isABuiltinToolName(toolCallSoFar.name)) return null
|
||||||
|
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
|
|
||||||
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
|
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
|
||||||
|
|
||||||
const title = titleOfToolName[toolCallSoFar.name].proposed
|
const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed
|
||||||
|
|
||||||
const uriDone = toolCallSoFar.doneParams.includes('uri')
|
const uriDone = toolCallSoFar.doneParams.includes('uri')
|
||||||
const desc1 = <span className='flex items-center'>
|
const desc1 = <span className='flex items-center'>
|
||||||
|
|
@ -2772,12 +2861,11 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
|
||||||
<EditToolChildren
|
<EditToolChildren
|
||||||
uri={uri}
|
uri={uri}
|
||||||
code={toolCallSoFar.rawParams.search_replace_blocks ?? toolCallSoFar.rawParams.new_content ?? ''}
|
code={toolCallSoFar.rawParams.search_replace_blocks ?? toolCallSoFar.rawParams.new_content ?? ''}
|
||||||
|
type={'rewrite'} // as it streams, show in rewrite format, don't make a diff editor
|
||||||
/>
|
/>
|
||||||
<IconLoading />
|
<IconLoading />
|
||||||
</ToolHeaderWrapper>
|
</ToolHeaderWrapper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ import { URI } from '../../../../../../../base/common/uri.js';
|
||||||
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
|
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
|
||||||
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
||||||
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
||||||
|
import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
|
||||||
|
import { extractSearchReplaceBlocks, ExtractedSearchReplaceBlock } from '../../../../common/helpers/extractCodeFromResult.js';
|
||||||
|
import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
||||||
|
import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js';
|
||||||
|
import { detectLanguage } from '../../../../common/helpers/languageHelpers.js';
|
||||||
|
|
||||||
|
|
||||||
// type guard
|
// type guard
|
||||||
|
|
@ -951,11 +956,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||||
|
|
||||||
const contextViewProvider = accessor.get('IContextViewService')
|
const contextViewProvider = accessor.get('IContextViewService')
|
||||||
return <WidgetComponent
|
return <WidgetComponent
|
||||||
ctor={InputBox}
|
|
||||||
className='
|
className='
|
||||||
bg-void-bg-1
|
bg-void-bg-1
|
||||||
@@void-force-child-placeholder-void-fg-1
|
@@void-force-child-placeholder-void-fg-1
|
||||||
'
|
'
|
||||||
|
ctor={InputBox}
|
||||||
propsFn={useCallback((container) => [
|
propsFn={useCallback((container) => [
|
||||||
container,
|
container,
|
||||||
contextViewProvider,
|
contextViewProvider,
|
||||||
|
|
@ -991,8 +996,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||||
inputBoxRef.current = instance;
|
inputBoxRef.current = instance;
|
||||||
|
|
||||||
return disposables
|
return disposables
|
||||||
}, [onChangeText, onCreateInstance, inputBoxRef])
|
}, [onChangeText, onCreateInstance, inputBoxRef])}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1841,3 +1845,154 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: {
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const SingleDiffEditor = ({ block, lang }: { block: ExtractedSearchReplaceBlock, lang: string | undefined }) => {
|
||||||
|
const accessor = useAccessor();
|
||||||
|
const modelService = accessor.get('IModelService');
|
||||||
|
const instantiationService = accessor.get('IInstantiationService');
|
||||||
|
const languageService = accessor.get('ILanguageService');
|
||||||
|
|
||||||
|
const languageSelection = useMemo(() => languageService.createById(lang), [lang, languageService]);
|
||||||
|
|
||||||
|
// Create models for original and modified
|
||||||
|
const originalModel = useMemo(() =>
|
||||||
|
modelService.createModel(block.orig, languageSelection),
|
||||||
|
[block.orig, languageSelection, modelService]
|
||||||
|
);
|
||||||
|
const modifiedModel = useMemo(() =>
|
||||||
|
modelService.createModel(block.final, languageSelection),
|
||||||
|
[block.final, languageSelection, modelService]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up models on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
originalModel.dispose();
|
||||||
|
modifiedModel.dispose();
|
||||||
|
};
|
||||||
|
}, [originalModel, modifiedModel]);
|
||||||
|
|
||||||
|
// Imperatively mount the DiffEditorWidget
|
||||||
|
const divRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!divRef.current) return;
|
||||||
|
// Create the diff editor instance
|
||||||
|
const editor = instantiationService.createInstance(
|
||||||
|
DiffEditorWidget,
|
||||||
|
divRef.current,
|
||||||
|
{
|
||||||
|
automaticLayout: true,
|
||||||
|
readOnly: true,
|
||||||
|
renderSideBySide: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
lineNumbers: 'off',
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'auto',
|
||||||
|
verticalScrollbarSize: 0,
|
||||||
|
horizontalScrollbarSize: 8,
|
||||||
|
alwaysConsumeMouseWheel: false,
|
||||||
|
ignoreHorizontalScrollbarInContentHeight: true,
|
||||||
|
},
|
||||||
|
hover: { enabled: false },
|
||||||
|
folding: false,
|
||||||
|
selectionHighlight: false,
|
||||||
|
renderLineHighlight: 'none',
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
hideCursorInOverviewRuler: true,
|
||||||
|
overviewRulerBorder: false,
|
||||||
|
glyphMargin: false,
|
||||||
|
stickyScroll: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
renderGutterMenu: false,
|
||||||
|
renderIndicators: false,
|
||||||
|
},
|
||||||
|
{ originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } }
|
||||||
|
);
|
||||||
|
editor.setModel({ original: originalModel, modified: modifiedModel });
|
||||||
|
|
||||||
|
// Calculate the height based on content
|
||||||
|
const updateHeight = () => {
|
||||||
|
const contentHeight = Math.max(
|
||||||
|
originalModel.getLineCount() * 19, // approximate line height
|
||||||
|
modifiedModel.getLineCount() * 19
|
||||||
|
) + 19 * 2 + 1; // add padding
|
||||||
|
|
||||||
|
// Set reasonable min/max heights
|
||||||
|
const height = Math.min(Math.max(contentHeight, 100), 300);
|
||||||
|
if (divRef.current) {
|
||||||
|
divRef.current.style.height = `${height}px`;
|
||||||
|
editor.layout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateHeight();
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Update height when content changes
|
||||||
|
const disposable1 = originalModel.onDidChangeContent(() => updateHeight());
|
||||||
|
const disposable2 = modifiedModel.onDidChangeContent(() => updateHeight());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposable1.dispose();
|
||||||
|
disposable2.dispose();
|
||||||
|
editor.dispose();
|
||||||
|
editorRef.current = null;
|
||||||
|
};
|
||||||
|
}, [originalModel, modifiedModel, instantiationService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-void-bg-3 @@bg-editor-style-override" ref={divRef} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks.
|
||||||
|
* Props:
|
||||||
|
* - uri: URI of the file (for language detection, etc)
|
||||||
|
* - searchReplaceBlocks: string in search/replace format (from LLM)
|
||||||
|
* - language?: string (optional, fallback to 'plaintext')
|
||||||
|
*/
|
||||||
|
export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => {
|
||||||
|
const accessor = useAccessor();
|
||||||
|
const languageService = accessor.get('ILanguageService');
|
||||||
|
|
||||||
|
// Extract all blocks
|
||||||
|
const blocks = extractSearchReplaceBlocks(searchReplaceBlocks);
|
||||||
|
|
||||||
|
// Use detectLanguage for language detection if not provided
|
||||||
|
let lang = language;
|
||||||
|
if (!lang && blocks.length > 0) {
|
||||||
|
lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: blocks[0].orig });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no blocks, show empty state
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
return <div className="w-full p-4 text-void-fg-4 text-sm">No changes found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display all blocks
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-2">
|
||||||
|
{blocks.map((block, index) => (
|
||||||
|
<div key={index} className="w-full">
|
||||||
|
{blocks.length > 1 && (
|
||||||
|
<div className="text-void-fg-4 text-xs mb-1 px-1">
|
||||||
|
Change {index + 1} of {blocks.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SingleDiffEditor block={block} lang={lang} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*--------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||||
|
|
@ -51,6 +51,7 @@ import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService
|
||||||
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
||||||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||||
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
|
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
|
||||||
|
import { IMCPService } from '../../../../common/mcpService.js';
|
||||||
|
|
||||||
|
|
||||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||||
|
|
@ -78,6 +79,8 @@ const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) =>
|
||||||
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
|
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
|
||||||
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
|
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
|
||||||
|
|
||||||
|
const mcpListeners: Set<() => void> = new Set()
|
||||||
|
|
||||||
|
|
||||||
// must call this before you can use any of the hooks below
|
// must call this before you can use any of the hooks below
|
||||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||||
|
|
@ -95,9 +98,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||||
editCodeService: accessor.get(IEditCodeService),
|
editCodeService: accessor.get(IEditCodeService),
|
||||||
voidCommandBarService: accessor.get(IVoidCommandBarService),
|
voidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||||
modelService: accessor.get(IModelService),
|
modelService: accessor.get(IModelService),
|
||||||
|
mcpService: accessor.get(IMCPService),
|
||||||
}
|
}
|
||||||
|
|
||||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
|
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -164,6 +168,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
disposables.push(
|
||||||
|
mcpService.onDidChangeState(() => {
|
||||||
|
mcpListeners.forEach(l => l())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return disposables
|
return disposables
|
||||||
|
|
@ -215,6 +224,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||||
ITerminalService: accessor.get(ITerminalService),
|
ITerminalService: accessor.get(ITerminalService),
|
||||||
IExtensionManagementService: accessor.get(IExtensionManagementService),
|
IExtensionManagementService: accessor.get(IExtensionManagementService),
|
||||||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||||
|
IMCPService: accessor.get(IMCPService),
|
||||||
|
|
||||||
} as const
|
} as const
|
||||||
return reactAccessor
|
return reactAccessor
|
||||||
|
|
@ -376,3 +386,16 @@ export const useActiveURI = () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const useMCPServiceState = () => {
|
||||||
|
const accessor = useAccessor()
|
||||||
|
const mcpService = accessor.get('IMCPService')
|
||||||
|
const [s, ss] = useState(mcpService.state)
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = () => { ss(mcpService.state) }
|
||||||
|
mcpListeners.add(listener);
|
||||||
|
return () => { mcpListeners.delete(listener) };
|
||||||
|
}, []);
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsSer
|
||||||
import Severity from '../../../../../../../base/common/severity.js'
|
import Severity from '../../../../../../../base/common/severity.js'
|
||||||
import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js';
|
import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js';
|
||||||
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
|
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
|
||||||
|
import { MCPServer } from '../../../../common/mcpServiceTypes.js';
|
||||||
|
import { useMCPServiceState } from '../util/services.js';
|
||||||
|
|
||||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||||
|
|
||||||
|
|
@ -454,7 +456,7 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
|
||||||
{/* left part is width:full */}
|
{/* left part is width:full */}
|
||||||
<div className={`flex flex-grow items-center gap-4`}>
|
<div className={`flex flex-grow items-center gap-4`}>
|
||||||
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
|
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
|
||||||
<span className='w-fit truncate'>{modelName}</span>
|
<span className='w-fit max-w-[400px] truncate'>{modelName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* right part is anything that fits */}
|
{/* right part is anything that fits */}
|
||||||
|
|
@ -493,7 +495,15 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
|
||||||
|
|
||||||
{/* X button */}
|
{/* X button */}
|
||||||
<div className={`w-5 flex items-center justify-center`}>
|
<div className={`w-5 flex items-center justify-center`}>
|
||||||
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}><X className="size-4" /></button>}
|
{type === 'default' || type === 'autodetected' ? null : <button
|
||||||
|
onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}
|
||||||
|
data-tooltip-id='void-tooltip'
|
||||||
|
data-tooltip-place='right'
|
||||||
|
data-tooltip-content='Delete'
|
||||||
|
className={`${hasOverrides ? '' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}
|
||||||
|
>
|
||||||
|
<X size={12} className="text-void-fg-3 opacity-50" />
|
||||||
|
</button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -898,6 +908,110 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
||||||
|
|
||||||
// full settings
|
// full settings
|
||||||
|
|
||||||
|
// MCP Server component
|
||||||
|
const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer }) => {
|
||||||
|
const accessor = useAccessor();
|
||||||
|
const mcpService = accessor.get('IMCPService');
|
||||||
|
|
||||||
|
const voidSettings = useSettingsState()
|
||||||
|
const isOn = voidSettings.mcpUserStateOfName[name]?.isOn
|
||||||
|
|
||||||
|
const removeUniquePrefix = (name: string) => name.split('_').slice(1).join('_')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b border-gray-800 bg-gray-300/10 py-4 rounded-lg ">
|
||||||
|
<div className="flex items-center mx-4">
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div className={`w-2 h-2 rounded-full mr-2
|
||||||
|
${server.status === 'success' ? 'bg-green-500'
|
||||||
|
: server.status === 'error' ? 'bg-red-500'
|
||||||
|
: server.status === 'loading' ? 'bg-yellow-500'
|
||||||
|
: server.status === 'offline' ? 'bg-gray-500'
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
`}></div>
|
||||||
|
|
||||||
|
{/* Server name */}
|
||||||
|
<div className="text-sm font-medium mr-2">{name}</div>
|
||||||
|
|
||||||
|
{/* Power toggle switch */}
|
||||||
|
<div className="ml-auto mb-2">
|
||||||
|
<VoidSwitch
|
||||||
|
value={isOn ?? false}
|
||||||
|
size='sm'
|
||||||
|
disabled={server.status === 'error'}
|
||||||
|
onChange={() => mcpService.toggleServerIsOn(name, !isOn)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tools section */}
|
||||||
|
<div className="mt-1 mx-4">
|
||||||
|
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto pb-1">
|
||||||
|
{isOn && (server.tools ?? []).length > 0 ? (
|
||||||
|
(server.tools ?? []).map((tool: { name: string; description?: string }) => (
|
||||||
|
<span
|
||||||
|
key={tool.name}
|
||||||
|
className="px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded text-xs"
|
||||||
|
|
||||||
|
data-tooltip-id='void-tooltip'
|
||||||
|
data-tooltip-content={tool.description || ''}
|
||||||
|
data-tooltip-class-name='void-max-w-[300px]'
|
||||||
|
>
|
||||||
|
{removeUniquePrefix(tool.name)}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-500">No tools available</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Command badge */}
|
||||||
|
{isOn && server.command && (
|
||||||
|
<div className="mt-2 mx-4">
|
||||||
|
<div className="text-xs text-gray-400">Command:</div>
|
||||||
|
<div className="px-2 py-1 bg-void-bg-3 text-xs font-mono overflow-x-auto whitespace-nowrap">
|
||||||
|
{server.command}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error message if present */}
|
||||||
|
{server.error && (<WarningBox className='ml-4' text={server.error} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main component that renders the list of servers
|
||||||
|
const MCPServersList = () => {
|
||||||
|
const mcpServiceState = useMCPServiceState()
|
||||||
|
|
||||||
|
let content: React.ReactNode
|
||||||
|
if (mcpServiceState.error) {
|
||||||
|
content = <div className="text-red-500 text-sm font-medium">
|
||||||
|
{mcpServiceState.error}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const entries = Object.entries(mcpServiceState.mcpServerOfName)
|
||||||
|
if (entries.length === 0) {
|
||||||
|
content = <div className="text-red-500 text-sm font-medium">
|
||||||
|
No servers found
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
content = entries.map(([name, server]) => (
|
||||||
|
<div className="py-2" key={name}>
|
||||||
|
<MCPServerComponent name={name} server={server} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
};
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const isDark = useIsDark()
|
const isDark = useIsDark()
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
|
|
@ -908,6 +1022,7 @@ export const Settings = () => {
|
||||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||||
const chatThreadsService = accessor.get('IChatThreadService')
|
const chatThreadsService = accessor.get('IChatThreadService')
|
||||||
const notificationService = accessor.get('INotificationService')
|
const notificationService = accessor.get('INotificationService')
|
||||||
|
const mcpService = accessor.get('IMCPService')
|
||||||
|
|
||||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||||
let dataStr: string
|
let dataStr: string
|
||||||
|
|
@ -1033,7 +1148,7 @@ export const Settings = () => {
|
||||||
className='hover:brightness-110'
|
className='hover:brightness-110'
|
||||||
data-tooltip-id='void-tooltip'
|
data-tooltip-id='void-tooltip'
|
||||||
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
||||||
data-tooltip-class-name='void-max-w-[20px]'
|
data-tooltip-class-name='void-max-w-[300px]'
|
||||||
>
|
>
|
||||||
Only works with FIM models.*
|
Only works with FIM models.*
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1233,6 +1348,7 @@ export const Settings = () => {
|
||||||
|
|
||||||
<div className='mt-12 max-w-[600px]'>
|
<div className='mt-12 max-w-[600px]'>
|
||||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||||
|
|
||||||
<h4 className={`text-void-fg-3 mb-4`}>
|
<h4 className={`text-void-fg-3 mb-4`}>
|
||||||
<ChatMarkdownRender inPTag={true} string={`
|
<ChatMarkdownRender inPTag={true} string={`
|
||||||
System instructions to include with all AI requests.
|
System instructions to include with all AI requests.
|
||||||
|
|
@ -1243,6 +1359,24 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
||||||
<AIInstructionsBox />
|
<AIInstructionsBox />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12 max-w-[600px]'>
|
||||||
|
<h2 className='text-3xl mb-2'>MCP</h2>
|
||||||
|
<h4 className={`text-void-fg-3 mb-4`}>
|
||||||
|
<ChatMarkdownRender inPTag={true} string={`
|
||||||
|
Use Model Context Protocol to provide Agent mode with more tools.
|
||||||
|
`} chatMessageLocation={undefined} />
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
<VoidButtonBgDarken className='px-4 py-1 mb-2 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
|
||||||
|
Add MCP Server
|
||||||
|
</VoidButtonBgDarken>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ErrorBoundary>
|
||||||
|
<MCPServersList />
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ export const VoidTooltip = () => {
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
|
max-width: 300px;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
#void-tooltip {
|
#void-tooltip {
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ registerAction2(class extends Action2 {
|
||||||
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
|
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
|
||||||
|
|
||||||
const oldSelns = oldThread?.state.stagingSelections
|
const oldSelns = oldThread?.state.stagingSelections
|
||||||
const oldVal = oldUI?.textAreaRef.current?.value
|
const oldVal = oldUI?.textAreaRef?.current?.value
|
||||||
|
|
||||||
// open and focus new thread
|
// open and focus new thread
|
||||||
chatThreadsService.openNewThread()
|
chatThreadsService.openNewThread()
|
||||||
|
|
|
||||||
|
|
@ -346,20 +346,20 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
||||||
await Promise.any([waitUntilDone, waitUntilInterrupt])
|
await Promise.any([waitUntilDone, waitUntilInterrupt])
|
||||||
.finally(() => disposables.forEach(d => d.dispose()))
|
.finally(() => disposables.forEach(d => d.dispose()))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
||||||
|
if (resolveReason?.type === 'timeout') {
|
||||||
|
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
||||||
|
result = await this.readTerminal(terminalId)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPersistent) {
|
if (!isPersistent) {
|
||||||
interrupt()
|
interrupt()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||||
|
|
||||||
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
|
||||||
if (resolveReason.type === 'timeout') {
|
|
||||||
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
|
||||||
result = await this.readTerminal(terminalId)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!isPersistent) result = `$ ${command}\n${result}`
|
if (!isPersistent) result = `$ ${command}\n${result}`
|
||||||
result = removeAnsiEscapeCodes(result)
|
result = removeAnsiEscapeCodes(result)
|
||||||
// trim
|
// trim
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
||||||
import { ISearchService } from '../../../services/search/common/search.js'
|
import { ISearchService } from '../../../services/search/common/search.js'
|
||||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||||
import { ITerminalToolService } from './terminalToolService.js'
|
import { ITerminalToolService } from './terminalToolService.js'
|
||||||
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
|
import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType, BuiltinToolName } from '../common/toolsServiceTypes.js'
|
||||||
import { IVoidModelService } from '../common/voidModelService.js'
|
import { IVoidModelService } from '../common/voidModelService.js'
|
||||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||||
|
|
@ -16,20 +16,15 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
|
||||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||||
import { timeout } from '../../../../base/common/async.js'
|
import { timeout } from '../../../../base/common/async.js'
|
||||||
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
|
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'
|
||||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||||
import { generateUuid } from '../../../../base/common/uuid.js'
|
import { generateUuid } from '../../../../base/common/uuid.js'
|
||||||
|
|
||||||
|
|
||||||
// tool use for AI
|
// tool use for AI
|
||||||
|
type ValidateBuiltinParams = { [T in BuiltinToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] }
|
||||||
|
type CallBuiltinTool = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise<BuiltinToolResultType[T]>, interruptTool?: () => void }> }
|
||||||
|
type BuiltinToolResultToString = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T], result: Awaited<BuiltinToolResultType[T]>) => string }
|
||||||
|
|
||||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
|
||||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
|
|
||||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const isFalsy = (u: unknown) => {
|
const isFalsy = (u: unknown) => {
|
||||||
|
|
@ -110,9 +105,9 @@ const checkIfIsFolder = (uriStr: string) => {
|
||||||
|
|
||||||
export interface IToolsService {
|
export interface IToolsService {
|
||||||
readonly _serviceBrand: undefined;
|
readonly _serviceBrand: undefined;
|
||||||
validateParams: ValidateParams;
|
validateParams: ValidateBuiltinParams;
|
||||||
callTool: CallTool;
|
callTool: CallBuiltinTool;
|
||||||
stringOfResult: ToolResultToString;
|
stringOfResult: BuiltinToolResultToString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||||
|
|
@ -121,9 +116,9 @@ export class ToolsService implements IToolsService {
|
||||||
|
|
||||||
readonly _serviceBrand: undefined;
|
readonly _serviceBrand: undefined;
|
||||||
|
|
||||||
public validateParams: ValidateParams;
|
public validateParams: ValidateBuiltinParams;
|
||||||
public callTool: CallTool;
|
public callTool: CallBuiltinTool;
|
||||||
public stringOfResult: ToolResultToString;
|
public stringOfResult: BuiltinToolResultToString;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@IFileService fileService: IFileService,
|
@IFileService fileService: IFileService,
|
||||||
|
|
@ -446,7 +441,6 @@ export class ToolsService implements IToolsService {
|
||||||
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
|
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
|
||||||
return { result: {} }
|
return { result: {} }
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -550,7 +544,6 @@ export class ToolsService implements IToolsService {
|
||||||
kill_persistent_terminal: (params, _result) => {
|
kill_persistent_terminal: (params, _result) => {
|
||||||
return `Successfully closed terminal "${params.persistentTerminalId}".`;
|
return `Successfully closed terminal "${params.persistentTerminalId}".`;
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,32 @@
|
||||||
|
|
||||||
import { URI } from '../../../../base/common/uri.js';
|
import { URI } from '../../../../base/common/uri.js';
|
||||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||||
import { ToolName } from './prompt/prompts.js';
|
|
||||||
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
|
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||||
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
import { ToolCallParams, ToolName, ToolResult } from './toolsServiceTypes.js';
|
||||||
|
|
||||||
export type ToolMessage<T extends ToolName> = {
|
export type ToolMessage<T extends ToolName> = {
|
||||||
role: 'tool';
|
role: 'tool';
|
||||||
content: string; // give this result to LLM (string of value)
|
content: string; // give this result to LLM (string of value)
|
||||||
id: string;
|
id: string;
|
||||||
rawParams: RawToolParamsObj;
|
rawParams: RawToolParamsObj;
|
||||||
|
mcpServerName: string | undefined; // the server name at the time of the call
|
||||||
} & (
|
} & (
|
||||||
// in order of events:
|
// in order of events:
|
||||||
| { type: 'invalid_params', result: null, name: T, }
|
| { type: 'invalid_params', result: null, name: T, }
|
||||||
|
|
||||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
| { type: 'tool_request', result: null, name: T, params: ToolCallParams<T>, } // params were validated, awaiting user
|
||||||
|
|
||||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
|
| { type: 'running_now', result: null, name: T, params: ToolCallParams<T>, }
|
||||||
|
|
||||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
| { type: 'tool_error', result: string, name: T, params: ToolCallParams<T>, } // error when tool was running
|
||||||
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
|
| { type: 'success', result: Awaited<ToolResult<T>>, name: T, params: ToolCallParams<T>, }
|
||||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
|
| { type: 'rejected', result: null, name: T, params: ToolCallParams<T> }
|
||||||
) // user rejected
|
) // user rejected
|
||||||
|
|
||||||
export type DecorativeCanceledTool = {
|
export type DecorativeCanceledTool = {
|
||||||
role: 'interrupted_streaming_tool';
|
role: 'interrupted_streaming_tool';
|
||||||
name: ToolName;
|
name: ToolName;
|
||||||
|
mcpServerName: string | undefined; // the server name at the time of the call
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
||||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
|
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
|
||||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
import { ShallowDirectoryItem, BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js';
|
||||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
|
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ export const computeDirectoryTree1Deep = async (
|
||||||
fileService: IFileService,
|
fileService: IFileService,
|
||||||
rootURI: URI,
|
rootURI: URI,
|
||||||
pageNumber: number = 1,
|
pageNumber: number = 1,
|
||||||
): Promise<ToolResultType['ls_dir']> => {
|
): Promise<BuiltinToolResultType['ls_dir']> => {
|
||||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||||
if (!stat.isDirectory) {
|
if (!stat.isDirectory) {
|
||||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||||
|
|
@ -107,7 +107,7 @@ export const computeDirectoryTree1Deep = async (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_dir'], result: BuiltinToolResultType['ls_dir']): string => {
|
||||||
if (!result.children) {
|
if (!result.children) {
|
||||||
return `Error: ${params.uri} is not a directory`;
|
return `Error: ${params.uri} is not a directory`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
360
src/vs/workbench/contrib/void/common/mcpService.ts
Normal file
360
src/vs/workbench/contrib/void/common/mcpService.ts
Normal file
|
|
@ -0,0 +1,360 @@
|
||||||
|
/*--------------------------------------------------------------------------------------
|
||||||
|
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||||
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { URI } from '../../../../base/common/uri.js';
|
||||||
|
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||||
|
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||||
|
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||||
|
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||||
|
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||||
|
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||||
|
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||||
|
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||||
|
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||||
|
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||||
|
import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, RawMCPToolCall, MCPServerEventResponse } from './mcpServiceTypes.js';
|
||||||
|
import { Event, Emitter } from '../../../../base/common/event.js';
|
||||||
|
import { InternalToolInfo } from './prompt/prompts.js';
|
||||||
|
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||||
|
import { MCPUserStateOfName } from './voidSettingsTypes.js';
|
||||||
|
|
||||||
|
|
||||||
|
type MCPServiceState = {
|
||||||
|
mcpServerOfName: MCPServerOfName,
|
||||||
|
error: string | undefined, // global parsing error
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMCPService {
|
||||||
|
readonly _serviceBrand: undefined;
|
||||||
|
revealMCPConfigFile(): Promise<void>;
|
||||||
|
toggleServerIsOn(serverName: string, isOn: boolean): Promise<void>;
|
||||||
|
|
||||||
|
readonly state: MCPServiceState; // NOT persisted
|
||||||
|
onDidChangeState: Event<void>;
|
||||||
|
|
||||||
|
getMCPTools(): InternalToolInfo[] | undefined;
|
||||||
|
callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>;
|
||||||
|
stringifyResult(result: RawMCPToolCall): string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IMCPService = createDecorator<IMCPService>('mcpConfigService');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const MCP_CONFIG_FILE_NAME = 'mcp.json';
|
||||||
|
const MCP_CONFIG_SAMPLE = { mcpServers: {} }
|
||||||
|
const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2);
|
||||||
|
|
||||||
|
|
||||||
|
// export interface MCPCallToolOfToolName {
|
||||||
|
// [toolName: string]: (params: any) => Promise<{
|
||||||
|
// result: any | Promise<any>,
|
||||||
|
// interruptTool?: () => void
|
||||||
|
// }>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
class MCPService extends Disposable implements IMCPService {
|
||||||
|
_serviceBrand: undefined;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly channel: IChannel // MCPChannel
|
||||||
|
|
||||||
|
// list of MCP servers pulled from mcpChannel
|
||||||
|
state: MCPServiceState = {
|
||||||
|
mcpServerOfName: {},
|
||||||
|
error: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emitters for server events
|
||||||
|
private readonly _onDidChangeState = new Emitter<void>();
|
||||||
|
public readonly onDidChangeState = this._onDidChangeState.event;
|
||||||
|
|
||||||
|
// private readonly _onLoadingServersChange = new Emitter<MCPServerEventLoadingParam>();
|
||||||
|
// public readonly onLoadingServersChange = this._onLoadingServersChange.event;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@IFileService private readonly fileService: IFileService,
|
||||||
|
@IPathService private readonly pathService: IPathService,
|
||||||
|
@IProductService private readonly productService: IProductService,
|
||||||
|
@IEditorService private readonly editorService: IEditorService,
|
||||||
|
@IMainProcessService private readonly mainProcessService: IMainProcessService,
|
||||||
|
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.channel = this.mainProcessService.getChannel('void-channel-mcp')
|
||||||
|
|
||||||
|
|
||||||
|
const onEvent = (e: MCPServerEventResponse) => {
|
||||||
|
console.log('GOT EVENT', e)
|
||||||
|
this._setMCPServerState(e.response.name, e.response.newServer)
|
||||||
|
}
|
||||||
|
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||||
|
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||||
|
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||||
|
|
||||||
|
this._initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async _initialize() {
|
||||||
|
try {
|
||||||
|
await this.voidSettingsService.waitForInitState;
|
||||||
|
|
||||||
|
// Create .mcpConfig if it doesn't exist
|
||||||
|
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||||
|
const fileExists = await this._configFileExists(mcpConfigUri);
|
||||||
|
if (!fileExists) {
|
||||||
|
await this._createMCPConfigFile(mcpConfigUri);
|
||||||
|
console.log('MCP Config file created:', mcpConfigUri.toString());
|
||||||
|
}
|
||||||
|
await this._addMCPConfigFileWatcher();
|
||||||
|
await this._refreshMCPServers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing MCPService:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => {
|
||||||
|
if (newServer === undefined) {
|
||||||
|
// Remove the server from the state
|
||||||
|
const { [serverName]: removed, ...remainingServers } = this.state.mcpServerOfName;
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
mcpServerOfName: remainingServers
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add or update the server
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
mcpServerOfName: {
|
||||||
|
...this.state.mcpServerOfName,
|
||||||
|
[serverName]: newServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._onDidChangeState.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _setHasError = async (errMsg: string | undefined) => {
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
error: errMsg,
|
||||||
|
}
|
||||||
|
this._onDidChangeState.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file/directory if it doesn't exist
|
||||||
|
private async _createMCPConfigFile(mcpConfigUri: URI): Promise<void> {
|
||||||
|
await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path }));
|
||||||
|
const buffer = VSBuffer.fromString(MCP_CONFIG_SAMPLE_STRING);
|
||||||
|
await this.fileService.writeFile(mcpConfigUri, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async _addMCPConfigFileWatcher(): Promise<void> {
|
||||||
|
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||||
|
this._register(
|
||||||
|
this.fileService.watch(mcpConfigUri)
|
||||||
|
)
|
||||||
|
|
||||||
|
this._register(this.fileService.onDidFilesChange(async e => {
|
||||||
|
if (!e.contains(mcpConfigUri)) return
|
||||||
|
await this._refreshMCPServers();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side functions
|
||||||
|
|
||||||
|
public async revealMCPConfigFile(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||||
|
await this.editorService.openEditor({
|
||||||
|
resource: mcpConfigUri,
|
||||||
|
options: {
|
||||||
|
pinned: true,
|
||||||
|
revealIfOpened: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening MCP config file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMCPTools(): InternalToolInfo[] | undefined {
|
||||||
|
const allTools: InternalToolInfo[] = []
|
||||||
|
for (const serverName in this.state.mcpServerOfName) {
|
||||||
|
const server = this.state.mcpServerOfName[serverName];
|
||||||
|
server.tools?.forEach(tool => {
|
||||||
|
allTools.push({
|
||||||
|
description: tool.description || '',
|
||||||
|
params: this._transformInputSchemaToParams(tool.inputSchema),
|
||||||
|
name: tool.name,
|
||||||
|
mcpServerName: serverName,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (allTools.length === 0) return undefined
|
||||||
|
return allTools
|
||||||
|
}
|
||||||
|
|
||||||
|
private _transformInputSchemaToParams(inputSchema?: Record<string, any>): { [paramName: string]: { description: string } } {
|
||||||
|
|
||||||
|
// Check if inputSchema is valid
|
||||||
|
if (!inputSchema || !inputSchema.properties) return {};
|
||||||
|
|
||||||
|
const params: { [paramName: string]: { description: string } } = {};
|
||||||
|
Object.keys(inputSchema.properties).forEach(paramName => {
|
||||||
|
const propertyValues = inputSchema.properties[paramName];
|
||||||
|
|
||||||
|
// Check if propertyValues is not an object
|
||||||
|
if (typeof propertyValues !== 'object') {
|
||||||
|
console.warn(`Invalid property value for ${paramName}: expected object, got ${typeof propertyValues}`);
|
||||||
|
return; // in forEach the return is equivalent to continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the parameter to the params object
|
||||||
|
params[paramName] = {
|
||||||
|
description: JSON.stringify(propertyValues.description || '', null, 2) || '',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getMCPConfigFilePath(): Promise<URI> {
|
||||||
|
const appName = this.productService.dataFolderName
|
||||||
|
const userHome = await this.pathService.userHome();
|
||||||
|
const uri = URI.joinPath(userHome, appName, MCP_CONFIG_FILE_NAME)
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _configFileExists(mcpConfigUri: URI): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.fileService.stat(mcpConfigUri);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async _parseMCPConfigFile(): Promise<MCPConfigFileJSON | null> {
|
||||||
|
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||||
|
try {
|
||||||
|
const fileContent = await this.fileService.readFile(mcpConfigUri);
|
||||||
|
const contentString = fileContent.value.toString();
|
||||||
|
const configFileJson = JSON.parse(contentString);
|
||||||
|
if (!configFileJson.mcpServers) {
|
||||||
|
throw new Error('Missing mcpServers property');
|
||||||
|
}
|
||||||
|
return configFileJson as MCPConfigFileJSON;
|
||||||
|
} catch (error) {
|
||||||
|
const fullError = `Error parsing MCP config file: ${error}`;
|
||||||
|
this._setHasError(fullError)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Handle server state changes
|
||||||
|
private async _refreshMCPServers(): Promise<void> {
|
||||||
|
|
||||||
|
this._setHasError(undefined)
|
||||||
|
|
||||||
|
const newConfigFileJSON = await this._parseMCPConfigFile();
|
||||||
|
if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return }
|
||||||
|
if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
|
||||||
|
|
||||||
|
|
||||||
|
const oldConfigFileNames = Object.keys(this.state.mcpServerOfName)
|
||||||
|
const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers)
|
||||||
|
|
||||||
|
const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old
|
||||||
|
const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new
|
||||||
|
|
||||||
|
// set isOn to any new servers in the config
|
||||||
|
const addedUserStateOfName: MCPUserStateOfName = {}
|
||||||
|
for (const name of addedServerNames) { addedUserStateOfName[name] = { isOn: true } }
|
||||||
|
await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName);
|
||||||
|
|
||||||
|
// delete isOn for any servers that no longer show up in the config
|
||||||
|
await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames);
|
||||||
|
|
||||||
|
// set all servers to loading
|
||||||
|
for (const serverName in newConfigFileJSON.mcpServers) {
|
||||||
|
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||||
|
}
|
||||||
|
const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName))
|
||||||
|
|
||||||
|
this.channel.call('refreshMCPServers', {
|
||||||
|
mcpConfigFileJSON: newConfigFileJSON,
|
||||||
|
addedServerNames,
|
||||||
|
removedServerNames,
|
||||||
|
updatedServerNames,
|
||||||
|
userStateOfName: this.voidSettingsService.state.mcpUserStateOfName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stringifyResult(result: RawMCPToolCall): string {
|
||||||
|
let toolResultStr: string
|
||||||
|
if (result.event === 'text') {
|
||||||
|
toolResultStr = result.text
|
||||||
|
} else if (result.event === 'image') {
|
||||||
|
toolResultStr = `[Image: ${result.image.mimeType}]`
|
||||||
|
} else if (result.event === 'audio') {
|
||||||
|
toolResultStr = `[Audio content]`
|
||||||
|
} else if (result.event === 'resource') {
|
||||||
|
toolResultStr = `[Resource content]`
|
||||||
|
} else {
|
||||||
|
toolResultStr = JSON.stringify(result)
|
||||||
|
}
|
||||||
|
return toolResultStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle MCP server and update isOn in void settings
|
||||||
|
public async toggleServerIsOn(serverName: string, isOn: boolean): Promise<void> {
|
||||||
|
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||||
|
|
||||||
|
await this.voidSettingsService.setMCPServerState(serverName, { isOn });
|
||||||
|
this.channel.call('toggleMCPServer', { serverName, isOn })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
|
||||||
|
const result = await this.channel.call<RawMCPToolCall>('callTool', toolData);
|
||||||
|
if (result.event === 'error') {
|
||||||
|
throw new Error(`Error: ${result.text}`)
|
||||||
|
}
|
||||||
|
return { result };
|
||||||
|
}
|
||||||
|
|
||||||
|
// public getMCPToolFns(): MCPToolResultType {
|
||||||
|
// const tools = this.getMCPTools();
|
||||||
|
// const toolFns: MCPToolResultType = {};
|
||||||
|
|
||||||
|
// tools.forEach((tool) => {
|
||||||
|
// const name = tool.name;
|
||||||
|
// // Define the tool call function
|
||||||
|
// const toolFn = async (params: {
|
||||||
|
// serverName: string,
|
||||||
|
// toolName: string,
|
||||||
|
// args: any
|
||||||
|
// }) => {
|
||||||
|
// const { serverName, toolName, args } = params;
|
||||||
|
// const response = await this.callMCPTool({
|
||||||
|
// serverName,
|
||||||
|
// toolName,
|
||||||
|
// params: args,
|
||||||
|
// });
|
||||||
|
// return { result: response }
|
||||||
|
// };
|
||||||
|
// toolFns[name] = toolFn;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return toolFns
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
|
||||||
236
src/vs/workbench/contrib/void/common/mcpServiceTypes.ts
Normal file
236
src/vs/workbench/contrib/void/common/mcpServiceTypes.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
/**
|
||||||
|
* mcp-response-types.ts
|
||||||
|
* --------------------------------------------------
|
||||||
|
* **Pure** TypeScript interfaces (no external imports)
|
||||||
|
* describing the JSON-RPC response shapes for:
|
||||||
|
*
|
||||||
|
* 1. tools/list -> ToolsListResponse
|
||||||
|
* 2. prompts/list -> PromptsListResponse
|
||||||
|
* 3. tools/call -> ToolCallResponse
|
||||||
|
*
|
||||||
|
* They are distilled directly from the official MCP
|
||||||
|
* 2025‑03‑26 specification:
|
||||||
|
* • Tools list response examples
|
||||||
|
* • Prompts list response examples
|
||||||
|
* • Tool call response examples
|
||||||
|
*
|
||||||
|
* Use them to get full IntelliSense when working with
|
||||||
|
* @modelcontextprotocol/inspector‑cli responses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* Core JSON‑RPC envelope */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
|
// export interface JsonRpcSuccess<T> {
|
||||||
|
// /** JSON‑RPC version – always '2.0' */
|
||||||
|
// jsonrpc: '2.0';
|
||||||
|
// /** Request identifier echoed back by the server */
|
||||||
|
// id: string | number | null;
|
||||||
|
// /** The successful result payload */
|
||||||
|
// result: T;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* Utility: pagination */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
|
// export interface Paginated {
|
||||||
|
// /** Opaque cursor for fetching the next page */
|
||||||
|
// nextCursor?: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* 1. tools/list */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
|
export interface MCPTool {
|
||||||
|
/** Unique tool identifier */
|
||||||
|
name: string;
|
||||||
|
/** Human‑readable description */
|
||||||
|
description?: string;
|
||||||
|
/** JSON schema describing expected arguments */
|
||||||
|
inputSchema?: Record<string, unknown>;
|
||||||
|
/** Free‑form annotations describing behaviour, security, etc. */
|
||||||
|
annotations?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface ToolsListResult extends Paginated {
|
||||||
|
// tools: MCPTool[];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* 2. prompts/list */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
|
// export interface PromptArgument {
|
||||||
|
// name: string;
|
||||||
|
// description?: string;
|
||||||
|
// /** Whether the argument is required */
|
||||||
|
// required?: boolean;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface Prompt {
|
||||||
|
// name: string;
|
||||||
|
// description?: string;
|
||||||
|
// arguments?: PromptArgument[];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PromptsListResult extends Paginated {
|
||||||
|
// prompts: Prompt[];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* 3. tools/call */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
|
/** Additional resource structure that can be embedded in tool results */
|
||||||
|
// export interface Resource {
|
||||||
|
// uri: string;
|
||||||
|
// mimeType: string;
|
||||||
|
// /** Either plain‑text or base64‑encoded binary data */
|
||||||
|
// text?: string;
|
||||||
|
// data?: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Individual content items returned by a tool */
|
||||||
|
// export type ToolContent =
|
||||||
|
// | { type: 'text'; text: string }
|
||||||
|
// | { type: 'image'; data: string; mimeType: string }
|
||||||
|
// | { type: 'audio'; data: string; mimeType: string }
|
||||||
|
// | { type: 'resource'; resource: Resource };
|
||||||
|
|
||||||
|
// export interface ToolCallResult {
|
||||||
|
// /** List of content parts (text, images, resources, etc.) */
|
||||||
|
// content: ToolContent[];
|
||||||
|
// /** True if the tool itself encountered a domain‑level error */
|
||||||
|
// isError?: boolean;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
|
||||||
|
|
||||||
|
// MCP SERVER CONFIG FILE TYPES -----------------------------
|
||||||
|
|
||||||
|
export interface MCPConfigFileEntryJSON {
|
||||||
|
// Command-based server properties
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
|
||||||
|
// URL-based server properties
|
||||||
|
url?: URL;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPConfigFileJSON {
|
||||||
|
mcpServers: Record<string, MCPConfigFileEntryJSON>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SERVER EVENT TYPES ------------------------------------------
|
||||||
|
|
||||||
|
export type MCPServer = {
|
||||||
|
// Command-based server properties
|
||||||
|
tools: MCPTool[],
|
||||||
|
status: 'loading' | 'success' | 'offline',
|
||||||
|
command?: string,
|
||||||
|
error?: string,
|
||||||
|
} | {
|
||||||
|
tools?: undefined,
|
||||||
|
status: 'error',
|
||||||
|
command?: string,
|
||||||
|
error: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPServerOfName {
|
||||||
|
[serverName: string]: MCPServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MCPServerEvent = {
|
||||||
|
name: string;
|
||||||
|
prevServer?: MCPServer;
|
||||||
|
newServer?: MCPServer;
|
||||||
|
}
|
||||||
|
export type MCPServerEventResponse = { response: MCPServerEvent }
|
||||||
|
|
||||||
|
export interface MCPConfigFileParseErrorResponse {
|
||||||
|
response: {
|
||||||
|
type: 'config-file-error';
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
|
||||||
|
|
||||||
|
// Event parameter types
|
||||||
|
// export type MCPServerEventAddParam = { response: MCPAddResponse };
|
||||||
|
// export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
|
||||||
|
// export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
|
||||||
|
// export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
|
||||||
|
|
||||||
|
// Event Param union type
|
||||||
|
// export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
|
||||||
|
|
||||||
|
// TOOL CALL EVENT TYPES ------------------------------------------
|
||||||
|
|
||||||
|
type MCPToolResponseType = 'text' | 'image' | 'audio' | 'resource' | 'error';
|
||||||
|
|
||||||
|
type ResponseImageTypes = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' | 'image/svg+xml' | 'image/bmp' | 'image/tiff' | 'image/vnd.microsoft.icon';
|
||||||
|
|
||||||
|
interface ImageData {
|
||||||
|
data: string;
|
||||||
|
mimeType: ResponseImageTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MCPToolResponseBase {
|
||||||
|
toolName: string;
|
||||||
|
serverName?: string;
|
||||||
|
event: MCPToolResponseType;
|
||||||
|
text?: string;
|
||||||
|
image?: ImageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MCPToolResponseConstraints = {
|
||||||
|
'text': {
|
||||||
|
image?: never;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
'error': {
|
||||||
|
image?: never;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
'image': {
|
||||||
|
text?: never;
|
||||||
|
image: ImageData;
|
||||||
|
};
|
||||||
|
'audio': {
|
||||||
|
text?: never;
|
||||||
|
image?: never;
|
||||||
|
};
|
||||||
|
'resource': {
|
||||||
|
text?: never;
|
||||||
|
image?: never;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MCPToolEventResponse<T extends MCPToolResponseType> = Omit<MCPToolResponseBase, 'event' | keyof MCPToolResponseConstraints> & MCPToolResponseConstraints[T] & { event: T };
|
||||||
|
|
||||||
|
// Response types
|
||||||
|
export type MCPToolTextResponse = MCPToolEventResponse<'text'>;
|
||||||
|
export type MCPToolErrorResponse = MCPToolEventResponse<'error'>;
|
||||||
|
export type MCPToolImageResponse = MCPToolEventResponse<'image'>;
|
||||||
|
export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>;
|
||||||
|
export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>;
|
||||||
|
export type RawMCPToolCall = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse;
|
||||||
|
|
||||||
|
export interface MCPToolCallParams {
|
||||||
|
serverName: string;
|
||||||
|
toolName: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
@ -1375,7 +1375,6 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
||||||
|
|
||||||
const openRouterSettings: VoidStaticProviderInfo = {
|
const openRouterSettings: VoidStaticProviderInfo = {
|
||||||
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
||||||
// TODO!!! send a query to openrouter to get the price, etc.
|
|
||||||
modelOptionsFallback: (modelName) => {
|
modelOptionsFallback: (modelName) => {
|
||||||
const res = extensiveModelOptionsFallback(modelName)
|
const res = extensiveModelOptionsFallback(modelName)
|
||||||
// openRouter does not support gemini-style, use openai-style instead
|
// openRouter does not support gemini-style, use openai-style instead
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { IDirectoryStrService } from '../directoryStrService.js';
|
||||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||||
import { os } from '../helpers/systemInfo.js';
|
import { os } from '../helpers/systemInfo.js';
|
||||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
|
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js';
|
||||||
import { ChatMode } from '../voidSettingsTypes.js';
|
import { ChatMode } from '../voidSettingsTypes.js';
|
||||||
|
|
||||||
// Triple backtick wrapper used throughout the prompts for code blocks
|
// Triple backtick wrapper used throughout the prompts for code blocks
|
||||||
|
|
@ -147,6 +147,8 @@ export type InternalToolInfo = {
|
||||||
params: {
|
params: {
|
||||||
[paramName: string]: { description: string }
|
[paramName: string]: { description: string }
|
||||||
},
|
},
|
||||||
|
// Only if the tool is from an MCP server
|
||||||
|
mcpServerName?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -181,191 +183,197 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// export const voidTools = {
|
export const builtinTools: {
|
||||||
export const voidTools
|
[T in keyof BuiltinToolCallParams]: {
|
||||||
: {
|
name: string;
|
||||||
[T in keyof ToolCallParams]: {
|
description: string;
|
||||||
name: string;
|
// more params can be generated than exist here, but these params must be a subset of them
|
||||||
description: string;
|
params: Partial<{ [paramName in keyof SnakeCaseKeys<BuiltinToolCallParams[T]>]: { description: string } }>
|
||||||
// more params can be generated than exist here, but these params must be a subset of them
|
|
||||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string } }>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
= {
|
} = {
|
||||||
// --- context-gathering (read/search/list) ---
|
// --- context-gathering (read/search/list) ---
|
||||||
|
|
||||||
read_file: {
|
read_file: {
|
||||||
name: 'read_file',
|
name: 'read_file',
|
||||||
description: `Returns full contents of a given file.`,
|
description: `Returns full contents of a given file.`,
|
||||||
params: {
|
params: {
|
||||||
...uriParam('file'),
|
...uriParam('file'),
|
||||||
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
||||||
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
||||||
...paginationParam,
|
...paginationParam,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
ls_dir: {
|
ls_dir: {
|
||||||
name: 'ls_dir',
|
name: 'ls_dir',
|
||||||
description: `Lists all files and folders in the given URI.`,
|
description: `Lists all files and folders in the given URI.`,
|
||||||
params: {
|
params: {
|
||||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||||
...paginationParam,
|
...paginationParam,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
get_dir_tree: {
|
get_dir_tree: {
|
||||||
name: 'get_dir_tree',
|
name: 'get_dir_tree',
|
||||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||||
params: {
|
params: {
|
||||||
...uriParam('folder')
|
...uriParam('folder')
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// pathname_search: {
|
|
||||||
// name: 'pathname_search',
|
|
||||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
|
||||||
|
|
||||||
search_pathnames_only: {
|
|
||||||
name: 'search_pathnames_only',
|
|
||||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
|
||||||
params: {
|
|
||||||
query: { description: `Your query for the search.` },
|
|
||||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
|
||||||
...paginationParam,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
search_for_files: {
|
|
||||||
name: 'search_for_files',
|
|
||||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
|
||||||
params: {
|
|
||||||
query: { description: `Your query for the search.` },
|
|
||||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
|
||||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
|
||||||
...paginationParam,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// add new search_in_file tool
|
|
||||||
search_in_file: {
|
|
||||||
name: 'search_in_file',
|
|
||||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
|
||||||
params: {
|
|
||||||
...uriParam('file'),
|
|
||||||
query: { description: 'The string or regex to search for in the file.' },
|
|
||||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read_lint_errors: {
|
|
||||||
name: 'read_lint_errors',
|
|
||||||
description: `Use this tool to view all the lint errors on a file.`,
|
|
||||||
params: {
|
|
||||||
...uriParam('file'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- editing (create/delete) ---
|
|
||||||
|
|
||||||
create_file_or_folder: {
|
|
||||||
name: 'create_file_or_folder',
|
|
||||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
|
||||||
params: {
|
|
||||||
...uriParam('file or folder'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_file_or_folder: {
|
|
||||||
name: 'delete_file_or_folder',
|
|
||||||
description: `Delete a file or folder at the given path.`,
|
|
||||||
params: {
|
|
||||||
...uriParam('file or folder'),
|
|
||||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
edit_file: {
|
|
||||||
name: 'edit_file',
|
|
||||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
|
||||||
params: {
|
|
||||||
...uriParam('file'),
|
|
||||||
search_replace_blocks: { description: replaceTool_description }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
rewrite_file: {
|
|
||||||
name: 'rewrite_file',
|
|
||||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
|
||||||
params: {
|
|
||||||
...uriParam('file'),
|
|
||||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
run_command: {
|
|
||||||
name: 'run_command',
|
|
||||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
|
||||||
params: {
|
|
||||||
command: { description: 'The terminal command to run.' },
|
|
||||||
cwd: { description: cwdHelper },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
run_persistent_command: {
|
|
||||||
name: 'run_persistent_command',
|
|
||||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
|
||||||
params: {
|
|
||||||
command: { description: 'The terminal command to run.' },
|
|
||||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
open_persistent_terminal: {
|
|
||||||
name: 'open_persistent_terminal',
|
|
||||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
|
||||||
params: {
|
|
||||||
cwd: { description: cwdHelper },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
kill_persistent_terminal: {
|
|
||||||
name: 'kill_persistent_terminal',
|
|
||||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
|
||||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// pathname_search: {
|
||||||
|
// name: 'pathname_search',
|
||||||
|
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||||
|
|
||||||
|
search_pathnames_only: {
|
||||||
|
name: 'search_pathnames_only',
|
||||||
|
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||||
|
params: {
|
||||||
|
query: { description: `Your query for the search.` },
|
||||||
|
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||||
|
...paginationParam,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// go_to_definition
|
|
||||||
// go_to_usages
|
|
||||||
|
|
||||||
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
|
search_for_files: {
|
||||||
|
name: 'search_for_files',
|
||||||
|
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||||
|
params: {
|
||||||
|
query: { description: `Your query for the search.` },
|
||||||
|
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||||
|
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||||
|
...paginationParam,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// add new search_in_file tool
|
||||||
|
search_in_file: {
|
||||||
|
name: 'search_in_file',
|
||||||
|
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||||
|
params: {
|
||||||
|
...uriParam('file'),
|
||||||
|
query: { description: 'The string or regex to search for in the file.' },
|
||||||
|
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
read_lint_errors: {
|
||||||
|
name: 'read_lint_errors',
|
||||||
|
description: `Use this tool to view all the lint errors on a file.`,
|
||||||
|
params: {
|
||||||
|
...uriParam('file'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- editing (create/delete) ---
|
||||||
|
|
||||||
|
create_file_or_folder: {
|
||||||
|
name: 'create_file_or_folder',
|
||||||
|
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||||
|
params: {
|
||||||
|
...uriParam('file or folder'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_file_or_folder: {
|
||||||
|
name: 'delete_file_or_folder',
|
||||||
|
description: `Delete a file or folder at the given path.`,
|
||||||
|
params: {
|
||||||
|
...uriParam('file or folder'),
|
||||||
|
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
edit_file: {
|
||||||
|
name: 'edit_file',
|
||||||
|
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||||
|
params: {
|
||||||
|
...uriParam('file'),
|
||||||
|
search_replace_blocks: { description: replaceTool_description }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rewrite_file: {
|
||||||
|
name: 'rewrite_file',
|
||||||
|
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||||
|
params: {
|
||||||
|
...uriParam('file'),
|
||||||
|
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run_command: {
|
||||||
|
name: 'run_command',
|
||||||
|
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||||
|
params: {
|
||||||
|
command: { description: 'The terminal command to run.' },
|
||||||
|
cwd: { description: cwdHelper },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
run_persistent_command: {
|
||||||
|
name: 'run_persistent_command',
|
||||||
|
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||||
|
params: {
|
||||||
|
command: { description: 'The terminal command to run.' },
|
||||||
|
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
export type ToolName = keyof ToolResultType
|
|
||||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
|
||||||
|
|
||||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
open_persistent_terminal: {
|
||||||
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
|
name: 'open_persistent_terminal',
|
||||||
|
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||||
|
params: {
|
||||||
|
cwd: { description: cwdHelper },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
const toolNamesSet = new Set<string>(toolNames)
|
|
||||||
|
|
||||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
kill_persistent_terminal: {
|
||||||
|
name: 'kill_persistent_terminal',
|
||||||
|
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||||
|
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// go_to_definition
|
||||||
|
// go_to_usages
|
||||||
|
|
||||||
|
} satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[]
|
||||||
|
const toolNamesSet = new Set<string>(builtinToolNames)
|
||||||
|
export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => {
|
||||||
const isAToolName = toolNamesSet.has(toolName)
|
const isAToolName = toolNamesSet.has(toolName)
|
||||||
return isAToolName
|
return isAToolName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const availableTools = (chatMode: ChatMode) => {
|
|
||||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
|
||||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
|
|
||||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
|
||||||
|
export const availableTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||||
|
|
||||||
|
const builtinToolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||||
|
: chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName))
|
||||||
|
: chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[]
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
|
const effectiveBuiltinTools = builtinToolNames?.map(toolName => builtinTools[toolName]) ?? undefined
|
||||||
|
const effectiveMCPTools = chatMode === 'agent' ? mcpTools : undefined
|
||||||
|
|
||||||
|
const tools: InternalToolInfo[] | undefined = !(builtinToolNames || mcpTools) ? undefined
|
||||||
|
: [
|
||||||
|
...effectiveBuiltinTools ?? [],
|
||||||
|
...effectiveMCPTools ?? [],
|
||||||
|
]
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -382,7 +390,7 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
||||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName]}</${paramName}>`).join('\n')
|
||||||
return `\
|
return `\
|
||||||
<${toolName}>${!params ? '' : `\n${params}`}
|
<${toolName}>${!params ? '' : `\n${params}`}
|
||||||
</${toolName}>`
|
</${toolName}>`
|
||||||
|
|
@ -391,8 +399,8 @@ export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolPar
|
||||||
|
|
||||||
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
|
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
|
||||||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||||
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined) => {
|
||||||
const tools = availableTools(chatMode)
|
const tools = availableTools(chatMode, mcpTools)
|
||||||
if (!tools || tools.length === 0) return null
|
if (!tools || tools.length === 0) return null
|
||||||
|
|
||||||
const toolXMLDefinitions = (`\
|
const toolXMLDefinitions = (`\
|
||||||
|
|
@ -417,7 +425,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||||
// ======================================================== chat (normal, gather, agent) ========================================================
|
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||||
|
|
||||||
|
|
||||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, mcpTools, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined, includeXMLToolDefinitions: boolean }) => {
|
||||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||||
|
|
@ -451,7 +459,7 @@ ${directoryStr}
|
||||||
</files_overview>`)
|
</files_overview>`)
|
||||||
|
|
||||||
|
|
||||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode, mcpTools) : null
|
||||||
|
|
||||||
const details: string[] = []
|
const details: string[] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { generateUuid } from '../../../../base/common/uuid.js';
|
||||||
import { Event } from '../../../../base/common/event.js';
|
import { Event } from '../../../../base/common/event.js';
|
||||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||||
|
import { IMCPService } from './mcpService.js';
|
||||||
|
|
||||||
// calls channel to implement features
|
// calls channel to implement features
|
||||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||||
|
|
@ -61,6 +62,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||||
// @INotificationService private readonly notificationService: INotificationService,
|
// @INotificationService private readonly notificationService: INotificationService,
|
||||||
|
@IMCPService private readonly mcpService: IMCPService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
|
@ -116,6 +118,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
|
|
||||||
const { settingsOfProvider, } = this.voidSettingsService.state
|
const { settingsOfProvider, } = this.voidSettingsService.state
|
||||||
|
|
||||||
|
const mcpTools = this.mcpService.getMCPTools()
|
||||||
|
|
||||||
// add state for request id
|
// add state for request id
|
||||||
const requestId = generateUuid();
|
const requestId = generateUuid();
|
||||||
this.llmMessageHooks.onText[requestId] = onText
|
this.llmMessageHooks.onText[requestId] = onText
|
||||||
|
|
@ -129,6 +133,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
requestId,
|
requestId,
|
||||||
settingsOfProvider,
|
settingsOfProvider,
|
||||||
modelSelection,
|
modelSelection,
|
||||||
|
mcpTools,
|
||||||
} satisfies MainSendLLMMessageParams);
|
} satisfies MainSendLLMMessageParams);
|
||||||
|
|
||||||
return requestId
|
return requestId
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||||
*--------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
import { InternalToolInfo } from './prompt/prompts.js'
|
||||||
|
import { ToolName, ToolParamName } from './toolsServiceTypes.js'
|
||||||
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,12 +79,12 @@ export type LLMFIMMessage = {
|
||||||
|
|
||||||
|
|
||||||
export type RawToolParamsObj = {
|
export type RawToolParamsObj = {
|
||||||
[paramName in ToolParamName]?: string;
|
[paramName in ToolParamName<ToolName>]?: string;
|
||||||
}
|
}
|
||||||
export type RawToolCallObj = {
|
export type RawToolCallObj = {
|
||||||
name: ToolName;
|
name: ToolName;
|
||||||
rawParams: RawToolParamsObj;
|
rawParams: RawToolParamsObj;
|
||||||
doneParams: ToolParamName[];
|
doneParams: ToolParamName<ToolName>[];
|
||||||
id: string;
|
id: string;
|
||||||
isDone: boolean;
|
isDone: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -133,6 +134,7 @@ export type SendLLMMessageParams = {
|
||||||
overridesOfModel: OverridesOfModel | undefined;
|
overridesOfModel: OverridesOfModel | undefined;
|
||||||
|
|
||||||
settingsOfProvider: SettingsOfProvider;
|
settingsOfProvider: SettingsOfProvider;
|
||||||
|
mcpTools: InternalToolInfo[] | undefined;
|
||||||
} & SendLLMType
|
} & SendLLMType
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { URI } from '../../../../base/common/uri.js'
|
import { URI } from '../../../../base/common/uri.js'
|
||||||
import { ToolName } from './prompt/prompts.js';
|
import { RawMCPToolCall } from './mcpServiceTypes.js';
|
||||||
|
import { builtinTools } from './prompt/prompts.js';
|
||||||
|
import { RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,7 +18,7 @@ export type ShallowDirectoryItem = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'mcp-tools' }> = {
|
||||||
'create_file_or_folder': 'edits',
|
'create_file_or_folder': 'edits',
|
||||||
'delete_file_or_folder': 'edits',
|
'delete_file_or_folder': 'edits',
|
||||||
'rewrite_file': 'edits',
|
'rewrite_file': 'edits',
|
||||||
|
|
@ -28,16 +30,19 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName)[keyof typeof approvalTypeOfBuiltinToolName]>;
|
||||||
|
|
||||||
|
|
||||||
|
export const toolApprovalTypes = new Set<ToolApprovalType>([
|
||||||
|
...Object.values(approvalTypeOfBuiltinToolName),
|
||||||
|
'mcp-tools',
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
// {{add: define new type for approval types}}
|
|
||||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
|
|
||||||
|
|
||||||
export const toolApprovalTypes = new Set<ToolApprovalType>(
|
|
||||||
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
|
|
||||||
)
|
|
||||||
|
|
||||||
// PARAMS OF TOOL CALL
|
// PARAMS OF TOOL CALL
|
||||||
export type ToolCallParams = {
|
export type BuiltinToolCallParams = {
|
||||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||||
'ls_dir': { uri: URI, pageNumber: number },
|
'ls_dir': { uri: URI, pageNumber: number },
|
||||||
'get_dir_tree': { uri: URI },
|
'get_dir_tree': { uri: URI },
|
||||||
|
|
@ -58,7 +63,7 @@ export type ToolCallParams = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESULT OF TOOL CALL
|
// RESULT OF TOOL CALL
|
||||||
export type ToolResultType = {
|
export type BuiltinToolResultType = {
|
||||||
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
|
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
|
||||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||||
'get_dir_tree': { str: string, },
|
'get_dir_tree': { str: string, },
|
||||||
|
|
@ -78,3 +83,15 @@ export type ToolResultType = {
|
||||||
'kill_persistent_terminal': {},
|
'kill_persistent_terminal': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type ToolCallParams<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj
|
||||||
|
export type ToolResult<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall
|
||||||
|
|
||||||
|
export type BuiltinToolName = keyof BuiltinToolResultType
|
||||||
|
|
||||||
|
type BuiltinToolParamNameOfTool<T extends BuiltinToolName> = keyof (typeof builtinTools)[T]['params']
|
||||||
|
export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool<T> }[BuiltinToolName]
|
||||||
|
|
||||||
|
|
||||||
|
export type ToolName = BuiltinToolName | (string & {})
|
||||||
|
export type ToolParamName<T extends ToolName> = T extends BuiltinToolName ? BuiltinToolParamNameOfTool<T> : string
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||||
import { IMetricsService } from './metricsService.js';
|
import { IMetricsService } from './metricsService.js';
|
||||||
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
|
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
|
||||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel } from './voidSettingsTypes.js';
|
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
|
||||||
|
|
||||||
|
|
||||||
// name is the name in the dropdown
|
// name is the name in the dropdown
|
||||||
|
|
@ -43,6 +43,7 @@ export type VoidSettingsState = {
|
||||||
readonly optionsOfModelSelection: OptionsOfModelSelection;
|
readonly optionsOfModelSelection: OptionsOfModelSelection;
|
||||||
readonly overridesOfModel: OverridesOfModel;
|
readonly overridesOfModel: OverridesOfModel;
|
||||||
readonly globalSettings: GlobalSettings;
|
readonly globalSettings: GlobalSettings;
|
||||||
|
readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers
|
||||||
|
|
||||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +63,7 @@ export interface IVoidSettingsService {
|
||||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||||
setGlobalSetting: SetGlobalSettingFn;
|
setGlobalSetting: SetGlobalSettingFn;
|
||||||
|
// setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
|
||||||
|
|
||||||
// setting to undefined CLEARS it, unlike others:
|
// setting to undefined CLEARS it, unlike others:
|
||||||
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
|
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
|
||||||
|
|
@ -73,6 +75,10 @@ export interface IVoidSettingsService {
|
||||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||||
addModel(providerName: ProviderName, modelName: string): void;
|
addModel(providerName: ProviderName, modelName: string): void;
|
||||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||||
|
|
||||||
|
addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise<void>;
|
||||||
|
removeMCPUserStateOfNames(serverNames: string[]): Promise<void>;
|
||||||
|
setMCPServerState(serverName: string, state: MCPUserState): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -212,6 +218,7 @@ const defaultState = () => {
|
||||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||||
_modelOptions: [], // computed later
|
_modelOptions: [], // computed later
|
||||||
|
mcpUserStateOfName: {},
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
@ -361,6 +368,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||||
|
|
||||||
const newGlobalSettings = this.state.globalSettings
|
const newGlobalSettings = this.state.globalSettings
|
||||||
const newOverridesOfModel = this.state.overridesOfModel
|
const newOverridesOfModel = this.state.overridesOfModel
|
||||||
|
const newMCPUserStateOfName = this.state.mcpUserStateOfName
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||||
|
|
@ -368,6 +376,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||||
settingsOfProvider: newSettingsOfProvider,
|
settingsOfProvider: newSettingsOfProvider,
|
||||||
globalSettings: newGlobalSettings,
|
globalSettings: newGlobalSettings,
|
||||||
overridesOfModel: newOverridesOfModel,
|
overridesOfModel: newOverridesOfModel,
|
||||||
|
mcpUserStateOfName: newMCPUserStateOfName,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = _validatedModelState(newState)
|
this.state = _validatedModelState(newState)
|
||||||
|
|
@ -531,6 +540,55 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MCP Server State
|
||||||
|
private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => {
|
||||||
|
const newState: VoidSettingsState = {
|
||||||
|
...this.state,
|
||||||
|
mcpUserStateOfName: {
|
||||||
|
...this.state.mcpUserStateOfName,
|
||||||
|
...newStates
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.state = _validatedModelState(newState);
|
||||||
|
await this._storeState();
|
||||||
|
this._onDidChangeState.fire();
|
||||||
|
this._metricsService.capture('Set MCP Server States', { newStates });
|
||||||
|
}
|
||||||
|
|
||||||
|
addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => {
|
||||||
|
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||||
|
const newMCPServerStates = {
|
||||||
|
...mcpServerStates,
|
||||||
|
...newMCPStates,
|
||||||
|
}
|
||||||
|
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||||
|
this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMCPUserStateOfNames = async (serverNames: string[]) => {
|
||||||
|
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||||
|
const newMCPServerStates = {
|
||||||
|
...mcpServerStates,
|
||||||
|
}
|
||||||
|
serverNames.forEach(serverName => {
|
||||||
|
if (serverName in newMCPServerStates) {
|
||||||
|
delete newMCPServerStates[serverName]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||||
|
this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
setMCPServerState = async (serverName: string, state: MCPUserState) => {
|
||||||
|
const { mcpUserStateOfName } = this.state
|
||||||
|
const newMCPServerStates = {
|
||||||
|
...mcpUserStateOfName,
|
||||||
|
[serverName]: state,
|
||||||
|
}
|
||||||
|
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||||
|
this._metricsService.capture('Update MCP Server State', { serverName, state });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||||
modelName: string,
|
modelName: string,
|
||||||
type: 'default' | 'autodetected' | 'custom';
|
type: 'default' | 'autodetected' | 'custom';
|
||||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -491,3 +491,13 @@ export type OverridesOfModel = {
|
||||||
const overridesOfModel = {} as OverridesOfModel
|
const overridesOfModel = {} as OverridesOfModel
|
||||||
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
|
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
|
||||||
export const defaultOverridesOfModel = overridesOfModel
|
export const defaultOverridesOfModel = overridesOfModel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface MCPUserStateOfName {
|
||||||
|
[serverName: string]: MCPUserState | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPUserState {
|
||||||
|
isOn: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@
|
||||||
|
|
||||||
import { generateUuid } from '../../../../../base/common/uuid.js'
|
import { generateUuid } from '../../../../../base/common/uuid.js'
|
||||||
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
||||||
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
|
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'
|
||||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||||
|
import { ToolName, ToolParamName } from '../../common/toolsServiceTypes.js'
|
||||||
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -164,15 +165,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
|
||||||
|
|
||||||
|
|
||||||
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
||||||
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||||
const paramsObj: RawToolParamsObj = {}
|
const paramsObj: RawToolParamsObj = {}
|
||||||
const doneParams: ToolParamName[] = []
|
const doneParams: ToolParamName<T>[] = []
|
||||||
let isDone = false
|
let isDone = false
|
||||||
|
|
||||||
const getAnswer = (): RawToolCallObj => {
|
const getAnswer = (): RawToolCallObj => {
|
||||||
// trim off all whitespace at and before first \n and after last \n for each param
|
// trim off all whitespace at and before first \n and after last \n for each param
|
||||||
for (const p in paramsObj) {
|
for (const p in paramsObj) {
|
||||||
const paramName = p as ToolParamName
|
const paramName = p as ToolParamName<T>
|
||||||
const orig = paramsObj[paramName]
|
const orig = paramsObj[paramName]
|
||||||
if (orig === undefined) continue
|
if (orig === undefined) continue
|
||||||
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
|
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
|
||||||
|
|
@ -202,16 +203,16 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
|
||||||
|
|
||||||
const pm = new SurroundingsRemover(str)
|
const pm = new SurroundingsRemover(str)
|
||||||
|
|
||||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
|
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName<T>[]
|
||||||
if (allowedParams.length === 0) return getAnswer()
|
if (allowedParams.length === 0) return getAnswer()
|
||||||
let latestMatchedOpenParam: null | ToolParamName = null
|
let latestMatchedOpenParam: null | ToolParamName<T> = null
|
||||||
let n = 0
|
let n = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
n += 1
|
n += 1
|
||||||
if (n > 10) return getAnswer() // just for good measure as this code is early
|
if (n > 10) return getAnswer() // just for good measure as this code is early
|
||||||
|
|
||||||
// find the param name opening tag
|
// find the param name opening tag
|
||||||
let matchedOpenParam: null | ToolParamName = null
|
let matchedOpenParam: null | ToolParamName<T> = null
|
||||||
for (const paramName of allowedParams) {
|
for (const paramName of allowedParams) {
|
||||||
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
|
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
|
||||||
if (removed) {
|
if (removed) {
|
||||||
|
|
@ -260,11 +261,14 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractXMLToolsWrapper = (
|
export const extractXMLToolsWrapper = (
|
||||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
|
onText: OnText,
|
||||||
|
onFinalMessage: OnFinalMessage,
|
||||||
|
chatMode: ChatMode | null,
|
||||||
|
mcpTools: InternalToolInfo[] | undefined,
|
||||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||||
|
|
||||||
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||||
const tools = availableTools(chatMode)
|
const tools = availableTools(chatMode, mcpTools)
|
||||||
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||||
|
|
||||||
const toolOfToolName: ToolOfToolName = {}
|
const toolOfToolName: ToolOfToolName = {}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMe
|
||||||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
|
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
|
||||||
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
|
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
|
||||||
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js';
|
||||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||||
|
|
||||||
const getGoogleApiKey = async () => {
|
const getGoogleApiKey = async () => {
|
||||||
|
|
@ -48,6 +48,7 @@ type SendChatParams_Internal = InternalCommonMessageParams & {
|
||||||
messages: LLMChatMessage[];
|
messages: LLMChatMessage[];
|
||||||
separateSystemMessage: string | undefined;
|
separateSystemMessage: string | undefined;
|
||||||
chatMode: ChatMode | null;
|
chatMode: ChatMode | null;
|
||||||
|
mcpTools: InternalToolInfo[] | undefined;
|
||||||
}
|
}
|
||||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
|
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
|
||||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||||
|
|
@ -206,8 +207,8 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAITools = (chatMode: ChatMode) => {
|
const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||||
const allowedTools = availableTools(chatMode)
|
const allowedTools = availableTools(chatMode, mcpTools)
|
||||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||||
|
|
||||||
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
|
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
|
||||||
|
|
@ -217,29 +218,36 @@ const openAITools = (chatMode: ChatMode) => {
|
||||||
return openAITools
|
return openAITools
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
|
||||||
if (!isAToolName(name)) return null
|
// convert LLM tool call to our tool format
|
||||||
const rawParams: RawToolParamsObj = {}
|
const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||||
let input: unknown
|
let input: unknown
|
||||||
try {
|
try { input = JSON.parse(toolParamsStr) }
|
||||||
input = JSON.parse(toolParamsStr)
|
catch (e) { return null }
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (input === null) return null
|
if (input === null) return null
|
||||||
if (typeof input !== 'object') return null
|
if (typeof input !== 'object') return null
|
||||||
for (const paramName in voidTools[name].params) {
|
|
||||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
const rawParams: RawToolParamsObj = input
|
||||||
}
|
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], 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), isDone: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ------------ OPENAI-COMPATIBLE ------------
|
// ------------ OPENAI-COMPATIBLE ------------
|
||||||
|
|
||||||
|
|
||||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel }: SendChatParams_Internal) => {
|
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel, mcpTools }: SendChatParams_Internal) => {
|
||||||
const {
|
const {
|
||||||
modelName,
|
modelName,
|
||||||
specialToolFormat,
|
specialToolFormat,
|
||||||
|
|
@ -259,7 +267,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||||
}
|
}
|
||||||
|
|
||||||
// tools
|
// tools
|
||||||
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
|
const potentialTools = openAITools(chatMode, mcpTools)
|
||||||
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
||||||
{ tools: potentialTools } as const
|
{ tools: potentialTools } as const
|
||||||
: {}
|
: {}
|
||||||
|
|
@ -290,7 +298,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||||
|
|
||||||
// manually parse out tool results if XML
|
// manually parse out tool results if XML
|
||||||
if (!specialToolFormat) {
|
if (!specialToolFormat) {
|
||||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||||
onText = newOnText
|
onText = newOnText
|
||||||
onFinalMessage = newOnFinalMessage
|
onFinalMessage = newOnFinalMessage
|
||||||
}
|
}
|
||||||
|
|
@ -335,7 +343,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||||
onText({
|
onText({
|
||||||
fullText: fullTextSoFar,
|
fullText: fullTextSoFar,
|
||||||
fullReasoning: fullReasoningSoFar,
|
fullReasoning: fullReasoningSoFar,
|
||||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +352,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
|
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||||
const toolCallObj = toolCall ? { toolCall } : {}
|
const toolCallObj = toolCall ? { toolCall } : {}
|
||||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||||
}
|
}
|
||||||
|
|
@ -410,8 +418,8 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||||
} satisfies Anthropic.Messages.Tool
|
} satisfies Anthropic.Messages.Tool
|
||||||
}
|
}
|
||||||
|
|
||||||
const anthropicTools = (chatMode: ChatMode) => {
|
const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||||
const allowedTools = availableTools(chatMode)
|
const allowedTools = availableTools(chatMode, mcpTools)
|
||||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||||
|
|
||||||
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
||||||
|
|
@ -421,20 +429,10 @@ const anthropicTools = (chatMode: ChatMode) => {
|
||||||
return anthropicTools
|
return anthropicTools
|
||||||
}
|
}
|
||||||
|
|
||||||
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
|
||||||
const { id, name, input } = toolBlock
|
|
||||||
if (!isAToolName(name)) return null
|
|
||||||
const rawParams: RawToolParamsObj = {}
|
|
||||||
if (input === null) return null
|
|
||||||
if (typeof input !== 'object') return null
|
|
||||||
for (const paramName in voidTools[name].params) {
|
|
||||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
|
||||||
}
|
|
||||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------ ANTHROPIC ------------
|
// ------------ ANTHROPIC ------------
|
||||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => {
|
||||||
const {
|
const {
|
||||||
modelName,
|
modelName,
|
||||||
specialToolFormat,
|
specialToolFormat,
|
||||||
|
|
@ -451,7 +449,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||||
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
|
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
|
||||||
|
|
||||||
// tools
|
// tools
|
||||||
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
|
const potentialTools = anthropicTools(chatMode, mcpTools)
|
||||||
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
|
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
|
||||||
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
|
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
|
||||||
: {}
|
: {}
|
||||||
|
|
@ -475,7 +473,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||||
|
|
||||||
// manually parse out tool results if XML
|
// manually parse out tool results if XML
|
||||||
if (!specialToolFormat) {
|
if (!specialToolFormat) {
|
||||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||||
onText = newOnText
|
onText = newOnText
|
||||||
onFinalMessage = newOnFinalMessage
|
onFinalMessage = newOnFinalMessage
|
||||||
}
|
}
|
||||||
|
|
@ -492,7 +490,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||||
onText({
|
onText({
|
||||||
fullText,
|
fullText,
|
||||||
fullReasoning,
|
fullReasoning,
|
||||||
toolCall: isAToolName(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
|
// there are no events for tool_use, it comes in at the end
|
||||||
|
|
@ -542,7 +540,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
||||||
stream.on('finalMessage', (response) => {
|
stream.on('finalMessage', (response) => {
|
||||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||||
const tools = response.content.filter(c => c.type === 'tool_use')
|
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 } : {}
|
const toolCallObj = toolCall ? { toolCall } : {}
|
||||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||||
})
|
})
|
||||||
|
|
@ -676,8 +674,8 @@ const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
|
||||||
} satisfies FunctionDeclaration
|
} satisfies FunctionDeclaration
|
||||||
}
|
}
|
||||||
|
|
||||||
const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => {
|
const geminiTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined): GeminiTool[] | null => {
|
||||||
const allowedTools = availableTools(chatMode)
|
const allowedTools = availableTools(chatMode, mcpTools)
|
||||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||||
const functionDecls: FunctionDeclaration[] = []
|
const functionDecls: FunctionDeclaration[] = []
|
||||||
for (const t in allowedTools ?? {}) {
|
for (const t in allowedTools ?? {}) {
|
||||||
|
|
@ -703,6 +701,7 @@ const sendGeminiChat = async ({
|
||||||
providerName,
|
providerName,
|
||||||
modelSelectionOptions,
|
modelSelectionOptions,
|
||||||
chatMode,
|
chatMode,
|
||||||
|
mcpTools,
|
||||||
}: SendChatParams_Internal) => {
|
}: SendChatParams_Internal) => {
|
||||||
|
|
||||||
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
|
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
|
||||||
|
|
@ -728,7 +727,7 @@ const sendGeminiChat = async ({
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// tools
|
// tools
|
||||||
const potentialTools = chatMode !== null ? geminiTools(chatMode) : undefined
|
const potentialTools = geminiTools(chatMode, mcpTools)
|
||||||
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
|
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
|
||||||
potentialTools
|
potentialTools
|
||||||
: undefined
|
: undefined
|
||||||
|
|
@ -739,7 +738,7 @@ const sendGeminiChat = async ({
|
||||||
|
|
||||||
// manually parse out tool results if XML
|
// manually parse out tool results if XML
|
||||||
if (!specialToolFormat) {
|
if (!specialToolFormat) {
|
||||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||||
onText = newOnText
|
onText = newOnText
|
||||||
onFinalMessage = newOnFinalMessage
|
onFinalMessage = newOnFinalMessage
|
||||||
}
|
}
|
||||||
|
|
@ -786,7 +785,7 @@ const sendGeminiChat = async ({
|
||||||
onText({
|
onText({
|
||||||
fullText: fullTextSoFar,
|
fullText: fullTextSoFar,
|
||||||
fullReasoning: fullReasoningSoFar,
|
fullReasoning: fullReasoningSoFar,
|
||||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -795,7 +794,7 @@ const sendGeminiChat = async ({
|
||||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||||
} else {
|
} else {
|
||||||
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
|
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 } : {}
|
const toolCallObj = toolCall ? { toolCall } : {}
|
||||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export const sendLLMMessage = async ({
|
||||||
overridesOfModel,
|
overridesOfModel,
|
||||||
chatMode,
|
chatMode,
|
||||||
separateSystemMessage,
|
separateSystemMessage,
|
||||||
|
mcpTools,
|
||||||
}: SendLLMMessageParams,
|
}: SendLLMMessageParams,
|
||||||
|
|
||||||
metricsService: IMetricsService
|
metricsService: IMetricsService
|
||||||
|
|
@ -107,7 +108,7 @@ export const sendLLMMessage = async ({
|
||||||
}
|
}
|
||||||
const { sendFIM, sendChat } = implementation
|
const { sendFIM, sendChat } = implementation
|
||||||
if (messagesType === 'chatMessages') {
|
if (messagesType === 'chatMessages') {
|
||||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode, mcpTools })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (messagesType === 'FIMMessage') {
|
if (messagesType === 'FIMMessage') {
|
||||||
|
|
|
||||||
411
src/vs/workbench/contrib/void/electron-main/mcpChannel.ts
Normal file
411
src/vs/workbench/contrib/void/electron-main/mcpChannel.ts
Normal file
|
|
@ -0,0 +1,411 @@
|
||||||
|
/*--------------------------------------------------------------------------------------
|
||||||
|
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||||
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
// registered in app.ts
|
||||||
|
// can't make a service responsible for this, because it needs
|
||||||
|
// to be connected to the main process and node dependencies
|
||||||
|
|
||||||
|
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||||
|
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||||
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||||
|
import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, RawMCPToolCall, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams } from '../common/mcpServiceTypes.js';
|
||||||
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { MCPUserStateOfName } from '../common/voidSettingsTypes.js';
|
||||||
|
|
||||||
|
const getClientConfig = (serverName: string) => {
|
||||||
|
return {
|
||||||
|
name: `${serverName}-client`,
|
||||||
|
version: '0.1.0',
|
||||||
|
// debug: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MCPServerNonError = MCPServer & { status: Omit<MCPServer['status'], 'error'> }
|
||||||
|
type MCPServerError = MCPServer & { status: 'error' }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type ClientInfo = {
|
||||||
|
_client: Client, // _client is the client that connects with an mcp client. We're calling mcp clients "server" everywhere except here for naming consistency.
|
||||||
|
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||||
|
mcpServer: MCPServerNonError,
|
||||||
|
} | {
|
||||||
|
_client?: undefined,
|
||||||
|
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||||
|
mcpServer: MCPServerError,
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfoOfClientId = {
|
||||||
|
[clientId: string]: ClientInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MCPChannel implements IServerChannel {
|
||||||
|
|
||||||
|
private readonly infoOfClientId: InfoOfClientId = {}
|
||||||
|
private readonly _refreshingServerNames: Set<string> = new Set()
|
||||||
|
|
||||||
|
// mcp emitters
|
||||||
|
private readonly mcpEmitters = {
|
||||||
|
serverEvent: {
|
||||||
|
onAdd: new Emitter<MCPServerEventResponse>(),
|
||||||
|
onUpdate: new Emitter<MCPServerEventResponse>(),
|
||||||
|
onDelete: new Emitter<MCPServerEventResponse>(),
|
||||||
|
}
|
||||||
|
} satisfies {
|
||||||
|
serverEvent: {
|
||||||
|
onAdd: Emitter<MCPServerEventResponse>,
|
||||||
|
onUpdate: Emitter<MCPServerEventResponse>,
|
||||||
|
onDelete: Emitter<MCPServerEventResponse>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
) { }
|
||||||
|
|
||||||
|
// browser uses this to listen for changes
|
||||||
|
listen(_: unknown, event: string): Event<any> {
|
||||||
|
|
||||||
|
// server events
|
||||||
|
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.onAdd.event;
|
||||||
|
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.onUpdate.event;
|
||||||
|
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.onDelete.event;
|
||||||
|
// else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.onChangeLoading.event;
|
||||||
|
|
||||||
|
// tool call events
|
||||||
|
|
||||||
|
// handle unknown events
|
||||||
|
else throw new Error(`Event not found: ${event}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// browser uses this to call (see this.channel.call() in mcpConfigService.ts for all usages)
|
||||||
|
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||||
|
try {
|
||||||
|
if (command === 'refreshMCPServers') {
|
||||||
|
await this._refreshMCPServers(params)
|
||||||
|
}
|
||||||
|
else if (command === 'closeAllMCPServers') {
|
||||||
|
await this._closeAllMCPServers()
|
||||||
|
}
|
||||||
|
else if (command === 'toggleMCPServer') {
|
||||||
|
await this._toggleMCPServer(params.serverName, params.isOn)
|
||||||
|
}
|
||||||
|
else if (command === 'callTool') {
|
||||||
|
const p: MCPToolCallParams = params
|
||||||
|
const response = await this._safeCallTool(p.serverName, p.toolName, p.params)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('mcp channel: Call Error:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// server functions
|
||||||
|
|
||||||
|
|
||||||
|
private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName, addedServerNames: string[], removedServerNames: string[], updatedServerNames: string[] }) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
mcpConfigFileJSON,
|
||||||
|
userStateOfName,
|
||||||
|
addedServerNames,
|
||||||
|
removedServerNames,
|
||||||
|
updatedServerNames,
|
||||||
|
} = params
|
||||||
|
|
||||||
|
const { mcpServers: mcpServersJSON } = mcpConfigFileJSON
|
||||||
|
|
||||||
|
const allChanges: { type: 'added' | 'removed' | 'updated', serverName: string }[] = [
|
||||||
|
...addedServerNames.map(n => ({ serverName: n, type: 'added' }) as const),
|
||||||
|
...removedServerNames.map(n => ({ serverName: n, type: 'removed' }) as const),
|
||||||
|
...updatedServerNames.map(n => ({ serverName: n, type: 'updated' }) as const),
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
allChanges.map(async ({ serverName, type }) => {
|
||||||
|
|
||||||
|
// check if already refreshing
|
||||||
|
if (this._refreshingServerNames.has(serverName)) return
|
||||||
|
this._refreshingServerNames.add(serverName)
|
||||||
|
|
||||||
|
const prevServer = this.infoOfClientId[serverName]?.mcpServer;
|
||||||
|
|
||||||
|
// close and delete the old client
|
||||||
|
if (type === 'removed' || type === 'updated') {
|
||||||
|
await this._closeClient(serverName)
|
||||||
|
delete this.infoOfClientId[serverName]
|
||||||
|
this.mcpEmitters.serverEvent.onDelete.fire({ response: { prevServer, name: serverName, } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new client
|
||||||
|
if (type === 'added' || type === 'updated') {
|
||||||
|
const clientInfo = await this._createClient(mcpServersJSON[serverName], serverName, userStateOfName[serverName]?.isOn)
|
||||||
|
this.infoOfClientId[serverName] = clientInfo
|
||||||
|
this.mcpEmitters.serverEvent.onAdd.fire({ response: { newServer: clientInfo.mcpServer, name: serverName, } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
allChanges.forEach(({ serverName, type }) => {
|
||||||
|
this._refreshingServerNames.delete(serverName)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createClientUnsafe(server: MCPConfigFileEntryJSON, serverName: string, isOn: boolean): Promise<ClientInfo> {
|
||||||
|
|
||||||
|
const clientConfig = getClientConfig(serverName)
|
||||||
|
const client = new Client(clientConfig)
|
||||||
|
let transport: Transport;
|
||||||
|
let info: MCPServerNonError;
|
||||||
|
|
||||||
|
if (server.url) {
|
||||||
|
// first try HTTP, fall back to SSE
|
||||||
|
try {
|
||||||
|
transport = new StreamableHTTPClientTransport(server.url);
|
||||||
|
await client.connect(transport);
|
||||||
|
console.log(`Connected via HTTP to ${serverName}`);
|
||||||
|
const { tools } = await client.listTools()
|
||||||
|
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||||
|
info = {
|
||||||
|
status: isOn ? 'success' : 'offline',
|
||||||
|
tools: toolsWithUniqueName,
|
||||||
|
command: server.url.toString(),
|
||||||
|
}
|
||||||
|
} catch (httpErr) {
|
||||||
|
console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr);
|
||||||
|
transport = new SSEClientTransport(server.url);
|
||||||
|
await client.connect(transport);
|
||||||
|
const { tools } = await client.listTools()
|
||||||
|
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||||
|
console.log(`Connected via SSE to ${serverName}`);
|
||||||
|
info = {
|
||||||
|
status: isOn ? 'success' : 'offline',
|
||||||
|
tools: toolsWithUniqueName,
|
||||||
|
command: server.url.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (server.command) {
|
||||||
|
console.log('ENV DATA: ', server.env)
|
||||||
|
transport = new StdioClientTransport({
|
||||||
|
command: server.command,
|
||||||
|
args: server.args,
|
||||||
|
env: {
|
||||||
|
...server.env,
|
||||||
|
...process.env
|
||||||
|
} as Record<string, string>,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect(transport)
|
||||||
|
|
||||||
|
// Get the tools from the server
|
||||||
|
const { tools } = await client.listTools()
|
||||||
|
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||||
|
|
||||||
|
// Create a full command string for display
|
||||||
|
const fullCommand = `${server.command} ${server.args?.join(' ') || ''}`
|
||||||
|
|
||||||
|
// Format server object
|
||||||
|
info = {
|
||||||
|
status: isOn ? 'success' : 'offline',
|
||||||
|
tools: toolsWithUniqueName,
|
||||||
|
command: fullCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(`No url or command for server ${serverName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return { _client: client, mcpServerEntryJSON: server, mcpServer: info }
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addUniquePrefix(base: string) {
|
||||||
|
return `${Math.random().toString(36).slice(2, 8)}_${base}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise<ClientInfo> {
|
||||||
|
try {
|
||||||
|
const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn)
|
||||||
|
return c
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to connect to server "${serverName}":`, err)
|
||||||
|
const fullCommand = !serverConfig.command ? '' : `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}`
|
||||||
|
const c: MCPServerError = { status: 'error', error: err + '', command: fullCommand, }
|
||||||
|
return { mcpServerEntryJSON: serverConfig, mcpServer: c, }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _closeAllMCPServers() {
|
||||||
|
for (const serverName in this.infoOfClientId) {
|
||||||
|
await this._closeClient(serverName)
|
||||||
|
delete this.infoOfClientId[serverName]
|
||||||
|
}
|
||||||
|
console.log('Closed all MCP servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _closeClient(serverName: string) {
|
||||||
|
const info = this.infoOfClientId[serverName]
|
||||||
|
if (!info) return
|
||||||
|
const { _client: client } = info
|
||||||
|
if (client) {
|
||||||
|
await client.close()
|
||||||
|
}
|
||||||
|
console.log(`Closed MCP server ${serverName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async _toggleMCPServer(serverName: string, isOn: boolean) {
|
||||||
|
const prevServer = this.infoOfClientId[serverName]?.mcpServer
|
||||||
|
// Handle turning on the server
|
||||||
|
if (isOn) {
|
||||||
|
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||||
|
const clientInfo = await this._createClientUnsafe(this.infoOfClientId[serverName].mcpServerEntryJSON, serverName, isOn)
|
||||||
|
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||||
|
response: {
|
||||||
|
name: serverName,
|
||||||
|
newServer: clientInfo.mcpServer,
|
||||||
|
prevServer: prevServer,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle turning off the server
|
||||||
|
else {
|
||||||
|
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||||
|
this._closeClient(serverName)
|
||||||
|
delete this.infoOfClientId[serverName]._client
|
||||||
|
|
||||||
|
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||||
|
response: {
|
||||||
|
name: serverName,
|
||||||
|
newServer: {
|
||||||
|
status: 'offline',
|
||||||
|
tools: [],
|
||||||
|
command: '',
|
||||||
|
// Explicitly set error to undefined to reset the error state
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
prevServer: prevServer,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tool call functions
|
||||||
|
|
||||||
|
private async _callTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||||
|
const server = this.infoOfClientId[serverName]
|
||||||
|
if (!server) throw new Error(`Server ${serverName} not found`)
|
||||||
|
const { _client: client } = server
|
||||||
|
if (!client) throw new Error(`Client for server ${serverName} not found`)
|
||||||
|
|
||||||
|
// Call the tool with the provided parameters
|
||||||
|
const response = await client.callTool({
|
||||||
|
name: toolName,
|
||||||
|
arguments: params
|
||||||
|
})
|
||||||
|
const { content } = response as CallToolResult
|
||||||
|
const returnValue = content[0]
|
||||||
|
|
||||||
|
if (returnValue.type === 'text') {
|
||||||
|
// handle text response
|
||||||
|
|
||||||
|
if (response.isError) {
|
||||||
|
throw new Error(`Tool call error: ${response.content}`)
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle success
|
||||||
|
return {
|
||||||
|
event: 'text',
|
||||||
|
text: returnValue.text,
|
||||||
|
toolName,
|
||||||
|
serverName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (returnValue.type === 'audio') {
|
||||||
|
// // handle audio response
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (returnValue.type === 'image') {
|
||||||
|
// // handle image response
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (returnValue.type === 'resource') {
|
||||||
|
// // handle resource response
|
||||||
|
// }
|
||||||
|
|
||||||
|
throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for tool ${toolName} on server ${serverName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tool call error wrapper
|
||||||
|
private async _safeCallTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||||
|
try {
|
||||||
|
const response = await this._callTool(serverName, toolName, params)
|
||||||
|
return response
|
||||||
|
} catch (err) {
|
||||||
|
let errorMessage: string;
|
||||||
|
|
||||||
|
// Check if it's an MCP error with a code
|
||||||
|
if (err && typeof err === 'object' && 'code' in err) {
|
||||||
|
const errorCode = err.code;
|
||||||
|
const errorName = err.name || 'Unknown Error';
|
||||||
|
const errorMsg = err.message || '';
|
||||||
|
|
||||||
|
// Map common JSON-RPC error codes to user-friendly messages
|
||||||
|
let codeDescription = '';
|
||||||
|
switch (errorCode) {
|
||||||
|
case -32700:
|
||||||
|
codeDescription = 'Parse Error';
|
||||||
|
break;
|
||||||
|
case -32600:
|
||||||
|
codeDescription = 'Invalid Request';
|
||||||
|
break;
|
||||||
|
case -32601:
|
||||||
|
codeDescription = 'Method Not Found';
|
||||||
|
break;
|
||||||
|
case -32602:
|
||||||
|
codeDescription = 'Invalid Parameters';
|
||||||
|
break;
|
||||||
|
case -32603:
|
||||||
|
codeDescription = 'Internal Error';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
codeDescription = `Error Code ${errorCode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`;
|
||||||
|
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||||
|
// Standard error with message
|
||||||
|
errorMessage = err.message;
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
// String error
|
||||||
|
errorMessage = err;
|
||||||
|
} else {
|
||||||
|
// Unknown error format
|
||||||
|
errorMessage = JSON.stringify(err, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`;
|
||||||
|
const errorResponse: MCPToolErrorResponse = {
|
||||||
|
event: 'error',
|
||||||
|
text: fullErrorMessage,
|
||||||
|
toolName,
|
||||||
|
serverName,
|
||||||
|
}
|
||||||
|
return errorResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue