Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement "remove request/response" for Chat #183380

Merged
merged 1 commit into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -192,12 +192,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 @@ -217,6 +217,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