diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 99bf1033707..69dd832a312 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; @@ -44,6 +45,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _pendingProgress = new Map void>(); private readonly _proxy: ExtHostChatAgentsShape2; + private _responsePartHandlePool = 0; + private readonly _activeResponsePartPromises = new Map>(); + constructor( extHostContext: IExtHostContext, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @@ -166,7 +170,25 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); } - async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise { + async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { + if (progress.kind === 'progressTask') { + const handle = ++this._responsePartHandlePool; + const responsePartId = `${requestId}_${handle}`; + const deferredContentPromise = new DeferredPromise(); + this._activeResponsePartPromises.set(responsePartId, deferredContentPromise); + this._pendingProgress.get(requestId)?.({ ...progress, task: () => deferredContentPromise.p, isSettled: () => deferredContentPromise.isSettled }); + return handle; + } else if (progress.kind === 'progressTaskResult' && responsePartHandle !== undefined) { + const responsePartId = `${requestId}_${responsePartHandle}`; + const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId); + if (deferredContentPromise && progress.content) { + deferredContentPromise.complete(progress.content.value); + this._activeResponsePartPromises.delete(responsePartId); + } else { + deferredContentPromise?.complete(undefined); + } + return responsePartHandle; + } const revivedProgress = revive(progress); this._pendingProgress.get(requestId)?.(revivedProgress as IChatProgress); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fecf8932231..0a622e4d95a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1704,6 +1704,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, + ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2dd52e38465..c0babcbd2be 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -52,7 +52,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; @@ -1238,7 +1238,7 @@ export interface MainThreadChatAgentsShape2 extends IDisposable { $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $unregisterAgent(handle: number): void; - $handleProgressChunk(requestId: string, chunk: IChatProgressDto): Promise; + $handleProgressChunk(requestId: string, chunk: IChatProgressDto, handle?: number): Promise; $transferActiveChatSession(toWorkspace: UriComponents): void; } @@ -1253,7 +1253,8 @@ export interface IChatAgentCompletionItem { } export type IChatContentProgressDto = - | Dto; + | Dto> + | IChatTaskDto; export type IChatAgentHistoryEntryDto = { request: IChatAgentRequest; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index d981bf150d2..885bb627bf0 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -68,12 +68,20 @@ class ChatAgentResponseStream { } } - const _report = (progress: Dto) => { + const _report = (progress: Dto, task?: () => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - this._proxy.$handleProgressChunk(this._request.requestId, progress); + + this._proxy.$handleProgressChunk(this._request.requestId, progress) + .then((handle) => { + if (typeof handle === 'number' && task) { + task().then((res) => { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + }); + } + }); }; this._apiObject = { @@ -116,11 +124,11 @@ class ChatAgentResponseStream { _report(dto); return this; }, - progress(value) { + progress(value, task?: (() => Thenable)) { throwIfDone(this.progress); - const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.from(part); - _report(dto); + const part = new extHostTypes.ChatResponseProgressPart2(value, task); + const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); + _report(dto, task); return this; }, warning(value) { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b0daab77b36..3021b228fb4 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTask, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2391,6 +2391,24 @@ export namespace ChatResponseWarningPart { } } +export namespace ChatTask { + export function from(part: vscode.ChatResponseProgressPart2): Dto { + return { + kind: 'progressTask', + content: MarkdownString.from(part.value), + }; + } +} + +export namespace ChatTaskResult { + export function from(part: string | void): Dto { + return { + kind: 'progressTaskResult', + content: typeof part === 'string' ? MarkdownString.from(part) : undefined + }; + } +} + export namespace ChatResponseCommandButtonPart { export function from(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 63b6723fdf2..a21e39c192b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4374,6 +4374,15 @@ export class ChatResponseProgressPart { } } +export class ChatResponseProgressPart2 { + value: string; + task?: () => Thenable; + constructor(value: string, task?: () => Thenable) { + this.value = value; + this.task = task; + } +} + export class ChatResponseWarningPart { value: vscode.MarkdownString; constructor(value: string | vscode.MarkdownString) { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index c7baf4327cb..ad2d8e5564b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -70,7 +70,7 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -513,11 +513,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer): ReadonlyArray { - const result: Exclude[] = []; + const result: Exclude[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 277313e6d47..f303f0b56c6 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -25,13 +25,13 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc export interface IChatAgentHistoryEntry { request: IChatAgentRequest; - response: ReadonlyArray; + response: ReadonlyArray; result: IChatAgentResult; } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 3f456e6f2d7..dd896599889 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -63,6 +63,7 @@ export type IChatProgressResponseContent = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage + | IChatTask | IChatTextEditGroup | IChatConfirmation; @@ -170,7 +171,7 @@ export class Response implements IResponse { this._updateRepr(true); } - updateContent(progress: IChatProgressResponseContent | IChatTextEdit, quiet?: boolean): void { + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatTask, quiet?: boolean): void { if (progress.kind === 'markdownContent') { const responsePartLength = this._responseParts.length - 1; const lastResponsePart = this._responseParts[responsePartLength]; @@ -208,6 +209,20 @@ export class Response implements IResponse { } this._updateRepr(quiet); } + } else if (progress.kind === 'progressTask') { + // Add a new resolving part + const responsePosition = this._responseParts.push(progress) - 1; + this._updateRepr(quiet); + + if (progress.task) { + progress.task?.().then((content) => { + // Replace the resolving part's content with the resolved response + if (typeof content === 'string') { + this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; + } + this._updateRepr(false); + }); + } } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -788,6 +803,7 @@ export class ChatModel extends Disposable implements IChatModel { progress.kind === 'command' || progress.kind === 'textEdit' || progress.kind === 'warning' || + progress.kind === 'progressTask' || progress.kind === 'confirmation' ) { request.response.updateContent(progress, quiet); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index c9a9be9a42e..2664be3798c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -106,6 +106,21 @@ export interface IChatProgressMessage { kind: 'progressMessage'; } +export interface IChatTask extends IChatTaskDto { + task?: () => Promise; + isSettled?: () => boolean; +} + +export interface IChatTaskDto { + content: IMarkdownString; + kind: 'progressTask'; +} + +export interface IChatTaskResult { + content: IMarkdownString | void; + kind: 'progressTaskResult'; +} + export interface IChatWarningMessage { content: IMarkdownString; kind: 'warning'; @@ -149,6 +164,8 @@ export type IChatProgress = | IChatContentInlineReference | IChatAgentDetection | IChatProgressMessage + | IChatTask + | IChatTaskResult | IChatCommandButton | IChatWarningMessage | IChatTextEdit diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 429d32cbc2c..37e0ea43d45 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -14,7 +14,7 @@ import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/ import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -95,7 +95,7 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation; +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTask; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 6ff2a8a71cf..d062407d47a 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -116,10 +116,28 @@ declare module 'vscode' { constructor(value: string | MarkdownString); } + export class ChatResponseProgressPart2 extends ChatResponseProgressPart { + value: string; + task?: () => Thenable; + constructor(value: string, task?: () => Thenable); + } + export interface ChatResponseStream { + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. + * @returns This stream. + */ + progress(value: string, task?: () => Thenable): ChatResponseStream; + textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; detectedParticipant(participant: string, command?: ChatCommand): ChatResponseStream; + push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): ChatResponseStream; /** * Show an inline message in the chat view asking the user to confirm an action.