Don't allow picking the same variable multiple times (#203975)

* Cache agent subCommands, make some async code sync

* Fix lastSlashCommands

* Cache parsed chat query in one place so we don't have to parse the same thing over and over

* Don't allow picking the same variable multiple times
Fix microsoft/vscode-copilot-release#733

* Fix tests
This commit is contained in:
Rob Lourens
2024-02-01 16:33:38 +01:00
committed by GitHub
parent dcea438aca
commit dbd391ebba
17 changed files with 154 additions and 106 deletions
@@ -18,7 +18,7 @@ import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionCh
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
@@ -75,6 +75,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
}
$registerAgent(handle: number, name: string, metadata: IExtensionChatAgentMetadata): void {
let lastSlashCommands: IChatAgentCommand[] | undefined;
const d = this._chatAgentService.registerAgent({
id: name,
metadata: revive(metadata),
@@ -93,11 +94,15 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
return this._proxy.$provideFollowups(handle, sessionId, token);
},
get lastSlashCommands() {
return lastSlashCommands;
},
provideSlashCommands: async (token) => {
if (!this._agents.get(handle)?.hasSlashCommands) {
return []; // save an IPC call
}
return this._proxy.$provideSlashCommands(handle, token);
lastSlashCommands = await this._proxy.$provideSlashCommands(handle, token);
return lastSlashCommands;
}
});
this._agents.set(handle, {
@@ -141,7 +146,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
return;
}
const parsedRequest = (await this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts;
const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts;
const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart);
const thisAgentName = this._agents.get(handle)?.name;
if (agentPart?.agent.id !== thisAgentName) {
@@ -9,6 +9,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Selection } from 'vs/editor/common/core/selection';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
export const IChatWidgetService = createDecorator<IChatWidgetService>('chatWidgetService');
@@ -105,6 +106,7 @@ export interface IChatWidget {
readonly inputEditor: ICodeEditor;
readonly providerId: string;
readonly supportsFileReferences: boolean;
readonly parsedInput: IParsedChatRequest;
getContrib<T extends IChatWidgetContrib>(id: string): T | undefined;
reveal(item: ChatTreeItem): void;
@@ -33,6 +33,8 @@ import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common
import { IChatReplyFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
const $ = dom.$;
@@ -127,6 +129,15 @@ export class ChatWidget extends Disposable implements IChatWidget {
return this._viewModel;
}
private parsedChatRequest: IParsedChatRequest | undefined;
get parsedInput() {
if (this.parsedChatRequest === undefined) {
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput());
}
return this.parsedChatRequest;
}
constructor(
readonly viewContext: IChatWidgetViewContext,
private readonly viewOptions: IChatWidgetViewOptions,
@@ -436,6 +447,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
});
}));
this._register(this.inputPart.onDidChangeHeight(() => this.bodyDimension && this.layout(this.bodyDimension.height, this.bodyDimension.width)));
this._register(this.inputEditor.onDidChangeModelContent(() => {
this.parsedChatRequest = undefined;
}));
}
private onDidStyleChange(): void {
@@ -553,8 +567,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
const lastResponse = responses?.[responses.length - 1];
this._chatAccessibilityService.acceptResponse(lastResponse, requestId);
});
} else {
this._chatAccessibilityService.acceptResponse(undefined, requestId);
}
}
}
@@ -15,7 +15,6 @@ import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList }
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry';
@@ -33,7 +32,6 @@ import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestP
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
const decorationDescription = 'chat';
@@ -55,7 +53,6 @@ class InputEditorDecorations extends Disposable {
constructor(
private readonly widget: IChatWidget,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@IThemeService private readonly themeService: IThemeService,
@IChatService private readonly chatService: IChatService,
@@ -154,7 +151,7 @@ class InputEditorDecorations extends Disposable {
return;
}
const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(viewModel.sessionId, inputValue)).parts;
const parsedRequest = this.widget.parsedInput.parts;
let placeholderDecoration: IDecorationOptions[] | undefined;
const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart);
@@ -294,7 +291,6 @@ class SlashCommandCompletions extends Disposable {
constructor(
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService
) {
super();
@@ -313,7 +309,7 @@ class SlashCommandCompletions extends Disposable {
return null;
}
const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts;
const parsedRequest = widget.parsedInput.parts;
const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart);
if (usedAgent) {
// No (classic) global slash commands when an agent is used
@@ -351,7 +347,6 @@ class AgentCompletions extends Disposable {
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
@@ -364,7 +359,7 @@ class AgentCompletions extends Disposable {
return null;
}
const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts;
const parsedRequest = widget.parsedInput.parts;
const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart);
if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) {
// Only one agent allowed
@@ -407,7 +402,7 @@ class AgentCompletions extends Disposable {
return null;
}
const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts;
const parsedRequest = widget.parsedInput.parts;
const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart);
if (usedAgentIdx < 0) {
return;
@@ -428,7 +423,7 @@ class AgentCompletions extends Disposable {
}
const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart;
const commands = await usedAgent.agent.provideSlashCommands(token);
const commands = await usedAgent.agent.provideSlashCommands(token); // Refresh the cache here
return <CompletionList>{
suggestions: commands.map((c, i) => {
@@ -576,7 +571,6 @@ class VariableCompletions extends Disposable {
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IChatVariablesService private readonly chatVariablesService: IChatVariablesService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
@@ -595,33 +589,24 @@ class VariableCompletions extends Disposable {
return null;
}
const history = widget.viewModel!.getItems()
.filter(isResponseVM);
// TODO@roblourens work out a real API for this- maybe it can be part of the two-step flow that @file will probably use
const historyVariablesEnabled = this.configurationService.getValue('chat.experimental.historyVariables');
const historyItems = historyVariablesEnabled ? history.map((h, i): CompletionItem => ({
label: `${chatVariableLeader}response:${i + 1}`,
detail: h.response.asString(),
insertText: `${chatVariableLeader}response:${String(i + 1).padStart(String(history.length).length, '0')} `,
kind: CompletionItemKind.Text,
range,
})) : [];
const variableItems = Array.from(this.chatVariablesService.getVariables()).map(v => {
const withLeader = `${chatVariableLeader}${v.name}`;
return <CompletionItem>{
label: withLeader,
range,
insertText: withLeader + ' ',
detail: v.description,
kind: CompletionItemKind.Text, // The icons are disabled here anyway
sortText: 'z'
};
});
const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart);
const variableItems = Array.from(this.chatVariablesService.getVariables())
// This doesn't look at dynamic variables like `file`, where multiple makes sense.
.filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name))
.map(v => {
const withLeader = `${chatVariableLeader}${v.name}`;
return <CompletionItem>{
label: withLeader,
range,
insertText: withLeader + ' ',
detail: v.description,
kind: CompletionItemKind.Text, // The icons are disabled here anyway
sortText: 'z'
};
});
return <CompletionList>{
suggestions: [...variableItems, ...historyItems]
suggestions: variableItems
};
}
}));
@@ -655,22 +640,22 @@ class ChatTokenDeleter extends Disposable {
// If this was a simple delete, try to find out whether it was inside a token
if (!change.text && this.widget.viewModel) {
parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue).then(previousParsedValue => {
// For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping
const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart);
deletableTokens.forEach(token => {
const deletedRangeOfToken = Range.intersectRanges(token.editorRange, change.range);
// Part of this token was deleted, and the deletion range doesn't go off the front of the token, for simpler math
if ((deletedRangeOfToken && !deletedRangeOfToken.isEmpty()) && Range.compareRangesUsingStarts(token.editorRange, change.range) < 0) {
// Assume single line tokens
const length = deletedRangeOfToken.endColumn - deletedRangeOfToken.startColumn;
const rangeToDelete = new Range(token.editorRange.startLineNumber, token.editorRange.startColumn, token.editorRange.endLineNumber, token.editorRange.endColumn - length);
this.widget.inputEditor.executeEdits(this.id, [{
range: rangeToDelete,
text: '',
}]);
}
});
const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue);
// For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping
const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart);
deletableTokens.forEach(token => {
const deletedRangeOfToken = Range.intersectRanges(token.editorRange, change.range);
// Part of this token was deleted, and the deletion range doesn't go off the front of the token, for simpler math
if ((deletedRangeOfToken && !deletedRangeOfToken.isEmpty()) && Range.compareRangesUsingStarts(token.editorRange, change.range) < 0) {
// Assume single line tokens
const length = deletedRangeOfToken.endColumn - deletedRangeOfToken.startColumn;
const rangeToDelete = new Range(token.editorRange.startLineNumber, token.editorRange.startColumn, token.editorRange.endLineNumber, token.editorRange.endColumn - length);
this.widget.inputEditor.executeEdits(this.id, [{
range: rangeToDelete,
text: '',
}]);
}
});
}
@@ -17,20 +17,21 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat
//#region agent service, commands etc
export interface IChatAgentData {
id: string;
metadata: IChatAgentMetadata;
}
export interface IChatAgentHistoryEntry {
request: IChatAgentRequest;
response: ReadonlyArray<IChatProgressResponseContent>;
result: IChatAgentResult;
}
export interface IChatAgentData {
id: string;
metadata: IChatAgentMetadata;
}
export interface IChatAgent extends IChatAgentData {
invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
provideFollowups?(sessionId: string, token: CancellationToken): Promise<IChatFollowup[]>;
lastSlashCommands?: IChatAgentCommand[];
provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]>;
}
@@ -146,6 +147,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
throw new Error(`No agent with id ${id} registered`);
}
data.agent.metadata = { ...data.agent.metadata, ...updateMetadata };
data.agent.provideSlashCommands(CancellationToken.None); // Update the cached slash commands
this._onDidChangeAgents.fire();
}
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { Range } from 'vs/editor/common/core/range';
@@ -23,7 +22,7 @@ export class ChatRequestParser {
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService
) { }
async parseChatRequest(sessionId: string, message: string): Promise<IParsedChatRequest> {
parseChatRequest(sessionId: string, message: string): IParsedChatRequest {
const parts: IParsedChatRequestPart[] = [];
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
@@ -39,8 +38,7 @@ export class ChatRequestParser {
} else if (char === chatAgentLeader) {
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts);
} else if (char === chatSubcommandLeader) {
// TODO try to make this sync
newPart = await this.tryToParseSlashCommand(sessionId, message.slice(i), message, i, new Position(lineNumber, column), parts);
newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts);
}
if (!newPart) {
@@ -140,7 +138,7 @@ export class ChatRequestParser {
return;
}
private async tryToParseSlashCommand(sessionId: string, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): Promise<ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined> {
private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined {
const nextSlashMatch = remainingMessage.match(slashReg);
if (!nextSlashMatch) {
return;
@@ -169,8 +167,8 @@ export class ChatRequestParser {
return;
}
const subCommands = await usedAgent.agent.provideSlashCommands(CancellationToken.None);
const subCommand = subCommands.find(c => c.name === command);
const subCommands = usedAgent.agent.lastSlashCommands;
const subCommand = subCommands?.find(c => c.name === command);
if (subCommand) {
// Valid agent subcommand
return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);
@@ -459,7 +459,7 @@ export class ChatService extends Disposable implements IChatService {
}
private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, message: string): Promise<void> {
const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message);
const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message);
let request: ChatRequestModel;
const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
@@ -656,7 +656,7 @@ export class ChatService extends Disposable implements IChatService {
await model.waitForInitialization();
const parsedRequest = typeof message === 'string' ?
await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) :
this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) :
message;
const request = model.addRequest(parsedRequest, variableData || { message: parsedRequest.text, variables: {} });
if (typeof response.message === 'string') {
@@ -28,7 +28,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -28,7 +28,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -14,7 +14,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -14,7 +14,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -14,7 +14,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -14,7 +14,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -14,7 +14,13 @@
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
provideSlashCommands: [Function provideSlashCommands],
lastSlashCommands: [
{
name: "subCommand",
description: ""
}
]
},
kind: "agent"
},
@@ -50,7 +50,7 @@
},
response: [ ],
responseErrorDetails: undefined,
followups: [ ],
followups: undefined,
isCanceled: false,
vote: undefined,
agent: {
@@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti
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, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatAgentService, IChatAgent, IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
@@ -37,14 +37,14 @@ suite('ChatRequestParser', () => {
test('plain text', async () => {
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', 'test');
const result = parser.parseChatRequest('1', 'test');
await assertSnapshot(result);
});
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);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
@@ -55,7 +55,7 @@ suite('ChatRequestParser', () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/fix this';
const result = await parser.parseChatRequest('1', text);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
@@ -66,7 +66,7 @@ suite('ChatRequestParser', () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/explain this';
const result = await parser.parseChatRequest('1', text);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
@@ -77,7 +77,7 @@ suite('ChatRequestParser', () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/fix /fix';
const result = await parser.parseChatRequest('1', text);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
@@ -86,7 +86,7 @@ suite('ChatRequestParser', () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = 'What does #selection mean?';
const result = await parser.parseChatRequest('1', text);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
@@ -95,7 +95,7 @@ suite('ChatRequestParser', () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = 'What is #selection?';
const result = await parser.parseChatRequest('1', text);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
@@ -104,91 +104,95 @@ suite('ChatRequestParser', () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = 'What does #selection mean?';
const result = await parser.parseChatRequest('1', text);
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
const getAgentWithSlashcommands = (slashCommands: IChatAgentCommand[]) => {
return <Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => [], lastSlashCommands: slashCommands };
};
test('agent with subcommand after text', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', '@agent Please do /subCommand thanks');
const result = parser.parseChatRequest('1', '@agent Please do /subCommand thanks');
await assertSnapshot(result);
});
test('agents, subCommand', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', '@agent /subCommand Please do thanks');
const result = parser.parseChatRequest('1', '@agent /subCommand Please do thanks');
await assertSnapshot(result);
});
test('agent with question mark', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', '@agent? Are you there');
const result = parser.parseChatRequest('1', '@agent? Are you there');
await assertSnapshot(result);
});
test('agent and subcommand with leading whitespace', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', ' \r\n\t @agent \r\n\t /subCommand Thanks');
const result = parser.parseChatRequest('1', ' \r\n\t @agent \r\n\t /subCommand Thanks');
await assertSnapshot(result);
});
test('agent and subcommand after newline', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', ' \n@agent\n/subCommand Thanks');
const result = parser.parseChatRequest('1', ' \n@agent\n/subCommand Thanks');
await assertSnapshot(result);
});
test('agent not first', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', 'Hello Mr. @agent');
const result = parser.parseChatRequest('1', 'Hello Mr. @agent');
await assertSnapshot(result);
});
test('agents and variables and multiline', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole');
const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole');
await assertSnapshot(result);
});
test('agents and variables and multiline, part2', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(<Partial<IChatAgent>>{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => { return [{ name: 'subCommand', description: '' }]; } });
agentsService.getAgent.returns(getAgentWithSlashcommands([{ name: 'subCommand', description: '' }]));
instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole');
const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole');
await assertSnapshot(result);
});
});
@@ -227,8 +227,6 @@ suite('Chat', () => {
const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);
assert(response);
await response.responseCompletePromise;
assert.strictEqual(model.getRequests().length, 1);
await assertSnapshot(model.toExport());