Skip to content

Commit 093605a

Browse files
authored
Chat request removes words that start with / (#248761)
Chat request removes words that start with /
1 parent 4c60486 commit 093605a

10 files changed

+245
-24
lines changed

src/vs/workbench/contrib/chat/common/chatRequestParser.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IPromptsService } from './promptSyntax/service/types.js';
1616

1717
const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent
1818
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
19-
const slashReg = /\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command
19+
const slashReg = /^\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command
2020

2121
export interface IChatParserContext {
2222
/** 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 {
169169
return;
170170
}
171171

172-
if (parts.some(p => p instanceof ChatRequestSlashCommandPart)) {
173-
// Only one slash command allowed
172+
if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) {
173+
// no other part than agent or non-whitespace text allowed: that also means no other slash command
174+
return;
175+
}
176+
177+
// only whitespace after the last part
178+
const previousPart = parts.at(-1);
179+
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
180+
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
181+
if (textSincePreviousPart.trim() !== '') {
174182
return;
175183
}
176184

@@ -180,18 +188,6 @@ export class ChatRequestParser {
180188

181189
const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart);
182190
if (usedAgent) {
183-
// The slash command must come immediately after the agent
184-
if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart))) {
185-
return;
186-
}
187-
188-
const previousPart = parts.at(-1);
189-
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
190-
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
191-
if (textSincePreviousPart.trim() !== '') {
192-
return;
193-
}
194-
195191
const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command);
196192
if (subCommand) {
197193
// Valid agent subcommand

src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class PromptsService extends Disposable implements IPromptsService {
134134
}
135135

136136
public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined {
137-
if (command.match(/^[\w_\-\.]+/)) {
137+
if (command.match(/^[\w_\-\.]+$/)) {
138138
return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) };
139139
}
140140
return undefined;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
parts: [
3+
{
4+
range: {
5+
start: 0,
6+
endExclusive: 4
7+
},
8+
editorRange: {
9+
startLineNumber: 1,
10+
startColumn: 1,
11+
endLineNumber: 1,
12+
endColumn: 5
13+
},
14+
text: " ",
15+
kind: "text"
16+
},
17+
{
18+
range: {
19+
start: 4,
20+
endExclusive: 11
21+
},
22+
editorRange: {
23+
startLineNumber: 1,
24+
startColumn: 5,
25+
endLineNumber: 1,
26+
endColumn: 12
27+
},
28+
slashPromptCommand: { command: "prompt" },
29+
kind: "prompt"
30+
}
31+
],
32+
text: " /prompt"
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
parts: [
3+
{
4+
range: {
5+
start: 0,
6+
endExclusive: 41
7+
},
8+
editorRange: {
9+
startLineNumber: 1,
10+
startColumn: 1,
11+
endLineNumber: 1,
12+
endColumn: 42
13+
},
14+
text: "/ route and the request of /search-option",
15+
kind: "text"
16+
}
17+
],
18+
text: "/ route and the request of /search-option"
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
parts: [
3+
{
4+
range: {
5+
start: 0,
6+
endExclusive: 52
7+
},
8+
editorRange: {
9+
startLineNumber: 1,
10+
startColumn: 1,
11+
endLineNumber: 1,
12+
endColumn: 53
13+
},
14+
text: "handle the / route and the request of /search-option",
15+
kind: "text"
16+
}
17+
],
18+
text: "handle the / route and the request of /search-option"
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
parts: [
3+
{
4+
range: {
5+
start: 0,
6+
endExclusive: 4
7+
},
8+
editorRange: {
9+
startLineNumber: 1,
10+
startColumn: 1,
11+
endLineNumber: 1,
12+
endColumn: 5
13+
},
14+
text: " ",
15+
kind: "text"
16+
},
17+
{
18+
range: {
19+
start: 4,
20+
endExclusive: 8
21+
},
22+
editorRange: {
23+
startLineNumber: 1,
24+
startColumn: 5,
25+
endLineNumber: 1,
26+
endColumn: 9
27+
},
28+
slashCommand: { command: "fix" },
29+
kind: "slash"
30+
}
31+
],
32+
text: " /fix"
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
parts: [
3+
{
4+
range: {
5+
start: 0,
6+
endExclusive: 10
7+
},
8+
editorRange: {
9+
startLineNumber: 1,
10+
startColumn: 1,
11+
endLineNumber: 1,
12+
endColumn: 11
13+
},
14+
text: "Hello /fix",
15+
kind: "text"
16+
}
17+
],
18+
text: "Hello /fix"
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
parts: [
3+
{
4+
range: {
5+
start: 0,
6+
endExclusive: 65
7+
},
8+
editorRange: {
9+
startLineNumber: 1,
10+
startColumn: 1,
11+
endLineNumber: 1,
12+
endColumn: 66
13+
},
14+
text: "can we add a new file for an Express router to handle the / route",
15+
kind: "text"
16+
}
17+
],
18+
text: "can we add a new file for an Express router to handle the / route"
19+
}

src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ suite('ChatRequestParser', () => {
6161
await assertSnapshot(result);
6262
});
6363

64+
test('slash in text', async () => {
65+
parser = instantiationService.createInstance(ChatRequestParser);
66+
const text = 'can we add a new file for an Express router to handle the / route';
67+
const result = parser.parseChatRequest('1', text);
68+
await assertSnapshot(result);
69+
});
70+
6471
test('slash command', async () => {
6572
const slashCommandService = mockObject<IChatSlashCommandService>()({});
6673
slashCommandService.getCommands.returns([{ command: 'fix' }]);
@@ -94,6 +101,89 @@ suite('ChatRequestParser', () => {
94101
await assertSnapshot(result);
95102
});
96103

104+
test('slash command not first', async () => {
105+
const slashCommandService = mockObject<IChatSlashCommandService>()({});
106+
slashCommandService.getCommands.returns([{ command: 'fix' }]);
107+
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
108+
109+
parser = instantiationService.createInstance(ChatRequestParser);
110+
const text = 'Hello /fix';
111+
const result = parser.parseChatRequest('1', text);
112+
await assertSnapshot(result);
113+
});
114+
115+
test('slash command after whitespace', async () => {
116+
const slashCommandService = mockObject<IChatSlashCommandService>()({});
117+
slashCommandService.getCommands.returns([{ command: 'fix' }]);
118+
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
119+
120+
parser = instantiationService.createInstance(ChatRequestParser);
121+
const text = ' /fix';
122+
const result = parser.parseChatRequest('1', text);
123+
await assertSnapshot(result);
124+
});
125+
126+
test('prompt slash command', async () => {
127+
const slashCommandService = mockObject<IChatSlashCommandService>()({});
128+
slashCommandService.getCommands.returns([{ command: 'fix' }]);
129+
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
130+
131+
const promptSlashCommandService = mockObject<IPromptsService>()({});
132+
promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => {
133+
if (command.match(/^[\w_\-\.]+$/)) {
134+
return { command };
135+
}
136+
return undefined;
137+
});
138+
instantiationService.stub(IPromptsService, promptSlashCommandService as any);
139+
140+
parser = instantiationService.createInstance(ChatRequestParser);
141+
const text = ' /prompt';
142+
const result = parser.parseChatRequest('1', text);
143+
await assertSnapshot(result);
144+
});
145+
146+
test('prompt slash command after text', async () => {
147+
const slashCommandService = mockObject<IChatSlashCommandService>()({});
148+
slashCommandService.getCommands.returns([{ command: 'fix' }]);
149+
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
150+
151+
const promptSlashCommandService = mockObject<IPromptsService>()({});
152+
promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => {
153+
if (command.match(/^[\w_\-\.]+$/)) {
154+
return { command };
155+
}
156+
return undefined;
157+
});
158+
instantiationService.stub(IPromptsService, promptSlashCommandService as any);
159+
160+
parser = instantiationService.createInstance(ChatRequestParser);
161+
const text = 'handle the / route and the request of /search-option';
162+
const result = parser.parseChatRequest('1', text);
163+
await assertSnapshot(result);
164+
});
165+
166+
test('prompt slash command after slash', async () => {
167+
const slashCommandService = mockObject<IChatSlashCommandService>()({});
168+
slashCommandService.getCommands.returns([{ command: 'fix' }]);
169+
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
170+
171+
const promptSlashCommandService = mockObject<IPromptsService>()({});
172+
promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => {
173+
if (command.match(/^[\w_\-\.]+$/)) {
174+
return { command };
175+
}
176+
return undefined;
177+
});
178+
instantiationService.stub(IPromptsService, promptSlashCommandService as any);
179+
180+
parser = instantiationService.createInstance(ChatRequestParser);
181+
const text = '/ route and the request of /search-option';
182+
const result = parser.parseChatRequest('1', text);
183+
await assertSnapshot(result);
184+
});
185+
186+
97187
// test('variables', async () => {
98188
// varService.hasVariable.returns(true);
99189
// varService.getVariable.returns({ id: 'copilot.selection' });

src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import { URI } from '../../../../../base/common/uri.js';
77
import { ITextModel } from '../../../../../editor/common/model.js';
8-
import { PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js';
98
import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js';
109
import { IChatPromptSlashCommand, IMetadata, IPromptPath, IPromptsService, TCombinedToolsMetadata, TPromptsType } from '../../common/promptSyntax/service/types.js';
1110

@@ -26,13 +25,7 @@ export class MockPromptsService implements IPromptsService {
2625
getSourceFolders(type: TPromptsType): readonly IPromptPath[] {
2726
throw new Error('Method not implemented.');
2827
}
29-
public asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined {
30-
if (name.endsWith(PROMPT_FILE_EXTENSION)) {
31-
const command = `prompt:${name.substring(0, -PROMPT_FILE_EXTENSION.length)}`;
32-
return {
33-
command, detail: name,
34-
};
35-
}
28+
public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined {
3629
return undefined;
3730
}
3831
resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise<IPromptPath | undefined> {

0 commit comments

Comments
 (0)