Don't show suggestions for agent subcommand in an invalid spot after other text (#195752)

And clean up the parser a little
This commit is contained in:
Rob Lourens
2023-10-17 10:46:56 +02:00
committed by GitHub
parent c6482f7f4b
commit 6490e99d80
8 changed files with 243 additions and 162 deletions
@@ -374,8 +374,8 @@ class AgentCompletions extends Disposable {
}
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) {
const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart);
if (usedAgentIdx < 0) {
return;
}
@@ -385,6 +385,14 @@ class AgentCompletions extends Disposable {
return;
}
for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) {
if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.match(/^\s+(\/\w*)?$/)) {
// No text allowed between agent and subcommand
return;
}
}
const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart;
const commands = await usedAgent.agent.provideSlashCommands(token);
return <CompletionList>{
@@ -7,7 +7,7 @@ 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';
import { IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicReferencePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatVariableLeader } 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';
@@ -33,14 +33,14 @@ export class ChatRequestParser {
const previousChar = message.charAt(i - 1);
const char = message.charAt(i);
let newPart: IParsedChatRequestPart | undefined;
if (previousChar === ' ' || i === 0) {
if (previousChar.match(/\s/) || i === 0) {
if (char === chatVariableLeader) {
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts);
} else if (char === '@') {
newPart = this.tryToParseAgent(message.slice(i), i, new Position(lineNumber, column), parts);
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts);
} else if (char === '/') {
// TODO try to make this sync
newPart = await this.tryToParseSlashCommand(sessionId, message.slice(i), i, new Position(lineNumber, column), parts);
newPart = await this.tryToParseSlashCommand(sessionId, message.slice(i), message, i, new Position(lineNumber, column), parts);
} else if (char === '$') {
newPart = await this.tryToParseDynamicVariable(sessionId, message.slice(i), i, new Position(lineNumber, column), parts);
}
@@ -79,36 +79,13 @@ export class ChatRequestParser {
message.slice(lastPartEnd, message.length)));
}
// fix up parts:
// * only one agent at the beginning of the message
// * only one agent command after the agent or at the beginning of the message
let agentIndex = -1;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part instanceof ChatRequestAgentPart) {
if (i === 0) {
agentIndex = 0;
} else {
// agent not first -> make text part
parts[i] = new ChatRequestTextPart(part.range, part.editorRange, part.text);
}
}
if (part instanceof ChatRequestAgentSubcommandPart) {
if (!(i === 0 || agentIndex === 0 && i === 2 && /^\s+$/.test(parts[1].text))) {
// agent command not after agent nor first -> make text part
parts[i] = new ChatRequestTextPart(part.range, part.editorRange, part.text);
}
}
}
return {
parts,
text: message,
};
}
private tryToParseAgent(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
const nextVariableMatch = message.match(agentReg);
if (!nextVariableMatch) {
return;
@@ -118,17 +95,29 @@ export class ChatRequestParser {
const varRange = new OffsetRange(offset, offset + full.length);
const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
let agent: IChatAgent | undefined;
if ((agent = this.agentService.getAgent(name))) {
if (parts.some(p => p instanceof ChatRequestAgentPart)) {
// Only one agent allowed
return;
} else {
return new ChatRequestAgentPart(varRange, varEditorRange, agent);
}
const agent = this.agentService.getAgent(name);
if (!agent) {
return;
}
return;
if (parts.some(p => p instanceof ChatRequestAgentPart)) {
// Only one agent allowed
return;
}
// The agent must come first
if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart))) {
return;
}
const previousPart = parts.at(-1);
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
if (textSincePreviousPart.trim() !== '') {
return;
}
return new ChatRequestAgentPart(varRange, varEditorRange, agent);
}
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
@@ -149,8 +138,8 @@ export class ChatRequestParser {
return;
}
private async tryToParseSlashCommand(sessionId: string, message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): Promise<ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined> {
const nextSlashMatch = message.match(slashReg);
private async tryToParseSlashCommand(sessionId: string, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): Promise<ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined> {
const nextSlashMatch = remainingMessage.match(slashReg);
if (!nextSlashMatch) {
return;
}
@@ -166,6 +155,18 @@ export class ChatRequestParser {
const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart);
if (usedAgent) {
// The slash command must come immediately after the agent
if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart))) {
return;
}
const previousPart = parts.at(-1);
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
if (textSincePreviousPart.trim() !== '') {
return;
}
const subCommands = await usedAgent.agent.provideSlashCommands(CancellationToken.None);
const subCommand = subCommands.find(c => c.name === command);
if (subCommand) {
@@ -0,0 +1,82 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 5
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 1
},
text: " \n",
kind: "text"
},
{
range: {
start: 5,
endExclusive: 11
},
editorRange: {
startLineNumber: 2,
startColumn: 1,
endLineNumber: 2,
endColumn: 7
},
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
},
kind: "agent"
},
{
range: {
start: 11,
endExclusive: 12
},
editorRange: {
startLineNumber: 2,
startColumn: 7,
endLineNumber: 3,
endColumn: 1
},
text: "\n",
kind: "text"
},
{
range: {
start: 12,
endExclusive: 23
},
editorRange: {
startLineNumber: 3,
startColumn: 1,
endLineNumber: 3,
endColumn: 12
},
command: {
name: "subCommand",
description: ""
},
kind: "subcommand"
},
{
range: {
start: 23,
endExclusive: 30
},
editorRange: {
startLineNumber: 3,
startColumn: 12,
endLineNumber: 3,
endColumn: 19
},
text: " Thanks",
kind: "text"
}
],
text: " \n@agent\n/subCommand Thanks"
}
@@ -0,0 +1,82 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 10
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 5
},
text: " \r\n\t ",
kind: "text"
},
{
range: {
start: 10,
endExclusive: 16
},
editorRange: {
startLineNumber: 2,
startColumn: 5,
endLineNumber: 2,
endColumn: 11
},
agent: {
id: "agent",
metadata: { description: "" },
provideSlashCommands: [Function provideSlashCommands]
},
kind: "agent"
},
{
range: {
start: 16,
endExclusive: 23
},
editorRange: {
startLineNumber: 2,
startColumn: 11,
endLineNumber: 3,
endColumn: 5
},
text: " \r\n\t ",
kind: "text"
},
{
range: {
start: 23,
endExclusive: 34
},
editorRange: {
startLineNumber: 3,
startColumn: 5,
endLineNumber: 3,
endColumn: 16
},
command: {
name: "subCommand",
description: ""
},
kind: "subcommand"
},
{
range: {
start: 34,
endExclusive: 41
},
editorRange: {
startLineNumber: 3,
startColumn: 16,
endLineNumber: 3,
endColumn: 23
},
text: " Thanks",
kind: "text"
}
],
text: " \r\n\t @agent \r\n\t /subCommand Thanks"
}
@@ -3,73 +3,17 @@
{
range: {
start: 0,
endExclusive: 10
endExclusive: 16
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 11
},
text: "Hello Mr. ",
kind: "text"
},
{
range: {
start: 10,
endExclusive: 16
},
editorRange: {
startLineNumber: 1,
startColumn: 11,
endLineNumber: 1,
endColumn: 17
},
text: "@agent",
kind: "text"
},
{
range: {
start: 16,
endExclusive: 17
},
editorRange: {
startLineNumber: 1,
startColumn: 17,
endLineNumber: 1,
endColumn: 18
},
text: " ",
kind: "text"
},
{
range: {
start: 17,
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 18,
endLineNumber: 1,
endColumn: 29
},
text: "/subCommand",
kind: "text"
},
{
range: {
start: 28,
endExclusive: 35
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 36
},
text: " thanks",
text: "Hello Mr. @agent",
kind: "text"
}
],
text: "Hello Mr. @agent /subCommand thanks"
text: "Hello Mr. @agent"
}
@@ -21,43 +21,15 @@
{
range: {
start: 6,
endExclusive: 17
endExclusive: 35
},
editorRange: {
startLineNumber: 1,
startColumn: 7,
endLineNumber: 1,
endColumn: 18
},
text: " Please do ",
kind: "text"
},
{
range: {
start: 17,
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 18,
endLineNumber: 1,
endColumn: 29
},
text: "/subCommand",
kind: "text"
},
{
range: {
start: 28,
endExclusive: 35
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 36
},
text: " thanks",
text: " Please do /subCommand thanks",
kind: "text"
}
],
@@ -21,43 +21,15 @@
{
range: {
start: 6,
endExclusive: 18
endExclusive: 35
},
editorRange: {
startLineNumber: 1,
startColumn: 7,
endLineNumber: 2,
endColumn: 4
},
text: " Please \ndo ",
kind: "text"
},
{
range: {
start: 18,
endExclusive: 29
},
editorRange: {
startLineNumber: 2,
startColumn: 4,
endLineNumber: 2,
endColumn: 15
},
text: "/subCommand",
kind: "text"
},
{
range: {
start: 29,
endExclusive: 35
},
editorRange: {
startLineNumber: 2,
startColumn: 15,
endLineNumber: 2,
endColumn: 21
},
text: " with ",
text: " Please \ndo /subCommand with ",
kind: "text"
},
{
@@ -109,7 +109,7 @@ suite('ChatRequestParser', () => {
await assertSnapshot(result);
});
test('agents', async () => {
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: '' }]; } });
instantiationService.stub(IChatAgentService, agentsService as any);
@@ -139,13 +139,33 @@ suite('ChatRequestParser', () => {
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: '' }]; } });
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');
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: '' }]; } });
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await 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: '' }]; } });
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const result = await parser.parseChatRequest('1', 'Hello Mr. @agent /subCommand thanks');
const result = await parser.parseChatRequest('1', 'Hello Mr. @agent');
await assertSnapshot(result);
});