Skip to content

Commit

Permalink
Implement "remove request/response" for Chat (#183380)
Browse files Browse the repository at this point in the history
  • Loading branch information
roblourens committed May 25, 2023
1 parent d564334 commit 1c6825f
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export class MenuId {
static readonly InlineSuggestionToolbar = new MenuId('InlineSuggestionToolbar');
static readonly ChatContext = new MenuId('ChatContext');
static readonly ChatCodeBlock = new MenuId('ChatCodeblock');
static readonly ChatTitle = new MenuId('ChatTitle');
static readonly ChatMessageTitle = new MenuId('ChatMessageTitle');
static readonly ChatExecute = new MenuId('ChatExecute');

/**
Expand Down
7 changes: 5 additions & 2 deletions src/vs/workbench/api/browser/mainThreadChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ExtHostContext, ExtHostChatShape, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostChatShape, ExtHostContext, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { IChatProgress, IChatRequest, IChatResponse, IChat, IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChat, IChatDynamicRequest, IChatProgress, IChatRequest, IChatResponse, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';

@extHostNamedCustomer(MainContext.MainThreadChat)
Expand Down Expand Up @@ -134,6 +134,9 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
},
provideFollowups: (session, token) => {
return this._proxy.$provideFollowups(handle, session.id, token);
},
removeRequest: (session, requestId) => {
return this._proxy.$removeRequest(handle, session.id, requestId);
}
});

Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,7 @@ export interface ExtHostChatShape {
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IChatReplyFollowup[])[] | undefined>;
$provideFollowups(handle: number, sessionId: number, token: CancellationToken): Promise<IChatFollowup[] | undefined>;
$provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined>;
$removeRequest(handle: number, sessionId: number, requestId: string): void;
$provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise<ISlashCommand[] | undefined>;
$releaseSession(sessionId: number): void;
$onDidPerformUserAction(event: IChatUserActionEvent): Promise<void>;
Expand Down
23 changes: 21 additions & 2 deletions src/vs/workbench/api/common/extHostChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/exte
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostChatShape, IChatRequestDto, IChatResponseDto, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import type * as vscode from 'vscode';

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

$removeRequest(handle: number, sessionId: number, requestId: string): void {
const entry = this._chatProvider.get(handle);
if (!entry) {
return;
}

const realSession = this._chatSessions.get(sessionId);
if (!realSession) {
return;
}

if (!entry.provider.removeRequest) {
return;
}

entry.provider.removeRequest(realSession, requestId);
}

async $provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
Expand Down Expand Up @@ -186,7 +204,8 @@ export class ExtHostChat implements ExtHostChatShape {
firstProgress = stopWatch.elapsed();
}

this._proxy.$acceptResponseProgress(handle, sessionId, progress);
const vscodeProgress: IChatProgress = 'responseId' in progress ? { requestId: progress.responseId } : progress;
this._proxy.$acceptResponseProgress(handle, sessionId, vscodeProgress);
}
};
let result: vscode.InteractiveResponseForProgress | undefined | null;
Expand Down
53 changes: 45 additions & 8 deletions src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { localize } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatService, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
Expand All @@ -33,9 +34,10 @@ export function registerChatTitleActions() {
icon: Codicon.thumbsup,
toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('up'),
menu: {
id: MenuId.ChatTitle,
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 1
order: 1,
when: CONTEXT_RESPONSE
}
});
}
Expand Down Expand Up @@ -72,9 +74,10 @@ export function registerChatTitleActions() {
icon: Codicon.thumbsdown,
toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('down'),
menu: {
id: MenuId.ChatTitle,
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 2
order: 2,
when: CONTEXT_RESPONSE
}
});
}
Expand Down Expand Up @@ -110,10 +113,10 @@ export function registerChatTitleActions() {
category: CHAT_CATEGORY,
icon: Codicon.insert,
menu: {
id: MenuId.ChatTitle,
id: MenuId.ChatMessageTitle,
group: 'navigation',
isHiddenByDefault: true,
when: NOTEBOOK_IS_ACTIVE_EDITOR
when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CONTEXT_RESPONSE)
}
});
}
Expand Down Expand Up @@ -172,6 +175,40 @@ export function registerChatTitleActions() {
}
}
});


registerAction2(class RemoveAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.remove',
title: {
value: localize('chat.remove.label', "Remove Request and Response"),
original: 'Remove Request and Response'
},
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.x,
menu: {
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 2,
when: CONTEXT_REQUEST
}
});
}

run(accessor: ServicesAccessor, ...args: any[]) {
const item = args[0];
if (!isRequestVM(item)) {
return;
}

const chatService = accessor.get(IChatService);
if (item.providerRequestId) {
chatService.removeRequest(item.sessionId, item.providerRequestId);
}
}
});
}

interface MarkdownContent {
Expand Down
8 changes: 5 additions & 3 deletions src/vs/workbench/contrib/chat/browser/chatListRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/a
import { IChatCodeBlockInfo } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
import { CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatReplyFollowup, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestViewModel, 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 @@ -193,12 +193,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch

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

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

CONTEXT_RESPONSE.bindTo(templateData.contextKeyService).set(isResponseVM(element));
CONTEXT_REQUEST.bindTo(templateData.contextKeyService).set(isRequestVM(element));
CONTEXT_RESPONSE_HAS_PROVIDER_ID.bindTo(templateData.contextKeyService).set(isResponseVM(element) && !!element.providerResponseId);
if (isResponseVM(element)) {
CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set(element.vote === InteractiveSessionVoteDirection.Up ? 'up' : element.vote === InteractiveSessionVoteDirection.Down ? 'down' : '');
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/chat/browser/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
color: var(--vscode-badge-foreground) !important;
}

.interactive-item-container:not(.interactive-response:hover) .header .monaco-toolbar,
.interactive-item-container:not(.interactive-response:hover) .header .monaco-toolbar .action-label {
.monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar,
.monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar .action-label {
/* Also apply this rule to the .action-label directly to work around a strange issue- when the
toolbar is hidden without that second rule, tabbing from the list container into a list item doesn't work
and the tab key doesn't do anything. */
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/chat/common/chatContextKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey<boolean>('chat
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.") });
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.") });

export const CONTEXT_RESPONSE = new RawContextKey<boolean>('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") });
export const CONTEXT_REQUEST = new RawContextKey<boolean>('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") });

export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey<boolean>('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") });
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.") });
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.") });
Expand Down
45 changes: 38 additions & 7 deletions src/vs/workbench/contrib/chat/common/chatModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse,

export interface IChatRequestModel {
readonly id: string;
readonly providerRequestId: string | undefined;
readonly username: string;
readonly avatarIconUri?: URI;
readonly session: IChatModel;
Expand Down Expand Up @@ -56,6 +57,10 @@ export class ChatRequestModel implements IChatRequestModel {
return this._id;
}

public get providerRequestId(): string | undefined {
return this._providerRequestId;
}

public get username(): string {
return this.session.requesterUsername;
}
Expand All @@ -66,9 +71,14 @@ export class ChatRequestModel implements IChatRequestModel {

constructor(
public readonly session: ChatModel,
public readonly message: string | IChatReplyFollowup) {
public readonly message: string | IChatReplyFollowup,
private _providerRequestId?: string) {
this._id = 'request_' + ChatRequestModel.nextId++;
}

setProviderRequestId(providerRequestId: string) {
this._providerRequestId = providerRequestId;
}
}

export class ChatResponseModel extends Disposable implements IChatResponseModel {
Expand Down Expand Up @@ -189,7 +199,7 @@ export interface ISerializableChatsData {
}

export interface ISerializableChatRequestData {
providerResponseId: string | undefined;
providerRequestId: string | undefined;
message: string;
response: string | undefined;
responseErrorDetails: IChatResponseErrorDetails | undefined;
Expand Down Expand Up @@ -230,7 +240,7 @@ export function isSerializableSessionData(obj: unknown): obj is ISerializableCha
typeof data.sessionId === 'string';
}

export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent;
export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent;

export interface IChatAddRequestEvent {
kind: 'addRequest';
Expand All @@ -242,6 +252,12 @@ export interface IChatAddResponseEvent {
response: IChatResponseModel;
}

export interface IChatRemoveRequestEvent {
kind: 'removeRequest';
requestId: string;
responseId?: string;
}

export interface IChatInitEvent {
kind: 'initialize';
}
Expand Down Expand Up @@ -360,9 +376,9 @@ export class ChatModel extends Disposable implements IChatModel {
}

return requests.map((raw: ISerializableChatRequestData) => {
const request = new ChatRequestModel(this, raw.message);
const request = new ChatRequestModel(this, raw.message, raw.providerRequestId);
if (raw.response || raw.responseErrorDetails) {
request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups);
request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups);
}
return request;
});
Expand Down Expand Up @@ -433,7 +449,22 @@ export class ChatModel extends Disposable implements IChatModel {
if ('content' in progress) {
request.response.updateContent(progress.content);
} else {
request.response.setProviderResponseId(progress.responseId);
request.setProviderRequestId(progress.requestId);
request.response.setProviderResponseId(progress.requestId);
}
}

removeRequest(requestId: string): void {
const index = this._requests.findIndex(request => request.providerRequestId === requestId);
const request = this._requests[index];
if (!request.providerRequestId) {
return;
}

if (index !== -1) {
this._onDidChange.fire({ kind: 'removeRequest', requestId: request.providerRequestId, responseId: request.response?.providerResponseId });
this._requests.splice(index, 1);
request.response?.dispose();
}
}

Expand Down Expand Up @@ -484,7 +515,7 @@ export class ChatModel extends Disposable implements IChatModel {
}),
requests: this._requests.map((r): ISerializableChatRequestData => {
return {
providerResponseId: r.response?.providerResponseId,
providerRequestId: r.providerRequestId,
message: typeof r.message === 'string' ? r.message : r.message.message,
response: r.response ? r.response.response.value : undefined,
responseErrorDetails: r.response?.errorDetails,
Expand Down
4 changes: 3 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface IChatResponse {
}

export type IChatProgress =
{ content: string } | { responseId: string };
{ content: string } | { requestId: string };

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

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

0 comments on commit 1c6825f

Please sign in to comment.