diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d0b5accb676..599fce994ee 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1724,6 +1724,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, + ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9f3631f5a78..d981bf150d2 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -182,6 +182,15 @@ class ChatAgentResponseStream { _report(dto); return this; }, + confirmation(title, message, data) { + throwIfDone(this.confirmation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data); + const dto = typeConvert.ChatResponseConfirmationPart.from(part); + _report(dto); + return this; + }, push(part) { throwIfDone(this.push); @@ -189,7 +198,8 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseTextEditPart || part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || part instanceof extHostTypes.ChatResponseDetectedParticipantPart || - part instanceof extHostTypes.ChatResponseWarningPart + part instanceof extHostTypes.ChatResponseWarningPart || + part instanceof extHostTypes.ChatResponseConfirmationPart ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6d3beb0e99a..4d7ec217589 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2297,6 +2297,17 @@ export namespace ChatResponseDetectedParticipantPart { } } +export namespace ChatResponseConfirmationPart { + export function from(part: vscode.ChatResponseConfirmationPart): Dto { + return { + kind: 'confirmation', + title: part.title, + message: part.message, + data: part.data + }; + } +} + export namespace ChatResponseFilesPart { export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData { const { value, baseUri } = part; @@ -2452,7 +2463,7 @@ export namespace ChatResponseReferencePart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2516,6 +2527,8 @@ export namespace ChatAgentRequest { enableCommandDetection: request.enableCommandDetection ?? true, variables: request.variables.variables.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), + acceptedConfirmationData: request.acceptedConfirmationData, + rejectedConfirmationData: request.rejectedConfirmationData }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 220035f98f0..b0e4c2ce69e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4337,6 +4337,17 @@ export class ChatResponseDetectedParticipantPart { } } +export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + constructor(title: string, message: string, data: any) { + this.title = title; + this.message = message; + this.data = data; + } +} + export class ChatResponseFileTreePart { value: vscode.ChatResponseFileTree[]; baseUri: vscode.Uri; diff --git a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts new file mode 100644 index 00000000000..462cd0cb6ea --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; + +export interface IChatConfirmationButton { + label: string; + isSecondary?: boolean; + data: any; +} + +export class ChatConfirmationWidget extends Disposable { + private _onDidClick = this._register(new Emitter()); + get onDidClick(): Event { return this._onDidClick.event; } + + private _domNode: HTMLElement; + get domNode(): HTMLElement { + return this._domNode; + } + + constructor( + title: string, + message: string, + buttons: IChatConfirmationButton[], + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + const elements = dom.h('.chat-confirmation-widget@root', [ + dom.h('.chat-confirmation-widget-title@title'), + dom.h('.chat-confirmation-widget-message@message'), + dom.h('.chat-confirmation-buttons-container@buttonsContainer'), + ]); + this._domNode = elements.root; + const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + const renderedTitle = this._register(renderer.render(new MarkdownString(title))); + elements.title.appendChild(renderedTitle.element); + + const renderedMessage = this._register(renderer.render(new MarkdownString(message))); + elements.message.appendChild(renderedMessage.element); + + buttons.forEach(buttonData => { + const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary }); + button.label = buttonData.label; + this._register(button.onDidClick(() => this._onDidClick.fire(buttonData))); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 9019394e226..c7baf4327cb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -33,11 +33,13 @@ import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { Range } from 'vs/editor/common/core/range'; import { TextEdit } from 'vs/editor/common/languages'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -59,6 +61,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatConfirmationWidget'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; @@ -67,18 +70,16 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; const $ = dom.$; @@ -159,6 +160,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + if (isResponseVM(element)) { + const prompt = `${e.label}: "${confirmation.title}"`; + const data: IChatSendRequestOptions = e.isSecondary ? + { rejectedConfirmationData: [e.data] } : + { acceptedConfirmationData: [e.data] }; + data.agentId = element.agent?.id; + this.chatService.sendRequest(element.sessionId, prompt, data); + } + })); + + return { + element: confirmationWidget.domNode, + dispose() { store.dispose(); } + }; + } + private renderTextEdit(element: ChatTreeItem, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen @@ -1617,6 +1647,10 @@ function isTextEditRenderData(item: IChatRenderData): item is IChatTextEditGroup return item && 'kind' in item && item.kind === 'textEditGroup'; } +function isConfirmationRenderData(item: IChatRenderData): item is IChatConfirmation { + return item && 'kind' in item && item.kind === 'confirmation'; +} + function isMarkdownRenderData(item: IChatRenderData): item is IChatResponseMarkdownRenderData { return item && 'renderedWordCount' in item; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 17c0049f1c6..5d543466f41 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -256,10 +256,13 @@ } .interactive-item-container .value .rendered-markdown p { - margin: 0 0 16px 0; line-height: 1.5em; } +.interactive-item-container .value > .rendered-markdown p { + margin: 0 0 16px 0; +} + .interactive-item-container .value .rendered-markdown ul { padding-inline-start: 24px; } @@ -317,7 +320,7 @@ min-height: 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { +.interactive-item-container.interactive-item-compact .value > .rendered-markdown p { margin: 0 0 8px 0; } @@ -703,7 +706,8 @@ gap: 6px; } -.interactive-item-container .chat-command-button .monaco-button { +.interactive-item-container .chat-command-button .monaco-button, +.chat-confirmation-widget .chat-confirmation-buttons-container .monaco-button { text-align: left; width: initial; padding: 4px 8px; @@ -713,3 +717,23 @@ margin-left: 0; margin-top: 1px; } + +.chat-confirmation-widget { + border: 1px solid var(--vscode-chat-requestBorder); + border-radius: 6px; + margin-bottom: 16px; + padding: 10px 16px 12px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title { + font-weight: 600; +} + +.chat-confirmation-widget .chat-confirmation-widget-title p { + margin: 0 0 4px 0; +} + +.chat-confirmation-widget .chat-confirmation-buttons-container { + display: flex; + gap: 8px; +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 609a7c18dbf..277313e6d47 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -122,6 +122,8 @@ export interface IChatAgentRequest { enableCommandDetection?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; } export interface IChatAgentResult { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7a7ffacd78e..3f456e6f2d7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -63,7 +63,8 @@ export type IChatProgressResponseContent = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage - | IChatTextEditGroup; + | IChatTextEditGroup + | IChatConfirmation; export type IChatProgressRenderableResponseContent = Exclude; @@ -207,7 +208,6 @@ export class Response implements IResponse { } this._updateRepr(quiet); } - } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -226,6 +226,8 @@ export class Response implements IResponse { return ''; } else if (part.kind === 'progressMessage') { return ''; + } else if (part.kind === 'confirmation') { + return `${part.title}\n${part.message}`; } else { return part.content.value; } @@ -778,7 +780,16 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || progress.kind === 'textEdit' || progress.kind === 'warning') { + if (progress.kind === 'markdownContent' || + progress.kind === 'treeData' || + progress.kind === 'inlineReference' || + progress.kind === 'markdownVuln' || + progress.kind === 'progressMessage' || + progress.kind === 'command' || + progress.kind === 'textEdit' || + progress.kind === 'warning' || + progress.kind === 'confirmation' + ) { request.response.updateContent(progress, quiet); } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 26a25b167cb..c9a9be9a42e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -133,6 +133,13 @@ export interface IChatTextEdit { kind: 'textEdit'; } +export interface IChatConfirmation { + title: string; + message: string; + data: any; + kind: 'confirmation'; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -144,7 +151,8 @@ export type IChatProgress = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage - | IChatTextEdit; + | IChatTextEdit + | IChatConfirmation; export interface IChatFollowup { kind: 'reply'; @@ -268,6 +276,11 @@ export interface IChatSendRequestOptions { parserContext?: IChatParserContext; attempt?: number; noCommandDetection?: boolean; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; + + /** The target agent ID can be specified with this property instead of using @ in 'message' */ + agentId?: string; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 90f32ecbd10..5f22e62a370 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -116,6 +116,11 @@ type ChatTerminalClassification = { comment: 'Provides insight into the usage of Chat features.'; }; +interface IRequestConfirmationData { + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; +} + const maxPersistedSessions = 25; export class ChatService extends Disposable implements IChatService { @@ -460,18 +465,33 @@ export class ChatService extends Disposable implements IChatService { const implicitVariablesEnabled = options?.implicitVariablesEnabled ?? false; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, options?.parserContext); + const parsedRequest = this.parseChatRequest(sessionId, request, location, options); const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); // This method is only returning whether the request was accepted - don't block on the actual request return { - responseCompletePromise: this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location), + responseCompletePromise: this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location, options), agent, slashCommand: agentSlashCommandPart?.command, }; } + private parseChatRequest(sessionId: string, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { + let parserContext = options?.parserContext; + if (options?.agentId) { + const agent = this.chatAgentService.getAgent(options.agentId); + if (!agent) { + throw new Error(`Unknown agent: ${options.agentId}`); + } + parserContext = { selectedAgent: agent }; + request = `${chatAgentLeader}${agent.name} ${request}`; + } + + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); + return parsedRequest; + } + private refreshFollowupsCancellationToken(sessionId: string): CancellationToken { this._sessionFollowupCancelTokens.get(sessionId)?.cancel(); const newTokenSource = new CancellationTokenSource(); @@ -480,7 +500,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private async _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation): Promise { + private async _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, confirmData?: IRequestConfirmationData): Promise { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -563,7 +583,8 @@ export class ChatService extends Disposable implements IChatService { variables: updatedVariableData, enableCommandDetection, attempt, - location + location, + ...confirmData }; const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 8a39c4d2e63..429d32cbc2c 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -14,7 +14,7 @@ import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/ import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -95,7 +95,7 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup; +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f8914d33b94..6ff2a8a71cf 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -102,6 +102,15 @@ declare module 'vscode' { constructor(uri: Uri, edits: TextEdit | TextEdit[]); } + export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + constructor(title: string, message: string, data: any); + } + + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart; + export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -111,7 +120,19 @@ declare module 'vscode' { textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; detectedParticipant(participant: string, command?: ChatCommand): ChatResponseStream; - push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart): ChatResponseStream; + + /** + * Show an inline message in the chat view asking the user to confirm an action. + * Multiple confirmations may be shown per response. The UI might show "Accept All" / "Reject All" actions. + * @param title The title of the confirmation entry + * @param message An extra message to display to the user + * @param data An arbitrary JSON-stringifiable object that will be included in the ChatRequest when + * the confirmation is accepted or rejected + * TODO@API should this be MarkdownString? + * TODO@API should actually be a more generic function that takes an array of buttons + */ + confirmation(title: string, message: string, data: any): ChatResponseStream; + /** * Push a warning to this stream. Short-hand for * `push(new ChatResponseWarningPart(message))`. @@ -121,6 +142,23 @@ declare module 'vscode' { */ warning(message: string | MarkdownString): ChatResponseStream; + push(part: ExtendedChatResponsePart): ChatResponseStream; + } + + /** + * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? + * Does it show up in history? + */ + export interface ChatRequest { + /** + * The `data` for any confirmations that were accepted + */ + acceptedConfirmationData?: any[]; + + /** + * The `data` for any confirmations that were rejected + */ + rejectedConfirmationData?: any[]; } // TODO@API fit this into the stream