diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 1636bf4db20..5e5e982cdfa 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -395,6 +395,9 @@ const _allApiProposals = { tokenInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', }, + toolProgress: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.toolProgress.d.ts', + }, treeViewActiveItem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 67502d9db2c..62fcc73e15e 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,7 +6,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolProgressStep, IToolResult, ToolProgress } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; @@ -16,7 +16,10 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre private readonly _proxy: ExtHostLanguageModelToolsShape; private readonly _tools = this._register(new DisposableMap()); - private readonly _countTokenCallbacks = new Map(); + private readonly _runningToolCalls = new Map(); constructor( extHostContext: IExtHostContext, @@ -45,26 +48,30 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre }; } + $acceptToolProgress(callId: string, progress: IToolProgressStep): void { + this._runningToolCalls.get(callId)?.progress.report(progress); + } + $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { - const fn = this._countTokenCallbacks.get(callId); + const fn = this._runningToolCalls.get(callId); if (!fn) { throw new Error(`Tool invocation call ${callId} not found`); } - return fn(input, token); + return fn.countTokens(input, token); } $registerTool(id: string): void { const disposable = this._languageModelToolsService.registerToolImplementation( id, { - invoke: async (dto, countTokens, token) => { + invoke: async (dto, countTokens, progress, token) => { try { - this._countTokenCallbacks.set(dto.callId, countTokens); + this._runningToolCalls.set(dto.callId, { countTokens, progress }); const resultDto = await this._proxy.$invokeTool(dto, token); return revive(resultDto) as IToolResult; } finally { - this._countTokenCallbacks.delete(dto.callId); + this._runningToolCalls.delete(dto.callId); } }, prepareToolInvocation: (parameters, token) => this._proxy.$prepareToolInvocation(id, parameters, token), diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index acfa2a7f3c9..d72909f4cae 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -59,7 +59,7 @@ import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatPro import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; -import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolProgressStep, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1380,6 +1380,7 @@ export type IToolDataDto = Omit; export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; + $acceptToolProgress(callId: string, progress: IToolProgressStep): void; $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $registerTool(id: string): void; diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index fcbd6c80691..9d3272028ee 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -138,7 +138,21 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); + let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined; + if (isProposedApiEnabled(item.extension, 'toolProgress')) { + progress = { + report: value => { + this._proxy.$acceptToolProgress(dto.callId, { + message: typeConvert.MarkdownString.fromStrict(value.message), + increment: value.increment, + total: 100, + }); + } + }; + } + + // todo: 'any' cast because TS can't handle the overloads + const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token); if (!extensionResult) { throw new CancellationError(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 16435e642ef..9303390e793 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -9,6 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorunWithStore } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; @@ -458,27 +459,37 @@ class ChatToolInvocationSubPart extends Disposable { } private createProgressPart(): HTMLElement { - let content: IMarkdownString; if (this.toolInvocation.isComplete && this.toolInvocation.isConfirmed !== false && this.toolInvocation.pastTenseMessage) { - content = typeof this.toolInvocation.pastTenseMessage === 'string' ? - new MarkdownString().appendText(this.toolInvocation.pastTenseMessage) : - this.toolInvocation.pastTenseMessage; + const part = this.renderProgressContent(this.toolInvocation.pastTenseMessage); + this._register(part); + return part.domNode; } else { - content = typeof this.toolInvocation.invocationMessage === 'string' ? - new MarkdownString().appendText(this.toolInvocation.invocationMessage + '…') : - MarkdownString.lift(this.toolInvocation.invocationMessage).appendText('…'); + const container = document.createElement('div'); + const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; + this._register(autorunWithStore((reader, store) => { + const progress = progressObservable?.read(reader); + const part = store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage)); + dom.reset(container, part.domNode); + })); + return container; + } + } + + private renderProgressContent(content: IMarkdownString | string) { + if (typeof content === 'string') { + content = new MarkdownString().appendText(content); } const progressMessage: IChatProgressMessage = { kind: 'progressMessage', content }; + const iconOverride = !this.toolInvocation.isConfirmed ? Codicon.error : this.toolInvocation.isComplete ? Codicon.check : undefined; - const progressPart = this._register(this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride)); - return progressPart.domNode; + return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride); } private createTerminalMarkdownProgressPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, terminalData: IChatTerminalToolInvocationData): HTMLElement { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 7b15fa89f3f..3f7389ecdde 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -7,6 +7,7 @@ import { $ } from '../../../../base/browser/dom.js'; import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { timeout } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { isCancellationError } from '../../../../base/common/errors.js'; @@ -52,11 +53,13 @@ import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; -import { IChatRequestModel, ChatRequestModel, ChatModel, IChatRequestVariableData, IChatRequestToolEntry } from '../common/chatModel.js'; +import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestToolEntry, IChatRequestVariableData } from '../common/chatModel.js'; +import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js'; import { IChatProgress, IChatService } from '../common/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; import { ILanguageModelsService } from '../common/languageModels.js'; @@ -64,9 +67,6 @@ import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './acti import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js'; import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; import './media/chatSetup.css'; -import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -518,7 +518,7 @@ class SetupTool extends Disposable implements IToolImpl { super(); } - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise { const result: IToolResult = { content: [ { diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index fa680a553d1..b21654e4d9e 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -12,7 +12,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; @@ -40,6 +40,11 @@ interface IToolEntry { impl?: IToolImpl; } +interface ITrackedCall { + invocation?: ChatToolInvocation; + store: IDisposable; +} + export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -53,7 +58,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private _toolContextKeys = new Set(); private readonly _ctxToolsCount: IContextKey; - private _callsByRequestId = new Map(); + private _callsByRequestId = new Map(); private _workspaceToolConfirmStore: Lazy; private _profileToolConfirmStore: Lazy; @@ -235,7 +240,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (!this._callsByRequestId.has(requestId)) { this._callsByRequestId.set(requestId, []); } - this._callsByRequestId.get(requestId)!.push(store); + const trackedCall: ITrackedCall = { store }; + this._callsByRequestId.get(requestId)!.push(trackedCall); const source = new CancellationTokenSource(); store.add(toDisposable(() => { @@ -252,6 +258,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const prepared = await this.prepareToolInvocation(tool, dto, token); toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId); + trackedCall.invocation = toolInvocation; if (this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace)) { toolInvocation.confirmed.complete(true); } @@ -285,7 +292,11 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo throw new CancellationError(); } - toolResult = await tool.impl.invoke(dto, countTokens, token); + toolResult = await tool.impl.invoke(dto, countTokens, { + report: step => { + toolInvocation?.acceptProgress(step); + } + }, token); this.ensureToolDetails(dto, toolResult, tool.data); this._telemetryService.publicLog2( @@ -403,7 +414,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private cleanupCallDisposables(requestId: string, store: DisposableStore): void { const disposables = this._callsByRequestId.get(requestId); if (disposables) { - const index = disposables.indexOf(store); + const index = disposables.findIndex(d => d.store === store); if (index > -1) { disposables.splice(index, 1); } @@ -417,7 +428,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo cancelToolCallsForRequest(requestId: string): void { const calls = this._callsByRequestId.get(requestId); if (calls) { - calls.forEach(call => call.dispose()); + calls.forEach(call => call.store.dispose()); this._callsByRequestId.delete(requestId); } } @@ -425,7 +436,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo public override dispose(): void { super.dispose(); - this._callsByRequestId.forEach(calls => dispose(calls)); + this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose())); this._ctxToolsCount.reset(); } } diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index ee387ceaa1f..b9b27e255f4 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -5,9 +5,10 @@ import { DeferredPromise } from '../../../../../base/common/async.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { observableValue } from '../../../../../base/common/observable.js'; import { localize } from '../../../../../nls.js'; import { IChatTerminalToolInvocationData, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../chatService.js'; -import { IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolResult } from '../languageModelToolsService.js'; +import { IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult } from '../languageModelToolsService.js'; export class ChatToolInvocation implements IChatToolInvocation { public readonly kind: 'toolInvocation' = 'toolInvocation'; @@ -45,6 +46,8 @@ export class ChatToolInvocation implements IChatToolInvocation { public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; + public readonly progress = observableValue<{ message?: string | IMarkdownString; progress: number }>(this, { progress: 0 }); + constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string) { const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; @@ -84,6 +87,14 @@ export class ChatToolInvocation implements IChatToolInvocation { return this._confirmationMessages; } + public acceptProgress(step: IToolProgressStep) { + const prev = this.progress.get(); + this.progress.set({ + progress: step.increment ? (prev.progress + step.increment) : prev.progress, + message: step.message, + }, undefined); + } + public toJSON(): IChatToolInvocationSerialized { return { kind: 'toolInvocationSerialized', diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index ec1d6c7b97e..9e9cead60f9 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -7,6 +7,7 @@ import { DeferredPromise } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; @@ -234,6 +235,7 @@ export interface IChatToolInvocation { invocationMessage: string | IMarkdownString; pastTenseMessage: string | IMarkdownString | undefined; resultDetails: IToolResult['toolResultDetails']; + progress: IObservable<{ message?: string | IMarkdownString; progress: number }>; readonly toolId: string; readonly toolCallId: string; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index aa6ca194f69..b574302dbc2 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -8,14 +8,15 @@ import { Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; +import { Location } from '../../../../editor/common/languages.js'; import { ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Location } from '../../../../editor/common/languages.js'; +import { IProgress } from '../../../../platform/progress/common/progress.js'; import { IChatTerminalToolInvocationData, IChatToolInputInvocationData } from './chatService.js'; -import { Schemas } from '../../../../base/common/network.js'; import { PromptElementJSON, stringifyPromptElementJSON } from './tools/promptTsxTypes.js'; export interface IToolData { @@ -40,6 +41,14 @@ export interface IToolData { supportsToolPicker?: boolean; } +export interface IToolProgressStep { + readonly message: string | IMarkdownString | undefined; + readonly increment: number | undefined; + readonly total: number | undefined; +} + +export type ToolProgress = IProgress; + export type ToolDataSource = | { type: 'extension'; @@ -134,7 +143,7 @@ export interface IPreparedToolInvocation { } export interface IToolImpl { - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise; prepareToolInvocation?(parameters: any, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index cdeff278af1..c2e8d61f62f 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -16,7 +16,7 @@ import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../common/languageModelToolsService.js'; export const ExtensionEditToolId = 'vscode_editFile'; export const InternalEditToolId = 'vscode_editFile_internal'; @@ -42,7 +42,7 @@ export class EditTool implements IToolImpl { @INotebookService private readonly notebookService: INotebookService, ) { } - async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { if (!invocation.context) { throw new Error('toolInvocationToken is required for this tool'); } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts index b81143b9467..632a9b9bbc3 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../nls.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart } from '../../common/languageModelToolsService.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart, ToolProgress } from '../../common/languageModelToolsService.js'; import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; export const FetchWebPageToolData: IToolData = { @@ -41,7 +41,7 @@ export class FetchWebPageTool implements IToolImpl { @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { const parsedUriResults = this._parseUris((invocation.parameters as { urls?: string[] }).urls); const validUris = Array.from(parsedUriResults.values()).filter((uri): uri is URI => !!uri); if (!validUris.length) { diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 855c89aefed..5454f7f7ca3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -149,7 +149,7 @@ suite('LanguageModelToolsService', () => { const toolBarrier = new Barrier(); const toolImpl: IToolImpl = { - invoke: async (invocation, countTokens, cancelToken) => { + invoke: async (invocation, countTokens, progress, cancelToken) => { assert.strictEqual(invocation.callId, '1'); assert.strictEqual(invocation.toolId, 'testTool'); assert.deepStrictEqual(invocation.parameters, { a: 1 }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index 87a60b7fc0e..2cf7956aa5a 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; export class MockLanguageModelToolsService implements ILanguageModelToolsService { @@ -46,6 +47,10 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService return undefined; } + acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { + + } + async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { return { content: [{ kind: 'text', value: 'result' }] diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index d86c92c7664..9668c594e98 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -9,8 +9,8 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; -import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; -import { ExtensionState, IExtensionsWorkbenchService, IExtension } from '../common/extensions.js'; +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { ExtensionState, IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; @@ -72,7 +72,7 @@ export class SearchExtensionsTool implements IToolImpl { @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { const params = invocation.parameters as InputParams; if (!params.keywords?.length && !params.category && !params.ids?.length) { return { diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index 51c78d8dd7a..2d3e09b6a4d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -16,12 +16,6 @@ const mcpActivationEventPrefix = 'onMcpCollection:'; export const mcpActivationEvent = (collectionId: string) => mcpActivationEventPrefix + collectionId; -const mcpSchemaExampleServer = { - command: 'node', - args: ['my-mcp-server.js'], - env: {}, -}; - export const enum DiscoverySource { ClaudeDesktop = 'claude-desktop', Windsurf = 'windsurf', @@ -55,15 +49,17 @@ export const mcpSchemaExampleServers = { } }; -const httpSchemaExample = { - url: 'http://localhost:3001/mcp', - headers: {}, +const httpSchemaExamples = { + 'my-mcp-server': { + url: 'http://localhost:3001/mcp', + headers: {}, + } }; export const mcpStdioServerSchema: IJSONSchema = { type: 'object', additionalProperties: false, - examples: [mcpSchemaExampleServer], + examples: [mcpSchemaExampleServers['mcp-server-time']], properties: { type: { type: 'string', @@ -110,14 +106,14 @@ export const mcpServerSchema: IJSONSchema = { servers: { examples: [ mcpSchemaExampleServers, - httpSchemaExample, + httpSchemaExamples, ], additionalProperties: { oneOf: [mcpStdioServerSchema, { type: 'object', additionalProperties: false, required: ['url'], - examples: [httpSchemaExample], + examples: [httpSchemaExamples['my-mcp-server']], properties: { type: { type: 'string', diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 3bc03f06603..01f828d7211 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -4,29 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import { raceCancellationError, Sequencer } from '../../../../base/common/async.js'; -import * as json from '../../../../base/common/json.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import * as json from '../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { autorun, autorunWithStore, derived, disposableObservableValue, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IOutputService } from '../../../services/output/common/output.js'; +import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { mcpActivationEvent } from './mcpConfiguration.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; type ServerBootData = { supportsLogging: boolean; @@ -497,7 +499,31 @@ export class McpTool implements IMcpTool { call(params: Record, token?: CancellationToken): Promise { // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. const name = this._definition.serverToolName ?? this._definition.name; - return this._server.callOn(h => h.callTool({ name, arguments: params }), token); + return this._server.callOn(h => h.callTool({ name, arguments: params }, token), token); + } + + callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise { + // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. + const name = this._definition.serverToolName ?? this._definition.name; + const progressToken = generateUuid(); + + return this._server.callOn(h => { + let lastProgressN = 0; + const listener = h.onDidReceiveProgressNotification((e) => { + if (e.params.progressToken === progressToken) { + progress.report({ + message: e.params.message, + increment: e.params.progress - lastProgressN, + total: e.params.total, + }); + lastProgressN = e.params.progress; + } + }); + + return h.callTool({ name, arguments: params, _meta: { progressToken } }, token).finally(() => { + listener.dispose(); + }); + }, token); } compare(other: IMcpTool): number { diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts index 8c0fa08cae0..011f50f026d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -466,7 +466,7 @@ export class McpServerRequestHandler extends Disposable { /** * Call a specific tool */ - callTool(params: MCP.CallToolRequest['params'], token?: CancellationToken): Promise { + callTool(params: MCP.CallToolRequest['params'] & MCP.Request['params'], token?: CancellationToken): Promise { return this.sendRequest({ method: 'tools/call', params }, token); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index bcc7320493a..536c63324e7 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -15,7 +15,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServer, McpServerMetadataCache } from './mcpServer.js'; import { IMcpServer, IMcpService, IMcpTool, McpCollectionDefinition, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; @@ -249,7 +249,7 @@ class McpToolImplementation implements IToolImpl { }; } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken) { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken) { const result: IToolResult = { content: [] @@ -257,7 +257,7 @@ class McpToolImplementation implements IToolImpl { const outputParts: string[] = []; - const callResult = await this._tool.call(invocation.parameters as Record, token); + const callResult = await this._tool.callWithProgress(invocation.parameters as Record, progress, token); for (const item of callResult.content) { if (item.type === 'text') { result.content.push({ diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 258040db89d..d87721225b8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equals as arraysEqual } from '../../../../base/common/arrays.js'; import { assertNever } from '../../../../base/common/assert.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { equals as objectsEqual } from '../../../../base/common/objects.js'; -import { equals as arraysEqual } from '../../../../base/common/arrays.js'; import { IObservable } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Location } from '../../../../editor/common/languages.js'; @@ -17,6 +17,7 @@ import { ExtensionIdentifier } from '../../../../platform/extensions/common/exte import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { MCP } from './modelContextProtocol.js'; @@ -259,6 +260,11 @@ export interface IMcpTool { * @throws {@link McpConnectionFailedError} if the connection to the server fails */ call(params: Record, token?: CancellationToken): Promise; + + /** + * Identical to {@link call}, but reports progress. + */ + callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise; } export const enum McpServerTransportType { diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts new file mode 100644 index 00000000000..0d20f626cc1 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * A progress update during an {@link LanguageModelTool.invoke} call. + */ + export interface ToolProgressStep { + /** + * A progress message that represents a chunk of work + */ + message?: string | MarkdownString; + /** + * An increment for discrete progress. Increments will be summed up until 100 (100%) is reached + */ + increment?: number; + } + + export interface LanguageModelTool { + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; + } +}