Chat request removes words that start with / (#248795)

This commit is contained in:
Martin Aeschlimann
2025-05-13 11:09:59 +02:00
committed by GitHub
parent ed48873ba2
commit 0c82ecfe3c
10 changed files with 245 additions and 24 deletions
@@ -16,7 +16,7 @@ import { IPromptsService } from './promptSyntax/service/types.js';
const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
const slashReg = /\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command
const slashReg = /^\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command
export interface IChatParserContext {
/** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */
@@ -169,8 +169,16 @@ export class ChatRequestParser {
return;
}
if (parts.some(p => p instanceof ChatRequestSlashCommandPart)) {
// Only one slash command allowed
if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) {
// no other part than agent or non-whitespace text allowed: that also means no other slash command
return;
}
// only whitespace after the last part
const previousPart = parts.at(-1);
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
if (textSincePreviousPart.trim() !== '') {
return;
}
@@ -180,18 +188,6 @@ 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 subCommand = usedAgent.agent.slashCommands.find(c => c.name === command);
if (subCommand) {
// Valid agent subcommand
@@ -144,7 +144,7 @@ export class PromptsService extends Disposable implements IPromptsService {
}
public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined {
if (command.match(/^[\w_\-\.]+/)) {
if (command.match(/^[\w_\-\.]+$/)) {
return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) };
}
return undefined;
@@ -0,0 +1,33 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 4
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 5
},
text: " ",
kind: "text"
},
{
range: {
start: 4,
endExclusive: 11
},
editorRange: {
startLineNumber: 1,
startColumn: 5,
endLineNumber: 1,
endColumn: 12
},
slashPromptCommand: { command: "prompt" },
kind: "prompt"
}
],
text: " /prompt"
}
@@ -0,0 +1,19 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 42
},
text: "/ route and the request of /search-option",
kind: "text"
}
],
text: "/ route and the request of /search-option"
}
@@ -0,0 +1,19 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 52
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 53
},
text: "handle the / route and the request of /search-option",
kind: "text"
}
],
text: "handle the / route and the request of /search-option"
}
@@ -0,0 +1,33 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 4
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 5
},
text: " ",
kind: "text"
},
{
range: {
start: 4,
endExclusive: 8
},
editorRange: {
startLineNumber: 1,
startColumn: 5,
endLineNumber: 1,
endColumn: 9
},
slashCommand: { command: "fix" },
kind: "slash"
}
],
text: " /fix"
}
@@ -0,0 +1,19 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 10
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 11
},
text: "Hello /fix",
kind: "text"
}
],
text: "Hello /fix"
}
@@ -0,0 +1,19 @@
{
parts: [
{
range: {
start: 0,
endExclusive: 65
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 66
},
text: "can we add a new file for an Express router to handle the / route",
kind: "text"
}
],
text: "can we add a new file for an Express router to handle the / route"
}
@@ -61,6 +61,13 @@ suite('ChatRequestParser', () => {
await assertSnapshot(result);
});
test('slash in text', async () => {
parser = instantiationService.createInstance(ChatRequestParser);
const text = 'can we add a new file for an Express router to handle the / route';
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
test('slash command', async () => {
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
@@ -94,6 +101,89 @@ suite('ChatRequestParser', () => {
await assertSnapshot(result);
});
test('slash command not first', async () => {
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = 'Hello /fix';
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
test('slash command after whitespace', async () => {
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = ' /fix';
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
test('prompt slash command', async () => {
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
const promptSlashCommandService = mockObject<IPromptsService>()({});
promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => {
if (command.match(/^[\w_\-\.]+$/)) {
return { command };
}
return undefined;
});
instantiationService.stub(IPromptsService, promptSlashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = ' /prompt';
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
test('prompt slash command after text', async () => {
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
const promptSlashCommandService = mockObject<IPromptsService>()({});
promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => {
if (command.match(/^[\w_\-\.]+$/)) {
return { command };
}
return undefined;
});
instantiationService.stub(IPromptsService, promptSlashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = 'handle the / route and the request of /search-option';
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
test('prompt slash command after slash', async () => {
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
const promptSlashCommandService = mockObject<IPromptsService>()({});
promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => {
if (command.match(/^[\w_\-\.]+$/)) {
return { command };
}
return undefined;
});
instantiationService.stub(IPromptsService, promptSlashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/ route and the request of /search-option';
const result = parser.parseChatRequest('1', text);
await assertSnapshot(result);
});
// test('variables', async () => {
// varService.hasVariable.returns(true);
// varService.getVariable.returns({ id: 'copilot.selection' });
@@ -5,7 +5,6 @@
import { URI } from '../../../../../base/common/uri.js';
import { ITextModel } from '../../../../../editor/common/model.js';
import { PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js';
import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js';
import { IChatPromptSlashCommand, IMetadata, IPromptPath, IPromptsService, TCombinedToolsMetadata, TPromptsType } from '../../common/promptSyntax/service/types.js';
@@ -27,13 +26,7 @@ export class MockPromptsService implements IPromptsService {
getSourceFolders(_type: TPromptsType): readonly IPromptPath[] {
throw new Error('Method not implemented.');
}
public asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined {
if (name.endsWith(PROMPT_FILE_EXTENSION)) {
const command = `prompt:${name.substring(0, -PROMPT_FILE_EXTENSION.length)}`;
return {
command, detail: name,
};
}
public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined {
return undefined;
}
resolvePromptSlashCommand(_data: IChatPromptSlashCommand): Promise<IPromptPath | undefined> {