Skip to content

Commit

Permalink
feat: support concurrent chat progress messages (#212300)
Browse files Browse the repository at this point in the history
* feat: support concurrent chat progress messages
  • Loading branch information
joyceerhl committed May 8, 2024
1 parent bd2df94 commit 9cb0df2
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 30 deletions.
24 changes: 23 additions & 1 deletion src/vs/workbench/api/browser/mainThreadChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
private readonly _pendingProgress = new Map<string, (part: IChatProgress) => void>();
private readonly _proxy: ExtHostChatAgentsShape2;

private _responsePartHandlePool = 0;
private readonly _activeResponsePartPromises = new Map<string, DeferredPromise<string | void>>();

constructor(
extHostContext: IExtHostContext,
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
Expand Down Expand Up @@ -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<number | void> {
async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise<number | void> {
if (progress.kind === 'progressTask') {
const handle = ++this._responsePartHandlePool;
const responsePartId = `${requestId}_${handle}`;
const deferredContentPromise = new DeferredPromise<string | void>();
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);
}
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<number | void>;
$handleProgressChunk(requestId: string, chunk: IChatProgressDto, handle?: number): Promise<number | void>;

$transferActiveChatSession(toWorkspace: UriComponents): void;
}
Expand All @@ -1253,7 +1253,8 @@ export interface IChatAgentCompletionItem {
}

export type IChatContentProgressDto =
| Dto<IChatProgressResponseContent>;
| Dto<Exclude<IChatProgressResponseContent, IChatTask>>
| IChatTaskDto;

export type IChatAgentHistoryEntryDto = {
request: IChatAgentRequest;
Expand Down
20 changes: 14 additions & 6 deletions src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,20 @@ class ChatAgentResponseStream {
}
}

const _report = (progress: Dto<IChatProgress>) => {
const _report = (progress: Dto<IChatProgress>, task?: () => Thenable<string | void>) => {
// 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 = {
Expand Down Expand Up @@ -116,11 +124,11 @@ class ChatAgentResponseStream {
_report(dto);
return this;
},
progress(value) {
progress(value, task?: (() => Thenable<string | void>)) {
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) {
Expand Down
20 changes: 19 additions & 1 deletion src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -2391,6 +2391,24 @@ export namespace ChatResponseWarningPart {
}
}

export namespace ChatTask {
export function from(part: vscode.ChatResponseProgressPart2): Dto<IChatTask> {
return {
kind: 'progressTask',
content: MarkdownString.from(part.value),
};
}
}

export namespace ChatTaskResult {
export function from(part: string | void): Dto<IChatTaskResult> {
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<IChatCommandButton> {
// If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore
Expand Down
9 changes: 9 additions & 0 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4374,6 +4374,15 @@ export class ChatResponseProgressPart {
}
}

export class ChatResponseProgressPart2 {
value: string;
task?: () => Thenable<string | void>;
constructor(value: string, task?: () => Thenable<string | void>) {
this.value = value;
this.task = task;
}
}

export class ChatResponseWarningPart {
value: vscode.MarkdownString;
constructor(value: string | vscode.MarkdownString) {
Expand Down
28 changes: 17 additions & 11 deletions src/vs/workbench/contrib/chat/browser/chatListRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -513,11 +513,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
: data.kind === 'markdownContent'
? this.renderMarkdown(data.content, element, templateData, fillInIncompleteTokens)
: data.kind === 'progressMessage' && onlyProgressMessagesAfterI(value, index) ? this.renderProgressMessage(data, false) // TODO render command
: data.kind === 'command' ? this.renderCommandButton(element, data)
: data.kind === 'textEditGroup' ? this.renderTextEdit(element, data, templateData)
: data.kind === 'warning' ? this.renderNotification('warning', data.content)
: data.kind === 'confirmation' ? this.renderConfirmation(element, data)
: undefined;
: data.kind === 'progressTask' && onlyProgressMessagesAfterI(value, index) ? this.renderProgressMessage(data, data.isSettled ? !data.isSettled() : false)
: data.kind === 'command' ? this.renderCommandButton(element, data)
: data.kind === 'textEditGroup' ? this.renderTextEdit(element, data, templateData)
: data.kind === 'warning' ? this.renderNotification('warning', data.content)
: data.kind === 'confirmation' ? this.renderConfirmation(element, data)
: undefined;

if (result) {
templateData.value.appendChild(result.element);
Expand Down Expand Up @@ -611,11 +612,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index),
isLast: index === renderableResponse.length - 1,
} satisfies IChatProgressMessageRenderData;
} else if (
part.kind === 'command' ||
} else if (part.kind === 'command' ||
part.kind === 'textEditGroup' ||
part.kind === 'confirmation'
) {
part.kind === 'confirmation' ||
part.kind === 'progressTask') {
partsToRender[index] = part;
} else {
const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), { renderedWordCount: 0, lastRenderTime: 0 });
Expand Down Expand Up @@ -690,6 +690,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
} else {
result = null;
}
} else if (isProgressTask(partToRender)) {
result = this.renderProgressMessage(partToRender, partToRender.isSettled ? !partToRender.isSettled() : false);
} else if (isCommandButtonRenderData(partToRender)) {
result = this.renderCommandButton(element, partToRender);
} else if (isTextEditRenderData(partToRender)) {
Expand Down Expand Up @@ -891,7 +893,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label);
}

private renderProgressMessage(progress: IChatProgressMessage, showSpinner: boolean): IMarkdownRenderResult {
private renderProgressMessage(progress: IChatProgressMessage | IChatTask, showSpinner: boolean): IMarkdownRenderResult {
if (showSpinner) {
// this step is in progress, communicate it to SR users
alert(progress.content.value);
Expand Down Expand Up @@ -1635,6 +1637,10 @@ function isProgressMessage(item: any): item is IChatProgressMessage {
return item && 'kind' in item && item.kind === 'progressMessage';
}

function isProgressTask(item: any): item is IChatTask {
return item && 'kind' in item && item.kind === 'progressTask';
}

function isProgressMessageRenderData(item: IChatRenderData): item is IChatProgressMessageRenderData {
return item && 'isAtEndOfResponse' in item;
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/chat/common/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, canMergeMarkdownStrings } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference, IChatMarkdownContent, IChatTask } from 'vs/workbench/contrib/chat/common/chatService';

export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI

export function annotateSpecialMarkdownContent(response: ReadonlyArray<IChatProgressResponseContent>): ReadonlyArray<IChatProgressRenderableResponseContent> {
const result: Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability>[] = [];
const result: Exclude<IChatProgressResponseContent | IChatTask, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability>[] = [];
for (const item of response) {
const previousItem = result[result.length - 1];
if (item.kind === 'inlineReference') {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/chat/common/chatAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IChatProgressResponseContent>;
response: ReadonlyArray<IChatProgressResponseContent | IChatTaskDto>;
result: IChatAgentResult;
}

Expand Down
20 changes: 18 additions & 2 deletions src/vs/workbench/contrib/chat/common/chatModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -63,6 +63,7 @@ export type IChatProgressResponseContent =
| IChatProgressMessage
| IChatCommandButton
| IChatWarningMessage
| IChatTask
| IChatTextEditGroup
| IChatConfirmation;

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 9cb0df2

Please sign in to comment.