Skip to content

Commit 10ade73

Browse files
authored
Support chat progress with transient content (microsoft#188526)
1 parent be83e99 commit 10ade73

File tree

7 files changed

+113
-16
lines changed

7 files changed

+113
-16
lines changed

src/vs/workbench/api/browser/mainThreadChat.ts

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

6+
import { DeferredPromise } from 'vs/base/common/async';
67
import { Emitter } from 'vs/base/common/event';
78
import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
89
import { URI, UriComponents } from 'vs/base/common/uri';
9-
import { ExtHostChatShape, ExtHostContext, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
10+
import { ExtHostChatShape, ExtHostContext, IChatRequestDto, IChatResponseProgressDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
1011
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
1112
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
1213
import { IChat, IChatDynamicRequest, IChatProgress, IChatRequest, IChatResponse, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
@@ -16,11 +17,14 @@ import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/ext
1617
export class MainThreadChat extends Disposable implements MainThreadChatShape {
1718

1819
private readonly _providerRegistrations = this._register(new DisposableMap<number>());
19-
private readonly _activeRequestProgressCallbacks = new Map<string, (progress: IChatProgress) => void>();
20+
private readonly _activeRequestProgressCallbacks = new Map<string, (progress: IChatProgress) => (DeferredPromise<string> | void)>();
2021
private readonly _stateEmitters = new Map<number, Emitter<any>>();
2122

2223
private readonly _proxy: ExtHostChatShape;
2324

25+
private _responsePartHandlePool = 0;
26+
private readonly _activeResponsePartPromises = new Map<string, DeferredPromise<string>>();
27+
2428
constructor(
2529
extHostContext: IExtHostContext,
2630
@IChatService private readonly _chatService: IChatService,
@@ -133,8 +137,25 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
133137
this._providerRegistrations.set(handle, unreg);
134138
}
135139

136-
$acceptResponseProgress(handle: number, sessionId: number, progress: IChatProgress): void {
140+
async $acceptResponseProgress(handle: number, sessionId: number, progress: IChatResponseProgressDto, responsePartHandle?: number): Promise<number | void> {
137141
const id = `${handle}_${sessionId}`;
142+
143+
if ('placeholder' in progress) {
144+
const responsePartId = `${id}_${++this._responsePartHandlePool}`;
145+
const deferredContentPromise = new DeferredPromise<string>();
146+
this._activeResponsePartPromises.set(responsePartId, deferredContentPromise);
147+
this._activeRequestProgressCallbacks.get(id)?.({ ...progress, resolvedContent: deferredContentPromise.p });
148+
return this._responsePartHandlePool;
149+
} else if (responsePartHandle) {
150+
// Complete an existing deferred promise with resolved content
151+
const responsePartId = `${id}_${responsePartHandle}`;
152+
const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId);
153+
if (deferredContentPromise && 'content' in progress) {
154+
deferredContentPromise.complete(progress.content);
155+
this._activeResponsePartPromises.delete(responsePartId);
156+
}
157+
}
158+
138159
this._activeRequestProgressCallbacks.get(id)?.(progress);
139160
}
140161

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { SaveReason } from 'vs/workbench/common/editor';
5151
import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views';
5252
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
5353
import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug';
54-
import { IChatProgress, IChatResponseErrorDetails, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
54+
import { IChatResponseErrorDetails, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
5555
import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon';
5656
import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService';
5757
import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
@@ -1166,13 +1166,15 @@ export interface IChatResponseDto {
11661166
};
11671167
}
11681168

1169+
export type IChatResponseProgressDto = { content: string } | { requestId: string } | { placeholder: string };
1170+
11691171
export interface MainThreadChatShape extends IDisposable {
11701172
$registerChatProvider(handle: number, id: string): Promise<void>;
11711173
$acceptChatState(sessionId: number, state: any): Promise<void>;
11721174
$addRequest(context: any): void;
11731175
$sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void;
11741176
$unregisterChatProvider(handle: number): Promise<void>;
1175-
$acceptResponseProgress(handle: number, sessionId: number, progress: IChatProgress): void;
1177+
$acceptResponseProgress(handle: number, sessionId: number, progress: IChatResponseProgressDto, responsePartHandle?: number): Promise<number | void>;
11761178
$transferChatSession(sessionId: number, toWorkspace: UriComponents): void;
11771179

11781180
$registerSlashCommandProvider(handle: number, chatProviderId: string): Promise<void>;

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

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

6+
import { raceCancellation } from 'vs/base/common/async';
67
import { CancellationToken } from 'vs/base/common/cancellation';
78
import { Emitter } from 'vs/base/common/event';
89
import { Iterable } from 'vs/base/common/iterator';
@@ -14,7 +15,7 @@ import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/exte
1415
import { ILogService } from 'vs/platform/log/common/log';
1516
import { ExtHostChatShape, IChatRequestDto, IChatResponseDto, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
1617
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
17-
import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
18+
import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
1819
import type * as vscode from 'vscode';
1920

2021
class ChatProviderWrapper<T> {
@@ -214,8 +215,20 @@ export class ExtHostChat implements ExtHostChatShape {
214215
firstProgress = stopWatch.elapsed();
215216
}
216217

217-
const vscodeProgress: IChatProgress = 'responseId' in progress ? { requestId: progress.responseId } : progress;
218-
this._proxy.$acceptResponseProgress(handle, sessionId, vscodeProgress);
218+
if ('responseId' in progress) {
219+
this._proxy.$acceptResponseProgress(handle, sessionId, { requestId: progress.responseId });
220+
} else if ('placeholder' in progress && 'resolvedContent' in progress) {
221+
const resolvedContent = Promise.all([this._proxy.$acceptResponseProgress(handle, sessionId, { placeholder: progress.placeholder }), progress.resolvedContent]);
222+
raceCancellation(resolvedContent, token).then((res) => {
223+
if (!res) {
224+
return; /* Cancelled */
225+
}
226+
const [progressHandle, progressContent] = res;
227+
this._proxy.$acceptResponseProgress(handle, sessionId, progressContent, progressHandle ?? undefined);
228+
});
229+
} else {
230+
this._proxy.$acceptResponseProgress(handle, sessionId, progress);
231+
}
219232
}
220233
};
221234
let result: vscode.InteractiveResponseForProgress | undefined | null;

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,53 @@ export class ChatRequestModel implements IChatRequestModel {
8181
}
8282
}
8383

84+
85+
interface ResponsePart { string: IMarkdownString; resolving?: boolean }
86+
class Response {
87+
private _responseParts: ResponsePart[];
88+
private _responseRepr: IMarkdownString;
89+
90+
get value(): IMarkdownString {
91+
return this._responseRepr;
92+
}
93+
94+
constructor(value: IMarkdownString) {
95+
this._responseRepr = value;
96+
this._responseParts = [{ string: value }];
97+
}
98+
99+
updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise<string> }): void {
100+
if (typeof responsePart === 'string') {
101+
const responsePartLength = this._responseParts.length - 1;
102+
const lastResponsePart = this._responseParts[responsePartLength];
103+
104+
if (lastResponsePart.resolving === true) {
105+
// The last part is resolving, start a new part
106+
this._responseParts.push({ string: new MarkdownString(responsePart) });
107+
} else {
108+
// Combine this part with the last, non-resolving part
109+
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart) };
110+
}
111+
112+
this._updateRepr();
113+
} else {
114+
// Add a new resolving part
115+
const responsePosition = this._responseParts.push({ string: new MarkdownString(responsePart.placeholder), resolving: true });
116+
this._updateRepr();
117+
118+
responsePart.resolvedContent?.then((content) => {
119+
// Replace the resolving part's content with the resolved response
120+
this._responseParts[responsePosition] = { string: new MarkdownString(content) };
121+
this._updateRepr();
122+
});
123+
}
124+
}
125+
126+
private _updateRepr() {
127+
this._responseRepr = new MarkdownString(this._responseParts.map(r => r.string.value).join('\n'));
128+
}
129+
}
130+
84131
export class ChatResponseModel extends Disposable implements IChatResponseModel {
85132
private readonly _onDidChange = this._register(new Emitter<void>());
86133
readonly onDidChange = this._onDidChange.event;
@@ -112,8 +159,9 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
112159
return this._followups;
113160
}
114161

162+
private _response: Response;
115163
public get response(): IMarkdownString {
116-
return this._response;
164+
return this._response.value;
117165
}
118166

119167
public get errorDetails(): IChatResponseErrorDetails | undefined {
@@ -133,7 +181,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
133181
}
134182

135183
constructor(
136-
private _response: IMarkdownString,
184+
_response: IMarkdownString,
137185
public readonly session: ChatModel,
138186
private _isComplete: boolean = false,
139187
private _isCanceled = false,
@@ -143,13 +191,17 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
143191
private _followups?: IChatFollowup[]
144192
) {
145193
super();
194+
this._response = new Response(_response);
146195
this._id = 'response_' + ChatResponseModel.nextId++;
147196
}
148197

149-
updateContent(responsePart: string, quiet?: boolean) {
150-
this._response = new MarkdownString(this.response.value + responsePart);
151-
if (!quiet) {
152-
this._onDidChange.fire();
198+
updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise<string> }, quiet?: boolean) {
199+
try {
200+
this._response.updateContent(responsePart);
201+
} finally {
202+
if (!quiet) {
203+
this._onDidChange.fire();
204+
}
153205
}
154206
}
155207

@@ -453,6 +505,8 @@ export class ChatModel extends Disposable implements IChatModel {
453505

454506
if ('content' in progress) {
455507
request.response.updateContent(progress.content, quiet);
508+
} else if ('placeholder' in progress) {
509+
request.response.updateContent(progress, quiet);
456510
} else {
457511
request.setProviderRequestId(progress.requestId);
458512
request.response.setProviderResponseId(progress.requestId);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export interface IChatResponse {
4343
}
4444

4545
export type IChatProgress =
46-
{ content: string } | { requestId: string };
46+
{ content: string } | { requestId: string } | { placeholder: string; resolvedContent: Promise<string> };
4747

4848
export interface IPersistedChatState { }
4949
export interface IChatProvider {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ export class ChatService extends Disposable implements IChatService {
424424
gotProgress = true;
425425
if ('content' in progress) {
426426
this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);
427+
} else if ('placeholder' in progress) {
428+
this.trace('sendRequest', `Provider returned placeholder for session ${model.sessionId}, ${progress.placeholder}`);
427429
} else {
428430
this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);
429431
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,12 @@ declare module 'vscode' {
126126
responseId: string;
127127
}
128128

129-
export type InteractiveProgress = InteractiveProgressContent | InteractiveProgressId;
129+
export interface InteractiveProgressTask {
130+
placeholder: string;
131+
resolvedContent: Thenable<InteractiveProgressContent>;
132+
}
133+
134+
export type InteractiveProgress = InteractiveProgressContent | InteractiveProgressId | InteractiveProgressTask;
130135

131136
export interface InteractiveResponseCommand {
132137
commandId: string;

0 commit comments

Comments
 (0)