Skip to content

Commit 5dc8011

Browse files
authored
Add chat progress message api (#198528)
* Add chat agent progress message update to model * Add chat progress messages above content * Check style, center spinner
1 parent aa61b56 commit 5dc8011

7 files changed

Lines changed: 90 additions & 19 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2339,8 +2339,12 @@ export namespace ChatResponseProgress {
23392339
} else if ('agentName' in progress) {
23402340
checkProposedApiEnabled(extension, 'chatAgents2Additions');
23412341
return { agentName: progress.agentName, command: progress.command, kind: 'agentDetection' };
2342-
} else {
2342+
} else if ('treeData' in progress) {
23432343
return { treeData: progress.treeData, kind: 'treeData' };
2344+
} else if ('message' in progress) {
2345+
return { content: progress.message, kind: 'progressMessage' };
2346+
} else {
2347+
throw new Error('Invalid progress type: ' + JSON.stringify(progress));
23442348
}
23452349
}
23462350
}

src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ interface IChatListItemTemplate {
7373
readonly agentAvatarContainer: HTMLElement;
7474
readonly username: HTMLElement;
7575
readonly detail: HTMLElement;
76+
readonly progressSteps: HTMLElement;
7677
readonly value: HTMLElement;
7778
readonly referencesListContainer: HTMLElement;
7879
readonly contextKeyService: IContextKeyService;
@@ -230,6 +231,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
230231
const detailContainer = dom.append(user, $('span.detail-container'));
231232
const detail = dom.append(detailContainer, $('span.detail'));
232233
dom.append(detailContainer, $('span.chat-animated-ellipsis'));
234+
const progressSteps = dom.append(rowContainer, $('.progress-steps'));
233235
const referencesListContainer = dom.append(rowContainer, $('.referencesListContainer'));
234236
const value = dom.append(rowContainer, $('.value'));
235237
const elementDisposables = new DisposableStore();
@@ -250,7 +252,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
250252
}));
251253

252254

253-
const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService };
255+
const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, progressSteps, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService };
254256
return template;
255257
}
256258

@@ -285,7 +287,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
285287

286288
dom.clearNode(templateData.detail);
287289
if (isResponseVM(element)) {
288-
this.renderProgressMessage(element, templateData);
290+
this.renderDetail(element, templateData);
291+
this.renderProgressSteps(element, templateData);
289292
}
290293

291294
// Do a progressive render if
@@ -325,7 +328,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
325328
}
326329
}
327330

328-
private renderProgressMessage(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
331+
private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
329332
let progressMsg: string = '';
330333
if (element.agent && !element.agent.metadata.isDefault) {
331334
let usingMsg = chatAgentLeader + element.agent.id;
@@ -350,6 +353,20 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
350353
}
351354
}
352355

356+
private renderProgressSteps(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
357+
dom.clearNode(templateData.progressSteps);
358+
if (element.response.value.length) {
359+
return;
360+
}
361+
362+
element.progressMessages.forEach((msg, index) => {
363+
const last = index === element.progressMessages.length - 1;
364+
const icon = last ? ThemeIcon.modify(Codicon.sync, 'spin') : Codicon.check;
365+
const step = dom.$('.progress-step', undefined, renderIcon(icon), dom.$('span.progress-step-message', undefined, msg.content));
366+
templateData.progressSteps.appendChild(step);
367+
});
368+
}
369+
353370
private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void {
354371
if (element.avatarIconUri) {
355372
const avatarImgIcon = dom.$<HTMLImageElement>('img.icon');
@@ -407,7 +424,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
407424
dom.clearNode(templateData.referencesListContainer);
408425

409426
if (isResponseVM(element)) {
410-
this.renderProgressMessage(element, templateData);
427+
this.renderDetail(element, templateData);
411428
}
412429

413430
this.renderContentReferencesIfNeeded(element, templateData, templateData.elementDisposables);

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,21 @@
586586
.interactive-session .chat-used-context .chat-used-context-label .monaco-button .codicon {
587587
margin: 0 0 0 4px;
588588
}
589+
590+
.interactive-item-container .progress-steps {
591+
display: flex;
592+
flex-direction: column;
593+
gap: 4px;
594+
margin-left: 4px;
595+
}
596+
597+
.interactive-item-container .progress-steps .progress-step {
598+
display: flex;
599+
gap: 5px;
600+
align-items: center;
601+
opacity: 0.7;
602+
}
603+
604+
.interactive-item-container .progress-steps .progress-step .codicon-check {
605+
color: var(--vscode-debugIcon-startForeground);
606+
}

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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange';
1616
import { ILogService } from 'vs/platform/log/common/log';
1717
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
1818
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
19-
import { IChat, IChatAsyncContent, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';
19+
import { IChat, IChatAsyncContent, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';
2020

2121
export interface IChatRequestModel {
2222
readonly id: string;
@@ -49,6 +49,7 @@ export interface IChatResponseModel {
4949
readonly agent?: IChatAgentData;
5050
readonly usedContext: IChatUsedContext | undefined;
5151
readonly contentReferences: ReadonlyArray<IChatContentReference>;
52+
readonly progressMessages: ReadonlyArray<IChatProgressMessage>;
5253
readonly slashCommand?: IChatAgentCommand;
5354
readonly response: IResponse;
5455
readonly isComplete: boolean;
@@ -249,6 +250,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
249250
return this._contentReferences;
250251
}
251252

253+
private readonly _progressMessages: IChatProgressMessage[] = [];
254+
public get progressMessages(): ReadonlyArray<IChatProgressMessage> {
255+
return this._progressMessages;
256+
}
252257

253258
constructor(
254259
_response: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference>,
@@ -269,15 +274,24 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
269274
this._id = 'response_' + ChatResponseModel.nextId++;
270275
}
271276

277+
/**
278+
* Apply a progress update to the actual response content.
279+
*/
272280
updateContent(responsePart: IChatProgressResponseContent | IChatContent, quiet?: boolean) {
273281
this._response.updateContent(responsePart, quiet);
274282
}
275283

276-
updateReferences(reference: IChatUsedContext | IChatContentReference) {
277-
if (reference.kind === 'usedContext') {
278-
this._usedContext = reference;
279-
} else if (reference.kind === 'reference') {
280-
this._contentReferences.push(reference);
284+
/**
285+
* Apply one of the progress updates that are not part of the actual response content.
286+
*/
287+
applyProgress(progress: IChatUsedContext | IChatContentReference | IChatProgressMessage) {
288+
if (progress.kind === 'usedContext') {
289+
this._usedContext = progress;
290+
} else if (progress.kind === 'reference') {
291+
this._contentReferences.push(progress);
292+
this._onDidChange.fire();
293+
} else if (progress.kind === 'progressMessage') {
294+
this._progressMessages.push(progress);
281295
this._onDidChange.fire();
282296
}
283297
}
@@ -528,11 +542,11 @@ export class ChatModel extends Disposable implements IChatModel {
528542
revive<ISerializableChatAgentData>(raw.agent) : undefined;
529543
request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, request.id, true, raw.isCanceled, raw.vote, raw.responseErrorDetails, raw.followups);
530544
if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?
531-
request.response.updateReferences(raw.usedContext);
545+
request.response.applyProgress(raw.usedContext);
532546
}
533547

534548
if (raw.contentReferences) {
535-
raw.contentReferences.forEach(r => request.response!.updateReferences(r));
549+
raw.contentReferences.forEach(r => request.response!.applyProgress(r));
536550
}
537551
}
538552
return request;
@@ -628,8 +642,8 @@ export class ChatModel extends Disposable implements IChatModel {
628642

629643
if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'asyncContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference') {
630644
request.response.updateContent(progress, quiet);
631-
} else if (progress.kind === 'usedContext' || progress.kind === 'reference') {
632-
request.response.updateReferences(progress);
645+
} else if (progress.kind === 'usedContext' || progress.kind === 'reference' || progress.kind === 'progressMessage') {
646+
request.response.applyProgress(progress);
633647
} else if (progress.kind === 'agentDetection') {
634648
const agent = this.chatAgentService.getAgent(progress.agentName);
635649
if (agent) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ export interface IChatAsyncContent {
125125
kind: 'asyncContent';
126126
}
127127

128+
export interface IChatProgressMessage {
129+
content: string;
130+
kind: 'progressMessage';
131+
}
132+
128133
export type IChatProgress =
129134
| IChatContent
130135
| IChatMarkdownContent
@@ -133,7 +138,8 @@ export type IChatProgress =
133138
| IChatUsedContext
134139
| IChatContentReference
135140
| IChatContentInlineReference
136-
| IChatAgentDetection;
141+
| IChatAgentDetection
142+
| IChatProgressMessage;
137143

138144
export interface IChatProvider {
139145
readonly id: string;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ILogService } from 'vs/platform/log/common/log';
1111
import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents';
1212
import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel';
1313
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
14-
import { IChatContentReference, IChatReplyFollowup, IChatResponseCommandFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
14+
import { IChatContentReference, IChatProgressMessage, IChatReplyFollowup, IChatResponseCommandFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
1515
import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
1616

1717
export function isRequestVM(item: unknown): item is IChatRequestViewModel {
@@ -96,6 +96,7 @@ export interface IChatResponseViewModel {
9696
readonly response: IResponse;
9797
readonly usedContext: IChatUsedContext | undefined;
9898
readonly contentReferences: ReadonlyArray<IChatContentReference>;
99+
readonly progressMessages: ReadonlyArray<IChatProgressMessage>;
99100
readonly isComplete: boolean;
100101
readonly isCanceled: boolean;
101102
readonly vote: InteractiveSessionVoteDirection | undefined;
@@ -297,6 +298,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
297298
return this._model.contentReferences;
298299
}
299300

301+
get progressMessages(): ReadonlyArray<IChatProgressMessage> {
302+
return this._model.progressMessages;
303+
}
304+
300305
get isComplete() {
301306
return this._model.isComplete;
302307
}

src/vscode-dts/vscode.proposed.chatAgents2.d.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,21 @@ declare module 'vscode' {
249249
variables: Record<string, ChatVariableValue[]>;
250250
}
251251

252-
// TODO@API should these each be prefixed ChatAgentProgress*?
253252
export type ChatAgentProgress =
254253
| ChatAgentContent
255254
| ChatAgentTask
256255
| ChatAgentFileTree
257256
| ChatAgentUsedContext
258257
| ChatAgentContentReference
259-
| ChatAgentInlineContentReference;
258+
| ChatAgentInlineContentReference
259+
| ChatAgentProgressMessage;
260+
261+
/**
262+
* Is displayed in the UI to communicate steps of progress to the user. Should be used when the agent may be slow to respond, e.g. due to doing extra work before sending the actual request to the LLM.
263+
*/
264+
export interface ChatAgentProgressMessage {
265+
message: string;
266+
}
260267

261268
/**
262269
* Indicates a piece of content that was used by the chat agent while processing the request. Will be displayed to the user.

0 commit comments

Comments
 (0)