mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-20 02:08:47 +00:00
Add decorations for variables in rendered chat requests (#193745)
* Use parsed chat request in model and renderer * Fix using variables with the chat parser * Test snapshot * Add test * Update test snapshots
This commit is contained in:
@@ -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<Ch
|
||||
} else if (isResponseVM(element)) {
|
||||
this.basicRenderElement(element.response.value, element, index, templateData);
|
||||
} else if (isRequestVM(element)) {
|
||||
this.basicRenderElement([new MarkdownString(element.messageText)], element, index, templateData);
|
||||
const markdown = 'kind' in element.message ?
|
||||
element.message.message :
|
||||
convertParsedRequestToMarkdown(element.message);
|
||||
this.basicRenderElement([new MarkdownString(markdown)], element, index, templateData);
|
||||
} else {
|
||||
this.renderWelcomeMessage(element, templateData);
|
||||
}
|
||||
@@ -608,11 +611,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
});
|
||||
|
||||
const codeblocks: IChatCodeBlockInfo[] = [];
|
||||
|
||||
if (isRequestVM(element)) {
|
||||
markdown = fixVariableReferences(markdown);
|
||||
}
|
||||
|
||||
const result = this.renderer.render(markdown, {
|
||||
fillInIncompleteTokens,
|
||||
codeBlockRendererSync: (languageId, text) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
73
src/vs/workbench/contrib/chat/common/chatParserTypes.ts
Normal file
73
src/vs/workbench/contrib/chat/common/chatParserTypes.ts
Normal file
@@ -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<IParsedChatRequestPart>;
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@@ -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<IParsedChatRequestPart[]> {
|
||||
async parseChatRequest(sessionId: string, message: string): Promise<IParsedChatRequest> {
|
||||
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<IParsedChatRequestPart>): 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) { }
|
||||
}
|
||||
|
||||
@@ -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<ISlashCommand[]>;
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
const resolvedAgent = typeof message === 'string' ? this.resolveAgent(message) : undefined;
|
||||
private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise<void> {
|
||||
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<void>(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<IChatSlashFragment>(p => {
|
||||
const agentResult = await this.chatAgentService.invokeAgent(agentPart.agent.id, message.substring(agentPart.agent.id.length + 1).trimStart(), new Progress<IChatSlashFragment>(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<IChatSlashFragment>(p => {
|
||||
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatSlashFragment>(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<string> {
|
||||
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<ISlashCommand[]> {
|
||||
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<void> {
|
||||
async addCompleteRequest(sessionId: string, message: string | IParsedChatRequest, response: IChatCompleteResponse): Promise<void> {
|
||||
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 {
|
||||
|
||||
@@ -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<IChatVariableResolveResult>;
|
||||
resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise<IChatVariableResolveResult>;
|
||||
}
|
||||
|
||||
interface IChatData {
|
||||
@@ -60,40 +61,29 @@ export class ChatVariablesService implements IChatVariablesService {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise<IChatVariableResolveResult> {
|
||||
async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise<IChatVariableResolveResult> {
|
||||
const resolvedVariables: Record<string, IChatRequestVariableValue[]> = {};
|
||||
const jobs: Promise<any>[] = [];
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
],
|
||||
text: "Hello Mr. @agent /subCommand thanks"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
],
|
||||
text: "@agent Please do /subCommand thanks"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
[
|
||||
{
|
||||
range: {
|
||||
start: 0,
|
||||
endExclusive: 13
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 14
|
||||
},
|
||||
text: "/explain this"
|
||||
}
|
||||
]
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
range: {
|
||||
start: 0,
|
||||
endExclusive: 13
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 14
|
||||
},
|
||||
text: "/explain this"
|
||||
}
|
||||
],
|
||||
text: "/explain this"
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
[
|
||||
{
|
||||
range: {
|
||||
start: 0,
|
||||
endExclusive: 26
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 27
|
||||
},
|
||||
text: "What does @selection mean?"
|
||||
}
|
||||
]
|
||||
{
|
||||
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?"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
{
|
||||
range: {
|
||||
start: 4,
|
||||
endExclusive: 9
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 5,
|
||||
endLineNumber: 1,
|
||||
endColumn: 10
|
||||
},
|
||||
text: " /fix"
|
||||
}
|
||||
],
|
||||
text: "/fix /fix"
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
[
|
||||
{
|
||||
range: {
|
||||
start: 0,
|
||||
endExclusive: 4
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 5
|
||||
},
|
||||
text: "test"
|
||||
}
|
||||
]
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
range: {
|
||||
start: 0,
|
||||
endExclusive: 4
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 5
|
||||
},
|
||||
text: "test"
|
||||
}
|
||||
],
|
||||
text: "test"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
{
|
||||
range: {
|
||||
start: 4,
|
||||
endExclusive: 9
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 5,
|
||||
endLineNumber: 1,
|
||||
endColumn: 10
|
||||
},
|
||||
text: " this"
|
||||
}
|
||||
],
|
||||
text: "/fix this"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
]
|
||||
{
|
||||
range: {
|
||||
start: 20,
|
||||
endExclusive: 26
|
||||
},
|
||||
editorRange: {
|
||||
startLineNumber: 1,
|
||||
startColumn: 21,
|
||||
endLineNumber: 1,
|
||||
endColumn: 27
|
||||
},
|
||||
text: " mean?"
|
||||
}
|
||||
],
|
||||
text: "What does @selection mean?"
|
||||
}
|
||||
@@ -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<IChatVariablesService>()({});
|
||||
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<IChatVariablesService>()({});
|
||||
variablesService.hasVariable.returns(false);
|
||||
@@ -108,6 +119,16 @@ suite('ChatRequestParser', () => {
|
||||
await assertSnapshot(result);
|
||||
});
|
||||
|
||||
test('agent with question mark', async () => {
|
||||
const agentsService = mockObject<IChatAgentService>()({});
|
||||
agentsService.getAgent.returns(<IChatAgentData>{ 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<IChatAgentService>()({});
|
||||
agentsService.getAgent.returns(<IChatAgentData>{ 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<IChatAgentService>()({});
|
||||
agentsService.getAgent.returns(<IChatAgentData>{ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } });
|
||||
instantiationService.stub(IChatAgentService, agentsService as any);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user