Skip to content

Commit 3876871

Browse files
committed
chat: allow tools to report progress
This adds a `toolProgress` proposed API that allows extensions to report progress keyed on the `toolInvocationToken`. Internally, when given to a tool, this now includes the call ID of the tool. We can use that both from extensions and internally to report progress for tools, and I hooked this up for MCP servers. Currently only the text is updated. Involved some changes in the progress service internally: - Previously `viewId` was a naked string, I wrapped it in an object to make it more identifiable. - The progress service is in `workbench/services` and directly calls into other services to effect progress. In leui of going for a full 'contribution' model, I made a small `ILanguageModelToolProgressService` that it writes state into and that can be read back out. The state (an observable) is kept as long as progress is ongoing or this is is an observer.
1 parent a2f628e commit 3876871

File tree

28 files changed

+248
-49
lines changed

28 files changed

+248
-49
lines changed

src/vs/platform/extensions/common/extensionsApiProposals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ const _allApiProposals = {
394394
tokenInformation: {
395395
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts',
396396
},
397+
toolProgress: {
398+
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.toolProgress.d.ts',
399+
},
397400
treeViewActiveItem: {
398401
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts',
399402
},

src/vs/platform/progress/common/progress.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,12 @@ export const enum ProgressLocation {
5050
Dialog = 20
5151
}
5252

53+
export type ProgressLocationDetailed = { viewId: string } | { toolInvocationCallId: string | undefined };
54+
55+
export type ProgressLocationDescriptor = ProgressLocation | ProgressLocationDetailed;
56+
5357
export interface IProgressOptions {
54-
readonly location: ProgressLocation | string;
58+
readonly location: ProgressLocationDescriptor;
5559
readonly title?: string;
5660
readonly source?: string | INotificationSource;
5761
readonly total?: number;
@@ -81,7 +85,7 @@ export interface IProgressWindowOptions extends IProgressOptions {
8185
}
8286

8387
export interface IProgressCompositeOptions extends IProgressOptions {
84-
readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string;
88+
readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | { viewId: string };
8589
readonly delay?: number;
8690
}
8791

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
815815

816816
return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } }));
817817
},
818-
withProgress<R>(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable<R>) {
818+
withProgress<R>(options: vscode.ProgressOptions | vscode.ProgressOptions2, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable<R>) {
819819
return extHostProgress.withProgress(extension, options, task);
820820
},
821821
createOutputChannel(name: string, options: string | { log: true } | undefined): any {

src/vs/workbench/api/common/extHostLanguageModelTools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape
7373
callId,
7474
parameters: options.input,
7575
tokenBudget: options.tokenizationOptions?.tokenBudget,
76-
context: options.toolInvocationToken as IToolInvocationContext | undefined,
76+
context: options.toolInvocationToken ? { ...(options.toolInvocationToken as IToolInvocationContext), callId } : undefined,
7777
chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined,
7878
chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined,
7979
}, token);

src/vs/workbench/api/common/extHostProgress.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { ProgressOptions } from 'vscode';
6+
import { ProgressOptions, ProgressOptions2 } from 'vscode';
77
import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol.js';
88
import { ProgressLocation } from './extHostTypeConverters.js';
99
import { Progress, IProgressStep } from '../../../platform/progress/common/progress.js';
1010
import { CancellationTokenSource, CancellationToken } from '../../../base/common/cancellation.js';
1111
import { throttle } from '../../../base/common/decorators.js';
1212
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
1313
import { onUnexpectedExternalError } from '../../../base/common/errors.js';
14+
import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js';
1415

1516
export class ExtHostProgress implements ExtHostProgressShape {
1617

@@ -22,11 +23,15 @@ export class ExtHostProgress implements ExtHostProgressShape {
2223
this._proxy = proxy;
2324
}
2425

25-
async withProgress<R>(extension: IExtensionDescription, options: ProgressOptions, task: (progress: Progress<IProgressStep>, token: CancellationToken) => Thenable<R>): Promise<R> {
26+
async withProgress<R>(extension: IExtensionDescription, options: ProgressOptions | ProgressOptions2, task: (progress: Progress<IProgressStep>, token: CancellationToken) => Thenable<R>): Promise<R> {
2627
const handle = this._handles++;
2728
const { title, location, cancellable } = options;
2829
const source = { label: extension.displayName || extension.name, id: extension.identifier.value };
2930

31+
if (typeof location === 'object' && location.hasOwnProperty('toolInvocationToken')) {
32+
checkProposedApiEnabled(extension, 'toolProgress');
33+
}
34+
3035
this._proxy.$startProgress(handle, { location: ProgressLocation.from(location), title, source, cancellable }, !extension.isUnderDevelopment ? extension.identifier.value : undefined).catch(onUnexpectedExternalError);
3136
return this._withProgress(handle, task, !!cancellable);
3237
}

src/vs/workbench/api/common/extHostTypeConverters.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/commo
3636
import { ITextEditorOptions } from '../../../platform/editor/common/editor.js';
3737
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
3838
import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js';
39-
import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js';
39+
import { ProgressLocation as MainProgressLocation, ProgressLocationDescriptor as MainProgressLocationDescriptor } from '../../../platform/progress/common/progress.js';
4040
import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js';
4141
import { IViewBadge } from '../../common/views.js';
4242
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
4343
import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
4444
import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js';
4545
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
46-
import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js';
46+
import { IToolData, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js';
4747
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
4848
import { IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js';
4949
import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js';
@@ -1458,9 +1458,13 @@ export namespace EndOfLine {
14581458
}
14591459

14601460
export namespace ProgressLocation {
1461-
export function from(loc: vscode.ProgressLocation | { viewId: string }): MainProgressLocation | string {
1461+
export function from(loc: vscode.ProgressLocation | { viewId: string } | { toolInvocationToken: object | undefined }): MainProgressLocationDescriptor {
14621462
if (typeof loc === 'object') {
1463-
return loc.viewId;
1463+
if ('toolInvocationToken' in loc) {
1464+
return { toolInvocationCallId: (loc.toolInvocationToken as IToolInvocationContext).callId };
1465+
} else {
1466+
return { viewId: loc.viewId };
1467+
}
14641468
}
14651469

14661470
switch (loc) {

src/vs/workbench/browser/parts/views/treeView.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
692692
const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);
693693
const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id));
694694
this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this));
695-
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
695+
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, <T>(task: Promise<T>) => this.progressService.withProgress({ location: { viewId: this.id } }, () => task));
696696
const aligner = this.treeDisposables.add(new Aligner(this.themeService));
697697
const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler());
698698
const renderer = this.treeDisposables.add(this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes));
@@ -1791,7 +1791,7 @@ export class CustomTreeView extends AbstractTreeView {
17911791
id: this.id,
17921792
});
17931793
this.createTree();
1794-
this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
1794+
this.progressService.withProgress({ location: { viewId: this.id } }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
17951795
.then(() => timeout(2000))
17961796
.then(() => {
17971797
this.updateMessage();

src/vs/workbench/browser/parts/views/viewPane.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,8 +651,8 @@ export abstract class ViewPane extends Pane implements IView {
651651
return this.progressIndicator;
652652
}
653653

654-
protected getProgressLocation(): string {
655-
return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id;
654+
protected getProgressLocation(): { viewId: string } {
655+
return { viewId: this.viewDescriptorService.getViewContainerByViewId(this.id)!.id };
656656
}
657657

658658
protected getLocationBasedColors(): IViewPaneLocationColors {

src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js';
99
import { Emitter } from '../../../../../base/common/event.js';
1010
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
1111
import { Disposable, DisposableStore, IDisposable, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js';
12+
import { autorunWithStore } from '../../../../../base/common/observable.js';
1213
import { ThemeIcon } from '../../../../../base/common/themables.js';
1314
import { URI } from '../../../../../base/common/uri.js';
1415
import { generateUuid } from '../../../../../base/common/uuid.js';
@@ -22,6 +23,8 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co
2223
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
2324
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
2425
import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
26+
import { IProgressService } from '../../../../../platform/progress/common/progress.js';
27+
import { ILanguageModelToolProgressService } from '../../../../services/progress/browser/languageModelToolProgressService.js';
2528
import { ChatContextKeys } from '../../common/chatContextKeys.js';
2629
import { IChatMarkdownContent, IChatProgressMessage, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js';
2730
import { IChatRendererContent } from '../../common/chatViewModel.js';
@@ -64,6 +67,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
6467
codeBlockModelCollection: CodeBlockModelCollection,
6568
codeBlockStartIndex: number,
6669
@IInstantiationService instantiationService: IInstantiationService,
70+
@IProgressService progressService: IProgressService,
6771
) {
6872
super();
6973

@@ -140,6 +144,7 @@ class ChatToolInvocationSubPart extends Disposable {
140144
@ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService,
141145
@ICommandService private readonly commandService: ICommandService,
142146
@IMarkerService private readonly markerService: IMarkerService,
147+
@ILanguageModelToolProgressService private readonly languageModelToolProgressService: ILanguageModelToolProgressService,
143148
) {
144149
super();
145150

@@ -454,27 +459,37 @@ class ChatToolInvocationSubPart extends Disposable {
454459
}
455460

456461
private createProgressPart(): HTMLElement {
457-
let content: IMarkdownString;
458462
if (this.toolInvocation.isComplete && this.toolInvocation.isConfirmed !== false && this.toolInvocation.pastTenseMessage) {
459-
content = typeof this.toolInvocation.pastTenseMessage === 'string' ?
460-
new MarkdownString().appendText(this.toolInvocation.pastTenseMessage) :
461-
this.toolInvocation.pastTenseMessage;
463+
const part = this.renderProgressContent(this.toolInvocation.pastTenseMessage);
464+
this._register(part);
465+
return part.domNode;
462466
} else {
463-
content = typeof this.toolInvocation.invocationMessage === 'string' ?
464-
new MarkdownString().appendText(this.toolInvocation.invocationMessage + '…') :
465-
MarkdownString.lift(this.toolInvocation.invocationMessage).appendText('…');
467+
const container = document.createElement('div');
468+
const progressObservable = this._register(this.languageModelToolProgressService.listenForProgress(this.toolInvocation.toolCallId));
469+
this._register(autorunWithStore((reader, store) => {
470+
const progress = progressObservable.object.read(reader);
471+
const part = store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage));
472+
dom.reset(container, part.domNode);
473+
}));
474+
return container;
475+
}
476+
}
477+
478+
private renderProgressContent(content: IMarkdownString | string) {
479+
if (typeof content === 'string') {
480+
content = new MarkdownString().appendText(content);
466481
}
467482

468483
const progressMessage: IChatProgressMessage = {
469484
kind: 'progressMessage',
470485
content
471486
};
487+
472488
const iconOverride = !this.toolInvocation.isConfirmed ?
473489
Codicon.error :
474490
this.toolInvocation.isComplete ?
475491
Codicon.check : undefined;
476-
const progressPart = this._register(this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride));
477-
return progressPart.domNode;
492+
return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride);
478493
}
479494

480495
private createTerminalMarkdownProgressPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, terminalData: IChatTerminalToolInvocationData): HTMLElement {

src/vs/workbench/contrib/chat/common/languageModelToolsService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface IToolInvocation {
8383

8484
export interface IToolInvocationContext {
8585
sessionId: string;
86+
callId: string | undefined;
8687
}
8788

8889
export function isToolInvocationContext(obj: any): obj is IToolInvocationContext {

0 commit comments

Comments
 (0)