Skip to content

Commit 1c6825f

Browse files
authored
Implement "remove request/response" for Chat (microsoft#183380)
1 parent d564334 commit 1c6825f

File tree

13 files changed

+175
-29
lines changed

13 files changed

+175
-29
lines changed

src/vs/platform/actions/common/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class MenuId {
183183
static readonly InlineSuggestionToolbar = new MenuId('InlineSuggestionToolbar');
184184
static readonly ChatContext = new MenuId('ChatContext');
185185
static readonly ChatCodeBlock = new MenuId('ChatCodeblock');
186-
static readonly ChatTitle = new MenuId('ChatTitle');
186+
static readonly ChatMessageTitle = new MenuId('ChatMessageTitle');
187187
static readonly ChatExecute = new MenuId('ChatExecute');
188188

189189
/**

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
88
import { URI } from 'vs/base/common/uri';
99
import { ILogService } from 'vs/platform/log/common/log';
1010
import { IProductService } from 'vs/platform/product/common/productService';
11-
import { ExtHostContext, ExtHostChatShape, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
11+
import { ExtHostChatShape, ExtHostContext, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
1212
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
1313
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
14-
import { IChatProgress, IChatRequest, IChatResponse, IChat, IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
14+
import { IChat, IChatDynamicRequest, IChatProgress, IChatRequest, IChatResponse, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
1515
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
1616

1717
@extHostNamedCustomer(MainContext.MainThreadChat)
@@ -134,6 +134,9 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
134134
},
135135
provideFollowups: (session, token) => {
136136
return this._proxy.$provideFollowups(handle, session.id, token);
137+
},
138+
removeRequest: (session, requestId) => {
139+
return this._proxy.$removeRequest(handle, session.id, requestId);
137140
}
138141
});
139142

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,7 @@ export interface ExtHostChatShape {
11741174
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IChatReplyFollowup[])[] | undefined>;
11751175
$provideFollowups(handle: number, sessionId: number, token: CancellationToken): Promise<IChatFollowup[] | undefined>;
11761176
$provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined>;
1177+
$removeRequest(handle: number, sessionId: number, requestId: string): void;
11771178
$provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise<ISlashCommand[] | undefined>;
11781179
$releaseSession(sessionId: number): void;
11791180
$onDidPerformUserAction(event: IChatUserActionEvent): Promise<void>;

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/exte
1313
import { ILogService } from 'vs/platform/log/common/log';
1414
import { ExtHostChatShape, IChatRequestDto, IChatResponseDto, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
1515
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
16-
import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
16+
import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
1717
import type * as vscode from 'vscode';
1818

1919
class ChatProviderWrapper<T> {
@@ -158,6 +158,24 @@ export class ExtHostChat implements ExtHostChatShape {
158158
return rawFollowups?.map(f => typeConvert.ChatFollowup.from(f));
159159
}
160160

161+
$removeRequest(handle: number, sessionId: number, requestId: string): void {
162+
const entry = this._chatProvider.get(handle);
163+
if (!entry) {
164+
return;
165+
}
166+
167+
const realSession = this._chatSessions.get(sessionId);
168+
if (!realSession) {
169+
return;
170+
}
171+
172+
if (!entry.provider.removeRequest) {
173+
return;
174+
}
175+
176+
entry.provider.removeRequest(realSession, requestId);
177+
}
178+
161179
async $provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined> {
162180
const entry = this._chatProvider.get(handle);
163181
if (!entry) {
@@ -186,7 +204,8 @@ export class ExtHostChat implements ExtHostChatShape {
186204
firstProgress = stopWatch.elapsed();
187205
}
188206

189-
this._proxy.$acceptResponseProgress(handle, sessionId, progress);
207+
const vscodeProgress: IChatProgress = 'responseId' in progress ? { requestId: progress.responseId } : progress;
208+
this._proxy.$acceptResponseProgress(handle, sessionId, vscodeProgress);
190209
}
191210
};
192211
let result: vscode.InteractiveResponseForProgress | undefined | null;

src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
99
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
1010
import { localize } from 'vs/nls';
1111
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
12+
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
1213
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
1314
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
14-
import { CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
15+
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
1516
import { IChatService, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
16-
import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
17+
import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
1718
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
1819
import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon';
1920
import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
@@ -33,9 +34,10 @@ export function registerChatTitleActions() {
3334
icon: Codicon.thumbsup,
3435
toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('up'),
3536
menu: {
36-
id: MenuId.ChatTitle,
37+
id: MenuId.ChatMessageTitle,
3738
group: 'navigation',
38-
order: 1
39+
order: 1,
40+
when: CONTEXT_RESPONSE
3941
}
4042
});
4143
}
@@ -72,9 +74,10 @@ export function registerChatTitleActions() {
7274
icon: Codicon.thumbsdown,
7375
toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('down'),
7476
menu: {
75-
id: MenuId.ChatTitle,
77+
id: MenuId.ChatMessageTitle,
7678
group: 'navigation',
77-
order: 2
79+
order: 2,
80+
when: CONTEXT_RESPONSE
7881
}
7982
});
8083
}
@@ -110,10 +113,10 @@ export function registerChatTitleActions() {
110113
category: CHAT_CATEGORY,
111114
icon: Codicon.insert,
112115
menu: {
113-
id: MenuId.ChatTitle,
116+
id: MenuId.ChatMessageTitle,
114117
group: 'navigation',
115118
isHiddenByDefault: true,
116-
when: NOTEBOOK_IS_ACTIVE_EDITOR
119+
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CONTEXT_RESPONSE)
117120
}
118121
});
119122
}
@@ -172,6 +175,40 @@ export function registerChatTitleActions() {
172175
}
173176
}
174177
});
178+
179+
180+
registerAction2(class RemoveAction extends Action2 {
181+
constructor() {
182+
super({
183+
id: 'workbench.action.chat.remove',
184+
title: {
185+
value: localize('chat.remove.label', "Remove Request and Response"),
186+
original: 'Remove Request and Response'
187+
},
188+
f1: false,
189+
category: CHAT_CATEGORY,
190+
icon: Codicon.x,
191+
menu: {
192+
id: MenuId.ChatMessageTitle,
193+
group: 'navigation',
194+
order: 2,
195+
when: CONTEXT_REQUEST
196+
}
197+
});
198+
}
199+
200+
run(accessor: ServicesAccessor, ...args: any[]) {
201+
const item = args[0];
202+
if (!isRequestVM(item)) {
203+
return;
204+
}
205+
206+
const chatService = accessor.get(IChatService);
207+
if (item.providerRequestId) {
208+
chatService.removeRequest(item.sessionId, item.providerRequestId);
209+
}
210+
}
211+
});
175212
}
176213

177214
interface MarkdownContent {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/a
5454
import { IChatCodeBlockInfo } from 'vs/workbench/contrib/chat/browser/chat';
5555
import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
5656
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
57-
import { CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
57+
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
5858
import { IChatReplyFollowup, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
5959
import { IChatRequestViewModel, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
6060
import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
@@ -193,12 +193,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
193193

194194
const contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(rowContainer));
195195
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]));
196-
const titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, header, MenuId.ChatTitle, {
196+
const titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, header, MenuId.ChatMessageTitle, {
197197
menuOptions: {
198198
shouldForwardArgs: true
199199
},
200200
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
201-
if (action instanceof MenuItemAction) {
201+
if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.chat.voteDown' || action.item.id === 'workbench.action.chat.voteUp')) {
202202
return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions);
203203
}
204204

@@ -218,6 +218,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
218218
'welcome';
219219
this.traceLayout('renderElement', `${kind}, index=${index}`);
220220

221+
CONTEXT_RESPONSE.bindTo(templateData.contextKeyService).set(isResponseVM(element));
222+
CONTEXT_REQUEST.bindTo(templateData.contextKeyService).set(isRequestVM(element));
221223
CONTEXT_RESPONSE_HAS_PROVIDER_ID.bindTo(templateData.contextKeyService).set(isResponseVM(element) && !!element.providerResponseId);
222224
if (isResponseVM(element)) {
223225
CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set(element.vote === InteractiveSessionVoteDirection.Up ? 'up' : element.vote === InteractiveSessionVoteDirection.Down ? 'down' : '');

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@
6565
color: var(--vscode-badge-foreground) !important;
6666
}
6767

68-
.interactive-item-container:not(.interactive-response:hover) .header .monaco-toolbar,
69-
.interactive-item-container:not(.interactive-response:hover) .header .monaco-toolbar .action-label {
68+
.monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar,
69+
.monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar .action-label {
7070
/* Also apply this rule to the .action-label directly to work around a strange issue- when the
7171
toolbar is hidden without that second rule, tabbing from the list container into a list item doesn't work
7272
and the tab key doesn't do anything. */

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey<boolean>('chat
1010
export const CONTEXT_RESPONSE_VOTE = new RawContextKey<string>('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") });
1111
export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey<boolean>('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") });
1212

13+
export const CONTEXT_RESPONSE = new RawContextKey<boolean>('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") });
14+
export const CONTEXT_REQUEST = new RawContextKey<boolean>('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") });
15+
1316
export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey<boolean>('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") });
1417
export const CONTEXT_IN_CHAT_INPUT = new RawContextKey<boolean>('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") });
1518
export const CONTEXT_IN_CHAT_SESSION = new RawContextKey<boolean>('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") });

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse,
1414

1515
export interface IChatRequestModel {
1616
readonly id: string;
17+
readonly providerRequestId: string | undefined;
1718
readonly username: string;
1819
readonly avatarIconUri?: URI;
1920
readonly session: IChatModel;
@@ -56,6 +57,10 @@ export class ChatRequestModel implements IChatRequestModel {
5657
return this._id;
5758
}
5859

60+
public get providerRequestId(): string | undefined {
61+
return this._providerRequestId;
62+
}
63+
5964
public get username(): string {
6065
return this.session.requesterUsername;
6166
}
@@ -66,9 +71,14 @@ export class ChatRequestModel implements IChatRequestModel {
6671

6772
constructor(
6873
public readonly session: ChatModel,
69-
public readonly message: string | IChatReplyFollowup) {
74+
public readonly message: string | IChatReplyFollowup,
75+
private _providerRequestId?: string) {
7076
this._id = 'request_' + ChatRequestModel.nextId++;
7177
}
78+
79+
setProviderRequestId(providerRequestId: string) {
80+
this._providerRequestId = providerRequestId;
81+
}
7282
}
7383

7484
export class ChatResponseModel extends Disposable implements IChatResponseModel {
@@ -189,7 +199,7 @@ export interface ISerializableChatsData {
189199
}
190200

191201
export interface ISerializableChatRequestData {
192-
providerResponseId: string | undefined;
202+
providerRequestId: string | undefined;
193203
message: string;
194204
response: string | undefined;
195205
responseErrorDetails: IChatResponseErrorDetails | undefined;
@@ -230,7 +240,7 @@ export function isSerializableSessionData(obj: unknown): obj is ISerializableCha
230240
typeof data.sessionId === 'string';
231241
}
232242

233-
export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent;
243+
export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent;
234244

235245
export interface IChatAddRequestEvent {
236246
kind: 'addRequest';
@@ -242,6 +252,12 @@ export interface IChatAddResponseEvent {
242252
response: IChatResponseModel;
243253
}
244254

255+
export interface IChatRemoveRequestEvent {
256+
kind: 'removeRequest';
257+
requestId: string;
258+
responseId?: string;
259+
}
260+
245261
export interface IChatInitEvent {
246262
kind: 'initialize';
247263
}
@@ -360,9 +376,9 @@ export class ChatModel extends Disposable implements IChatModel {
360376
}
361377

362378
return requests.map((raw: ISerializableChatRequestData) => {
363-
const request = new ChatRequestModel(this, raw.message);
379+
const request = new ChatRequestModel(this, raw.message, raw.providerRequestId);
364380
if (raw.response || raw.responseErrorDetails) {
365-
request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups);
381+
request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups);
366382
}
367383
return request;
368384
});
@@ -433,7 +449,22 @@ export class ChatModel extends Disposable implements IChatModel {
433449
if ('content' in progress) {
434450
request.response.updateContent(progress.content);
435451
} else {
436-
request.response.setProviderResponseId(progress.responseId);
452+
request.setProviderRequestId(progress.requestId);
453+
request.response.setProviderResponseId(progress.requestId);
454+
}
455+
}
456+
457+
removeRequest(requestId: string): void {
458+
const index = this._requests.findIndex(request => request.providerRequestId === requestId);
459+
const request = this._requests[index];
460+
if (!request.providerRequestId) {
461+
return;
462+
}
463+
464+
if (index !== -1) {
465+
this._onDidChange.fire({ kind: 'removeRequest', requestId: request.providerRequestId, responseId: request.response?.providerResponseId });
466+
this._requests.splice(index, 1);
467+
request.response?.dispose();
437468
}
438469
}
439470

@@ -484,7 +515,7 @@ export class ChatModel extends Disposable implements IChatModel {
484515
}),
485516
requests: this._requests.map((r): ISerializableChatRequestData => {
486517
return {
487-
providerResponseId: r.response?.providerResponseId,
518+
providerRequestId: r.providerRequestId,
488519
message: typeof r.message === 'string' ? r.message : r.message.message,
489520
response: r.response ? r.response.response.value : undefined,
490521
responseErrorDetails: r.response?.errorDetails,

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

Lines changed: 3 additions & 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 } | { responseId: string };
46+
{ content: string } | { requestId: string };
4747

4848
export interface IPersistedChatState { }
4949
export interface IChatProvider {
@@ -56,6 +56,7 @@ export interface IChatProvider {
5656
provideFollowups?(session: IChat, token: CancellationToken): ProviderResult<IChatFollowup[] | undefined>;
5757
provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void, token: CancellationToken): ProviderResult<IChatResponse>;
5858
provideSlashCommands?(session: IChat, token: CancellationToken): ProviderResult<ISlashCommand[]>;
59+
removeRequest?(session: IChat, requestId: string): void;
5960
}
6061

6162
export interface ISlashCommandProvider {
@@ -186,6 +187,7 @@ export interface IChatService {
186187
* Returns whether the request was accepted.
187188
*/
188189
sendRequest(sessionId: string, message: string | IChatReplyFollowup): Promise<{ responseCompletePromise: Promise<void> } | undefined>;
190+
removeRequest(sessionid: string, requestId: string): Promise<void>;
189191
cancelCurrentRequestForSession(sessionId: string): void;
190192
getSlashCommands(sessionId: string, token: CancellationToken): Promise<ISlashCommand[] | undefined>;
191193
clearSession(sessionId: string): void;

0 commit comments

Comments
 (0)