diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 9a48e8ea78c..407239a8d86 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -62,8 +62,8 @@ import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/brows import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; +import { convertParsedRequestToMarkdown, walkTreeAndAnnotateResourceLinks } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { fixVariableReferences, walkTreeAndAnnotateResourceLinks } from 'vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer'; import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IPlaceholderMarkdownString } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; @@ -314,7 +314,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts similarity index 60% rename from src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts rename to src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 1a7ad12b7b8..e6f7b341ae7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -4,27 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { IParsedChatRequest, ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -const variableRefUrlPrefix = 'http://vscodeVar_'; +const variableRefUrl = 'http://_vscodeDecoration_'; -export function fixVariableReferences(markdown: IMarkdownString): IMarkdownString { - const fixedMarkdownSource = markdown.value.replace(/\]\(values:(.*)/g, `](${variableRefUrlPrefix}_$1`); - return new MarkdownString(fixedMarkdownSource, { isTrusted: markdown.isTrusted, supportThemeIcons: markdown.supportThemeIcons, supportHtml: markdown.supportHtml }); +export function convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { + let result = ''; + for (const part of parsedRequest.parts) { + if (part instanceof ChatRequestTextPart) { + result += part.text; + } else { + result += `[${part.text}](${variableRefUrl})`; + } + } + + return result; } export function walkTreeAndAnnotateResourceLinks(element: HTMLElement): void { element.querySelectorAll('a').forEach(a => { const href = a.getAttribute('data-href'); if (href) { - if (href.startsWith(variableRefUrlPrefix)) { + if (href.startsWith(variableRefUrl)) { a.parentElement!.replaceChild( renderResourceWidget(a.textContent!), a); } } - - walkTreeAndAnnotateResourceLinks(a as HTMLElement); }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index 5c41a1a6f7e..679b4ac99dc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -19,6 +19,7 @@ import { IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export class QuickChatService extends Disposable implements IQuickChatService { @@ -271,7 +272,7 @@ class QuickChat extends Disposable { for (const request of this.model.getRequests()) { if (request.response?.response.value || request.response?.errorDetails) { this.chatService.addCompleteRequest(widget.viewModel.sessionId, - request.message as string, + request.message as IParsedChatRequest, { message: request.response.response.value, errorDetails: request.response.errorDetails, diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 00e3979d1ff..6b1b18d87e8 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -26,7 +26,7 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart } from '../../common/chatRequestParser'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -127,7 +127,7 @@ class InputEditorDecorations extends Disposable { return; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(viewModel.sessionId, inputValue); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(viewModel.sessionId, inputValue)).parts; let placeholderDecoration: IDecorationOptions[] | undefined; const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); @@ -252,7 +252,7 @@ class SlashCommandCompletions extends Disposable { return null; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts; const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); if (usedAgent) { // No (classic) global slash commands when an agent is used @@ -303,7 +303,7 @@ class AgentCompletions extends Disposable { return null; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts; const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { // Only one agent allowed @@ -340,7 +340,7 @@ class AgentCompletions extends Disposable { return; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts; const usedAgent = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); if (!usedAgent) { return; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 26c82c87dd2..7cf08b86d5a 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,7 +10,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; -import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatRequestModel { @@ -19,7 +20,7 @@ export interface IChatRequestModel { readonly username: string; readonly avatarIconUri?: URI; readonly session: IChatModel; - readonly message: string | IChatReplyFollowup; + readonly message: IParsedChatRequest | IChatReplyFollowup; readonly response: IChatResponseModel | undefined; } @@ -79,7 +80,7 @@ export class ChatRequestModel implements IChatRequestModel { constructor( public readonly session: ChatModel, - public readonly message: string | IChatReplyFollowup, + public readonly message: IParsedChatRequest | IChatReplyFollowup, private _providerRequestId?: string) { this._id = 'request_' + ChatRequestModel.nextId++; } @@ -457,8 +458,9 @@ export class ChatModel extends Disposable implements IChatModel { } get title(): string { - const firstRequestMessage = this._requests[0]?.message; - const message = typeof firstRequestMessage === 'string' ? firstRequestMessage : firstRequestMessage?.message ?? ''; + // const firstRequestMessage = this._requests[0]?.message; + // const message = typeof firstRequestMessage === 'string' ? firstRequestMessage : firstRequestMessage?.message ?? ''; + const message = ''; return message.split('\n')[0].substring(0, 50); } @@ -466,7 +468,6 @@ export class ChatModel extends Disposable implements IChatModel { public readonly providerId: string, private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @ILogService private readonly logService: ILogService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -492,14 +493,15 @@ export class ChatModel extends Disposable implements IChatModel { this._welcomeMessage = new ChatWelcomeMessageModel(this, content); } - return requests.map((raw: ISerializableChatRequestData) => { - const request = new ChatRequestModel(this, raw.message, raw.providerRequestId); - if (raw.response || raw.responseErrorDetails) { - const agent = raw.agent && this.chatAgentService.getAgents().find(a => a.id === raw.agent!.id); // TODO do something reasonable if this agent has disappeared since the last session - request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); - } - return request; - }); + return []; + // return requests.map((raw: ISerializableChatRequestData) => { + // const request = new ChatRequestModel(this, raw.message, raw.providerRequestId); + // if (raw.response || raw.responseErrorDetails) { + // const agent = raw.agent && this.chatAgentService.getAgents().find(a => a.id === raw.agent!.id); // TODO do something reasonable if this agent has disappeared since the last session + // request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); + // } + // return request; + // }); } startReinitialize(): void { @@ -543,7 +545,7 @@ export class ChatModel extends Disposable implements IChatModel { return this._requests; } - addRequest(message: string | IChatReplyFollowup, chatAgent?: IChatAgentData): ChatRequestModel { + addRequest(message: IParsedChatRequest | IChatReplyFollowup, chatAgent?: IChatAgentData): ChatRequestModel { if (!this._session) { throw new Error('addRequest: No session'); } @@ -649,7 +651,7 @@ export class ChatModel extends Disposable implements IChatModel { requests: this._requests.map((r): ISerializableChatRequestData => { return { providerRequestId: r.providerRequestId, - message: typeof r.message === 'string' ? r.message : r.message.message, + message: typeof r.message === 'string' ? r.message : '', response: r.response ? r.response.response.value : undefined, responseErrorDetails: r.response?.errorDetails, followups: r.response?.followups, diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts new file mode 100644 index 00000000000..14dbe0e24b6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IRange } from 'vs/editor/common/core/range'; +import { IChatAgentData, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; + +// These are in a separate file to avoid circular dependencies with the dependencies of the parser + +export interface IParsedChatRequest { + readonly parts: ReadonlyArray; + readonly text: string; +} + +export interface IParsedChatRequestPart { + readonly range: OffsetRange; + readonly editorRange: IRange; + readonly text: string; +} + +// TODO rename to tokens + +export class ChatRequestTextPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string) { } +} + +/** + * An invocation of a static variable that can be resolved by the variable service + */ +export class ChatRequestVariablePart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } + + get text(): string { + const argPart = this.variableArg ? `:${this.variableArg}` : ''; + return `@${this.variableName}${argPart}`; + } +} + +/** + * An invocation of an agent that can be resolved by the agent service + */ +export class ChatRequestAgentPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } + + get text(): string { + return `@${this.agent.id}`; + } +} + +/** + * An invocation of an agent's subcommand + */ +export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { } + + get text(): string { + return `/${this.command.name}`; + } +} + +/** + * An invocation of a standalone slash command + */ +export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: ISlashCommand) { } + + get text(): string { + return `/${this.slashCommand.command}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index b6ff47c6680..03bb2eb11f1 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -6,13 +6,14 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IPosition, Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { Range } from 'vs/editor/common/core/range'; +import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -const variableOrAgentReg = /^@([\w_\-]+)(:\d+)?(?=(\s|$))/i; // An @-variable with an optional numeric : arg (@response:2) -const slashReg = /\/([\w_-]+)(?=(\s|$))/i; // A / command +const variableOrAgentReg = /^@([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // An @-variable with an optional numeric : arg (@response:2) +const slashReg = /\/([\w_-]+)(?=(\s|$|\b))/i; // A / command export class ChatRequestParser { constructor( @@ -21,7 +22,7 @@ export class ChatRequestParser { @IChatService private readonly chatService: IChatService, ) { } - async parseChatRequest(sessionId: string, message: string): Promise { + async parseChatRequest(sessionId: string, message: string): Promise { const parts: IParsedChatRequestPart[] = []; let lineNumber = 1; @@ -68,7 +69,10 @@ export class ChatRequestParser { new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column), message.slice(lastPartEnd, message.length))); - return parts; + return { + parts, + text: message, + }; } private tryToParseVariableOrAgent(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { @@ -131,40 +135,3 @@ export class ChatRequestParser { return; } } - -export interface IParsedChatRequestPart { - readonly range: OffsetRange; - readonly editorRange: IRange; -} - -export class ChatRequestTextPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string) { } -} -/** - * An invocation of a static variable that can be resolved by the variable service - */ - -export class ChatRequestVariablePart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } -} -/** - * An invocation of an agent that can be resolved by the agent service - */ - -export class ChatRequestAgentPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } -} -/** - * An invocation of an agent's subcommand - */ - -export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { } -} -/** - * An invocation of a standalone slash command - */ - -export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: ISlashCommand) { } -} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index b97c1a7c995..a3f2d90a1b2 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChat { @@ -227,7 +228,7 @@ export interface IChatService { getSlashCommands(sessionId: string, token: CancellationToken): Promise; clearSession(sessionId: string): void; addRequest(context: any): void; - addCompleteRequest(sessionId: string, message: string, response: IChatCompleteResponse): void; + addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, response: IChatCompleteResponse): void; sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void; getHistory(): IChatDetail[]; removeHistoryEntry(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 75bed122f24..14d5f4022c5 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -21,10 +21,12 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData, isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart, ChatRequestSlashCommandPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatRequest, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -431,19 +433,21 @@ export class ChatService extends Disposable implements IChatService { } // This method is only returning whether the request was accepted - don't block on the actual request - return { responseCompletePromise: this._sendRequestAsync(model, provider, request, usedSlashCommand) }; + return { responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, request, usedSlashCommand) }; } - private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { - const resolvedAgent = typeof message === 'string' ? this.resolveAgent(message) : undefined; + private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { + const parsedRequest = typeof message === 'string' ? + await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : + message; // Handle the followup type along with the response + let request: ChatRequestModel; - - const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; - + const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); let gotProgress = false; const requestType = typeof message === 'string' ? - (message.startsWith('/') ? 'slashCommand' : 'string') : + commandPart ? 'slashCommand' : 'string' : 'followup'; const rawResponsePromise = createCancelablePromise(async token => { @@ -491,8 +495,8 @@ export class ChatService extends Disposable implements IChatService { let rawResponse: IChatResponse | null | undefined; let slashCommandFollowups: IChatFollowup[] | void = []; - if (typeof message === 'string' && resolvedAgent) { - request = model.addRequest(message); + if (typeof message === 'string' && agentPart) { + request = model.addRequest(parsedRequest); const history: IChatMessage[] = []; for (const request of model.getRequests()) { if (typeof request.message !== 'string' || !request.response) { @@ -503,15 +507,15 @@ export class ChatService extends Disposable implements IChatService { history.push({ role: ChatMessageRole.Assistant, content: request.response.response.value.value }); } } - const agentResult = await this.chatAgentService.invokeAgent(resolvedAgent.id, message.substring(resolvedAgent.id.length + 1).trimStart(), new Progress(p => { + const agentResult = await this.chatAgentService.invokeAgent(agentPart.agent.id, message.substring(agentPart.agent.id.length + 1).trimStart(), new Progress(p => { const { content } = p; const data = isCompleteInteractiveProgressTreeData(content) ? content : { content }; progressCallback(data); }), history, token); slashCommandFollowups = agentResult?.followUp; rawResponse = { session: model.session! }; - } else if ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) { - request = model.addRequest(message); + } else if (commandPart && typeof message === 'string' && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { + request = model.addRequest(parsedRequest); // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; @@ -524,7 +528,7 @@ export class ChatService extends Disposable implements IChatService { history.push({ role: ChatMessageRole.Assistant, content: request.response.response.value.value }); } } - const commandResult = await this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => { + const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { const { content } = p; const data = isCompleteInteractiveProgressTreeData(content) ? content : { content }; progressCallback(data); @@ -533,19 +537,18 @@ export class ChatService extends Disposable implements IChatService { rawResponse = { session: model.session! }; } else { + request = model.addRequest(parsedRequest); const requestProps: IChatRequest = { session: model.session!, - message: resolvedCommand, + message, variables: {} }; - if (typeof requestProps.message === 'string') { - const varResult = await this.chatVariablesService.resolveVariables(requestProps.message, model, token); + if ('parts' in parsedRequest) { + const varResult = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); requestProps.variables = varResult.variables; requestProps.message = varResult.prompt; } - request = model.addRequest(requestProps.message); - rawResponse = await provider.provideReply(requestProps, progressCallback, token); } @@ -613,26 +616,6 @@ export class ChatService extends Disposable implements IChatService { provider.removeRequest?.(model.session!, requestId); } - private async handleSlashCommand(sessionId: string, command: string): Promise { - const slashCommands = await this.getSlashCommands(sessionId, CancellationToken.None); - for (const slashCommand of slashCommands ?? []) { - if (command.startsWith(`/${slashCommand.command}`) && this.chatSlashCommandService.hasCommand(slashCommand.command)) { - return slashCommand.command; - } - } - return command; - } - - private resolveAgent(prompt: string): IChatAgentData | undefined { - prompt = prompt.trim(); - const agents = this.chatAgentService.getAgents(); - if (!prompt.startsWith('@')) { - return; - } - - return agents.find(a => prompt.match(new RegExp(`@${a.id}($|\\s)`))); - } - async getSlashCommands(sessionId: string, token: CancellationToken): Promise { const model = this._sessionModels.get(sessionId); if (!model) { @@ -707,7 +690,7 @@ export class ChatService extends Disposable implements IChatService { return Array.from(this._providers.keys()); } - async addCompleteRequest(sessionId: string, message: string, response: IChatCompleteResponse): Promise { + async addCompleteRequest(sessionId: string, message: string | IParsedChatRequest, response: IChatCompleteResponse): Promise { this.trace('addCompleteRequest', `message: ${message}`); const model = this._sessionModels.get(sessionId); @@ -716,7 +699,8 @@ export class ChatService extends Disposable implements IChatService { } await model.waitForInitialization(); - const request = model.addRequest(message, undefined); + const parsedRequest = typeof message === 'string' ? await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : message; + const request = model.addRequest(parsedRequest); if (typeof response.message === 'string') { model.acceptResponseProgress(request, { content: response.message }); } else { diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 0fb80a2b0ef..baadbbed310 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -9,6 +9,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; export interface IChatVariableData { name: string; @@ -39,7 +40,7 @@ export interface IChatVariablesService { /** * Resolves all variables that occur in `prompt` */ - resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise; + resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise; } interface IChatData { @@ -60,40 +61,29 @@ export class ChatVariablesService implements IChatVariablesService { constructor() { } - async resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise { const resolvedVariables: Record = {}; const jobs: Promise[] = []; - // TODO have a separate parser that is also used for decorations - const regex = /(^|\s)@(\w+)(:\w+)?(?=\s|$|\b)/ig; - - let lastMatch = 0; const parsedPrompt: string[] = []; - let match: RegExpMatchArray | null; - while (match = regex.exec(prompt)) { - const [fullMatch, leading, varName, arg] = match; - const data = this._resolver.get(varName.toLowerCase()); - if (data) { - if (!arg || data.data.canTakeArgument) { - parsedPrompt.push(prompt.substring(lastMatch, match.index!)); - parsedPrompt.push(''); - lastMatch = match.index! + fullMatch.length; - const varIndex = parsedPrompt.length - 1; - const argWithoutColon = arg?.slice(1); - const fullVarName = varName + (arg ?? ''); - jobs.push(data.resolver(prompt, argWithoutColon, model, token).then(value => { - if (value) { - resolvedVariables[fullVarName] = value; - parsedPrompt[varIndex] = `${leading}[@${fullVarName}](values:${fullVarName})`; - } else { - parsedPrompt[varIndex] = fullMatch; - } - }).catch(onUnexpectedExternalError)); + prompt.parts + .forEach((varPart, i) => { + if (varPart instanceof ChatRequestVariablePart) { + const data = this._resolver.get(varPart.variableName.toLowerCase()); + if (data) { + jobs.push(data.resolver(prompt.text, varPart.variableArg, model, token).then(value => { + if (value) { + resolvedVariables[varPart.variableName] = value; + parsedPrompt[i] = `[@${varPart.variableName}](values:${varPart.variableName})`; + } else { + parsedPrompt[i] = varPart.text; + } + }).catch(onUnexpectedExternalError)); + } + } else { + parsedPrompt[i] = varPart.text; } - } - } - - parsedPrompt.push(prompt.substring(lastMatch)); + }); await Promise.allSettled(jobs); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 7e891ea7be6..2238e4d949f 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -11,6 +11,7 @@ import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatReplyFollowup, IChatResponseCommandFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -51,7 +52,7 @@ export interface IChatRequestViewModel { readonly dataId: string; readonly username: string; readonly avatarIconUri?: URI; - readonly message: string | IChatReplyFollowup; + readonly message: IParsedChatRequest | IChatReplyFollowup; readonly messageText: string; currentRenderedHeight: number | undefined; } @@ -215,7 +216,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { } get messageText() { - return typeof this.message === 'string' ? this.message : this.message.message; + return 'kind' in this.message ? this.message.message : this.message.text; } currentRenderedHeight: number | undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline.0.snap deleted file mode 100644 index ca127ef833b..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline.0.snap +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - range: { - start: 0, - endExclusive: 6 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 7 - }, - agent: { - id: "agent", - metadata: { - description: "", - subCommands: [ { name: "subCommand" } ] - } - } - }, - { - range: { - start: 6, - endExclusive: 18 - }, - editorRange: { - startLineNumber: 1, - startColumn: 7, - endLineNumber: 2, - endColumn: 4 - }, - text: " Please \ndo " - }, - { - range: { - start: 18, - endExclusive: 29 - }, - editorRange: { - startLineNumber: 2, - startColumn: 4, - endLineNumber: 2, - endColumn: 15 - }, - command: { name: "subCommand" } - }, - { - range: { - start: 29, - endExclusive: 63 - }, - editorRange: { - startLineNumber: 2, - startColumn: 15, - endLineNumber: 3, - endColumn: 18 - }, - text: " with @selection\nand @debugConsole" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines.0.snap deleted file mode 100644 index 31b7d3be458..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines.0.snap +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - range: { - start: 0, - endExclusive: 21 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 3, - endColumn: 7 - }, - text: "line 1\nline 2\r\nline 3" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap index b7b48f33be9..3a65b0a6ea4 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap @@ -1,73 +1,76 @@ -[ - { - range: { - start: 0, - endExclusive: 10 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "Hello Mr. " }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 11 - }, - text: "Hello Mr. " - }, - { - range: { - start: 10, - endExclusive: 16 - }, - editorRange: { - startLineNumber: 1, - startColumn: 11, - endLineNumber: 1, - endColumn: 17 - }, - agent: { - id: "agent", - metadata: { - description: "", - subCommands: [ { name: "subCommand" } ] + { + range: { + start: 10, + endExclusive: 16 + }, + editorRange: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 17 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } } + }, + { + range: { + start: 16, + endExclusive: 17 + }, + editorRange: { + startLineNumber: 1, + startColumn: 17, + endLineNumber: 1, + endColumn: 18 + }, + text: " " + }, + { + range: { + start: 17, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 18, + endLineNumber: 1, + endColumn: 29 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 28, + endExclusive: 35 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 36 + }, + text: " thanks" } - }, - { - range: { - start: 16, - endExclusive: 17 - }, - editorRange: { - startLineNumber: 1, - startColumn: 17, - endLineNumber: 1, - endColumn: 18 - }, - text: " " - }, - { - range: { - start: 17, - endExclusive: 28 - }, - editorRange: { - startLineNumber: 1, - startColumn: 18, - endLineNumber: 1, - endColumn: 29 - }, - command: { name: "subCommand" } - }, - { - range: { - start: 28, - endExclusive: 35 - }, - editorRange: { - startLineNumber: 1, - startColumn: 29, - endLineNumber: 1, - endColumn: 36 - }, - text: " thanks" - } -] \ No newline at end of file + ], + text: "Hello Mr. @agent /subCommand thanks" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap new file mode 100644 index 00000000000..ce4e5ebadba --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -0,0 +1,50 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 14 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 15 + }, + text: "Are you there " + }, + { + range: { + start: 14, + endExclusive: 20 + }, + editorRange: { + startLineNumber: 1, + startColumn: 15, + endLineNumber: 1, + endColumn: 21 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 20, + endExclusive: 21 + }, + editorRange: { + startLineNumber: 1, + startColumn: 21, + endLineNumber: 1, + endColumn: 22 + }, + text: "?" + } + ], + text: "Are you there @agent?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap index 85afe6b0ae1..9b5a1010a4c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap @@ -1,60 +1,63 @@ -[ - { - range: { - start: 0, - endExclusive: 6 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 7 - }, - agent: { - id: "agent", - metadata: { - description: "", - subCommands: [ { name: "subCommand" } ] +{ + parts: [ + { + range: { + start: 0, + endExclusive: 6 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 7 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } } + }, + { + range: { + start: 6, + endExclusive: 17 + }, + editorRange: { + startLineNumber: 1, + startColumn: 7, + endLineNumber: 1, + endColumn: 18 + }, + text: " Please do " + }, + { + range: { + start: 17, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 18, + endLineNumber: 1, + endColumn: 29 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 28, + endExclusive: 35 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 36 + }, + text: " thanks" } - }, - { - range: { - start: 6, - endExclusive: 17 - }, - editorRange: { - startLineNumber: 1, - startColumn: 7, - endLineNumber: 1, - endColumn: 18 - }, - text: " Please do " - }, - { - range: { - start: 17, - endExclusive: 28 - }, - editorRange: { - startLineNumber: 1, - startColumn: 18, - endLineNumber: 1, - endColumn: 29 - }, - command: { name: "subCommand" } - }, - { - range: { - start: 28, - endExclusive: 35 - }, - editorRange: { - startLineNumber: 1, - startColumn: 29, - endLineNumber: 1, - endColumn: 36 - }, - text: " thanks" - } -] \ No newline at end of file + ], + text: "@agent Please do /subCommand thanks" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap new file mode 100644 index 00000000000..d74138f7fb4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -0,0 +1,63 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 6 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 7 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 6, + endExclusive: 18 + }, + editorRange: { + startLineNumber: 1, + startColumn: 7, + endLineNumber: 2, + endColumn: 4 + }, + text: " Please \ndo " + }, + { + range: { + start: 18, + endExclusive: 29 + }, + editorRange: { + startLineNumber: 2, + startColumn: 4, + endLineNumber: 2, + endColumn: 15 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 29, + endExclusive: 63 + }, + editorRange: { + startLineNumber: 2, + startColumn: 15, + endLineNumber: 3, + endColumn: 18 + }, + text: " with @selection\nand @debugConsole" + } + ], + text: "@agent Please \ndo /subCommand with @selection\nand @debugConsole" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap index e1389633f29..86e07d1cb2a 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap @@ -1,15 +1,18 @@ -[ - { - range: { - start: 0, - endExclusive: 13 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 14 - }, - text: "/explain this" - } -] \ No newline at end of file +{ + parts: [ + { + range: { + start: 0, + endExclusive: 13 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 14 + }, + text: "/explain this" + } + ], + text: "/explain this" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap index d3fa8a51d2d..854189c56e9 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap @@ -1,15 +1,18 @@ -[ - { - range: { - start: 0, - endExclusive: 26 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 27 - }, - text: "What does @selection mean?" - } -] \ No newline at end of file +{ + parts: [ + { + range: { + start: 0, + endExclusive: 26 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 27 + }, + text: "What does @selection mean?" + } + ], + text: "What does @selection mean?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap index 3e1f3c0147e..9babacf74e1 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap @@ -1,28 +1,31 @@ -[ - { - range: { - start: 0, - endExclusive: 4 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + slashCommand: { command: "fix" } }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 5 - }, - slashCommand: { command: "fix" } - }, - { - range: { - start: 4, - endExclusive: 9 - }, - editorRange: { - startLineNumber: 1, - startColumn: 5, - endLineNumber: 1, - endColumn: 10 - }, - text: " /fix" - } -] \ No newline at end of file + { + range: { + start: 4, + endExclusive: 9 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 10 + }, + text: " /fix" + } + ], + text: "/fix /fix" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap index d032da60253..e5e1fac6b73 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap @@ -1,15 +1,18 @@ -[ - { - range: { - start: 0, - endExclusive: 4 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 5 - }, - text: "test" - } -] \ No newline at end of file +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: "test" + } + ], + text: "test" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap new file mode 100644 index 00000000000..7f0c88fb724 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap @@ -0,0 +1,18 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 21 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 3, + endColumn: 7 + }, + text: "line 1\nline 2\r\nline 3" + } + ], + text: "line 1\nline 2\r\nline 3" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap index a2dadb07783..75e6df87612 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap @@ -1,28 +1,31 @@ -[ - { - range: { - start: 0, - endExclusive: 4 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + slashCommand: { command: "fix" } }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 5 - }, - slashCommand: { command: "fix" } - }, - { - range: { - start: 4, - endExclusive: 9 - }, - editorRange: { - startLineNumber: 1, - startColumn: 5, - endLineNumber: 1, - endColumn: 10 - }, - text: " this" - } -] \ No newline at end of file + { + range: { + start: 4, + endExclusive: 9 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 10 + }, + text: " this" + } + ], + text: "/fix this" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap new file mode 100644 index 00000000000..a6b846cf943 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap @@ -0,0 +1,45 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 8 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 9 + }, + text: "What is " + }, + { + range: { + start: 8, + endExclusive: 18 + }, + editorRange: { + startLineNumber: 1, + startColumn: 9, + endLineNumber: 1, + endColumn: 19 + }, + variableName: "selection", + variableArg: "" + }, + { + range: { + start: 18, + endExclusive: 19 + }, + editorRange: { + startLineNumber: 1, + startColumn: 19, + endLineNumber: 1, + endColumn: 20 + }, + text: "?" + } + ], + text: "What is @selection?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap index 75fe064e9aa..721dbc46117 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap @@ -1,42 +1,45 @@ -[ - { - range: { - start: 0, - endExclusive: 10 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "What does " }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 11 + { + range: { + start: 10, + endExclusive: 20 + }, + editorRange: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 21 + }, + variableName: "selection", + variableArg: "" }, - text: "What does " - }, - { - range: { - start: 10, - endExclusive: 20 - }, - editorRange: { - startLineNumber: 1, - startColumn: 11, - endLineNumber: 1, - endColumn: 21 - }, - variableName: "selection", - variableArg: "" - }, - { - range: { - start: 20, - endExclusive: 26 - }, - editorRange: { - startLineNumber: 1, - startColumn: 21, - endLineNumber: 1, - endColumn: 27 - }, - text: " mean?" - } -] \ No newline at end of file + { + range: { + start: 20, + endExclusive: 26 + }, + editorRange: { + startLineNumber: 1, + startColumn: 21, + endLineNumber: 1, + endColumn: 27 + }, + text: " mean?" + } + ], + text: "What does @selection mean?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index 7d207a080e5..221d59a4de4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -36,7 +36,7 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); - test('_plain text with newlines', async () => { + test('plain text with newlines', async () => { parser = instantiationService.createInstance(ChatRequestParser); const text = 'line 1\nline 2\r\nline 3'; const result = await parser.parseChatRequest('1', text); @@ -87,6 +87,17 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('variable with question mark', async () => { + const variablesService = mockObject()({}); + variablesService.hasVariable.returns(true); + instantiationService.stub(IChatVariablesService, variablesService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'What is @selection?'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + test('invalid variables', async () => { const variablesService = mockObject()({}); variablesService.hasVariable.returns(false); @@ -108,6 +119,16 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('agent with question mark', async () => { + const agentsService = mockObject()({}); + agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); + instantiationService.stub(IChatAgentService, agentsService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const result = await parser.parseChatRequest('1', 'Are you there @agent?'); + await assertSnapshot(result); + }); + test('agent not first', async () => { const agentsService = mockObject()({}); agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); @@ -118,7 +139,7 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); - test('_agents and variables and multiline', async () => { + test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); instantiationService.stub(IChatAgentService, agentsService as any); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index b3ccd1a70a6..9cf2a92c988 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -94,11 +94,11 @@ suite('Chat', () => { const session1 = testDisposables.add(testService.startSession('provider1', CancellationToken.None)); await session1.waitForInitialization(); - session1!.addRequest('request 1'); + session1!.addRequest({ parts: [], text: 'request 1' }); const session2 = testDisposables.add(testService.startSession('provider2', CancellationToken.None)); await session2.waitForInitialization(); - session2!.addRequest('request 2'); + session2!.addRequest({ parts: [], text: 'request 2' }); assert.strictEqual(provider1.lastInitialState, undefined); assert.strictEqual(provider2.lastInitialState, undefined); diff --git a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts index f67df48598e..dae6a364994 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts @@ -6,58 +6,78 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; +import { ChatVariablesService, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ChatVariables', function () { let service: ChatVariablesService; + let instantiationService: TestInstantiationService; + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(function () { service = new ChatVariablesService(); + instantiationService = testDisposables.add(new TestInstantiationService()); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IChatVariablesService, service); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); }); - ensureNoDisposablesAreLeakedInTestSuite(); - test('ChatVariables - resolveVariables', async function () { const v1 = service.registerVariable({ name: 'foo', description: 'bar' }, async () => ([{ level: 'full', value: 'farboo' }])); const v2 = service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }])); + const parser = instantiationService.createInstance(ChatRequestParser); + + const resolveVariables = async (text: string) => { + const result = await parser.parseChatRequest('1', text); + return await service.resolveVariables(result, null!, CancellationToken.None); + }; + { - const data = await service.resolveVariables('Hello @foo and@far', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and@far'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo) and@far'); } { - const data = await service.resolveVariables('@foo Hello', null!, CancellationToken.None); + const data = await resolveVariables('@foo Hello'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); assert.strictEqual(data.prompt, '[@foo](values:foo) Hello'); } { - const data = await service.resolveVariables('Hello @foo', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); } { - const data = await service.resolveVariables('Hello @foo?', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo?'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo)?'); } { - const data = await service.resolveVariables('Hello @foo and@far @foo', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and@far @foo'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); } { - const data = await service.resolveVariables('Hello @foo and @far @foo', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and @far @foo'); assert.strictEqual(Object.keys(data.variables).length, 2); assert.deepEqual(Object.keys(data.variables).sort(), ['far', 'foo']); } { - const data = await service.resolveVariables('Hello @foo and @far @foo @unknown', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and @far @foo @unknown'); assert.strictEqual(Object.keys(data.variables).length, 2); assert.deepEqual(Object.keys(data.variables).sort(), ['far', 'foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo) and [@far](values:far) [@foo](values:foo) @unknown');