Skip to content

Commit 111fec7

Browse files
aeschliCopilot
andauthored
agent files: validation, completion and hovers is target dependend (microsoft#273133)
* agent files: validation, completion and hovers is target dependend * use Lazy * Update src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0896163 commit 111fec7

File tree

12 files changed

+688
-165
lines changed

12 files changed

+688
-165
lines changed

src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ export class PromptFileRewriter {
2828
}
2929
const model = editor.getModel();
3030

31-
const parser = this._promptsService.getParsedPromptFile(model);
32-
if (!parser.header) {
31+
const promptAST = this._promptsService.getParsedPromptFile(model);
32+
if (!promptAST.header) {
3333
return undefined;
3434
}
3535

36-
const toolsAttr = parser.header.getAttribute(PromptHeaderAttributes.tools);
36+
const toolsAttr = promptAST.header.getAttribute(PromptHeaderAttributes.tools);
3737
if (!toolsAttr) {
3838
return undefined;
3939
}

src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ import { CommandsRegistry } from '../../../../../platform/commands/common/comman
1414
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1515
import { showToolsPicker } from '../actions/chatToolPicker.js';
1616
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
17-
import { ALL_PROMPTS_LANGUAGE_SELECTOR } from '../../common/promptSyntax/promptTypes.js';
17+
import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType } from '../../common/promptSyntax/promptTypes.js';
1818
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
1919
import { registerEditorFeature } from '../../../../../editor/common/editorFeatures.js';
2020
import { PromptFileRewriter } from './promptFileRewriter.js';
2121
import { Range } from '../../../../../editor/common/core/range.js';
2222
import { IEditorModel } from '../../../../../editor/common/editorCommon.js';
2323
import { PromptHeaderAttributes } from '../../common/promptSyntax/promptFileParser.js';
24+
import { isGithubTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js';
2425

2526
class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider {
2627

@@ -48,14 +49,23 @@ class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider
4849
}
4950

5051
async provideCodeLenses(model: ITextModel, token: CancellationToken): Promise<undefined | CodeLensList> {
52+
const promptType = getPromptsTypeForLanguageId(model.getLanguageId());
53+
if (!promptType || promptType === PromptsType.instructions) {
54+
// if the model is not a prompt, we don't provide any code actions
55+
return undefined;
56+
}
5157

52-
const parser = this.promptsService.getParsedPromptFile(model);
53-
if (!parser.header) {
58+
const promptAST = this.promptsService.getParsedPromptFile(model);
59+
const header = promptAST.header;
60+
if (!header) {
5461
return undefined;
5562
}
5663

64+
if (isGithubTarget(promptType, header.target)) {
65+
return undefined;
66+
}
5767

58-
const toolsAttr = parser.header.getAttribute(PromptHeaderAttributes.tools);
68+
const toolsAttr = header.getAttribute(PromptHeaderAttributes.tools);
5969
if (!toolsAttr || toolsAttr.value.type !== 'array') {
6070
return undefined;
6171
}

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export class PromptHeaderDefinitionProvider implements DefinitionProvider {
3232
return undefined;
3333
}
3434

35-
const parser = this.promptsService.getParsedPromptFile(model);
36-
const header = parser.header;
35+
const promptAST = this.promptsService.getParsedPromptFile(model);
36+
const header = promptAST.header;
3737
if (!header) {
3838
return undefined;
3939
}

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Lazy } from '../../../../../../base/common/lazy.js';
1717
import { LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js';
1818
import { IFileService } from '../../../../../../platform/files/common/files.js';
1919
import { URI } from '../../../../../../base/common/uri.js';
20+
import { isGithubTarget } from './promptValidator.js';
2021

2122
export class PromptCodeActionProvider implements CodeActionProvider {
2223
/**
@@ -40,15 +41,15 @@ export class PromptCodeActionProvider implements CodeActionProvider {
4041

4142
const result: CodeAction[] = [];
4243

43-
const parser = this.promptsService.getParsedPromptFile(model);
44+
const promptAST = this.promptsService.getParsedPromptFile(model);
4445
switch (promptType) {
4546
case PromptsType.agent:
46-
this.getUpdateToolsCodeActions(parser, model, range, result);
47+
this.getUpdateToolsCodeActions(promptAST, promptType, model, range, result);
4748
await this.getMigrateModeFileCodeActions(model.uri, result);
4849
break;
4950
case PromptsType.prompt:
50-
this.getUpdateModeCodeActions(parser, model, range, result);
51-
this.getUpdateToolsCodeActions(parser, model, range, result);
51+
this.getUpdateModeCodeActions(promptAST, model, range, result);
52+
this.getUpdateToolsCodeActions(promptAST, promptType, model, range, result);
5253
break;
5354
}
5455

@@ -91,11 +92,16 @@ export class PromptCodeActionProvider implements CodeActionProvider {
9192
}
9293
}
9394

94-
private getUpdateToolsCodeActions(promptFile: ParsedPromptFile, model: ITextModel, range: Range, result: CodeAction[]): void {
95+
private getUpdateToolsCodeActions(promptFile: ParsedPromptFile, promptType: PromptsType, model: ITextModel, range: Range, result: CodeAction[]): void {
9596
const toolsAttr = promptFile.header?.getAttribute(PromptHeaderAttributes.tools);
9697
if (toolsAttr?.value.type !== 'array' || !toolsAttr.value.range.containsRange(range)) {
9798
return;
9899
}
100+
if (isGithubTarget(promptType, promptFile.header?.target)) {
101+
// GitHub Copilot custom agents use a fixed set of tool names that are not deprecated
102+
return;
103+
}
104+
99105
const values = toolsAttr.value.items;
100106
const deprecatedNames = new Lazy(() => this.languageModelToolsService.getDeprecatedQualifiedToolNames());
101107
const edits: TextEdit[] = [];

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DocumentSemanticTokensProvider, ProviderResult, SemanticTokens, Semanti
88
import { ITextModel } from '../../../../../../editor/common/model.js';
99
import { getPromptsTypeForLanguageId } from '../promptTypes.js';
1010
import { IPromptsService } from '../service/promptsService.js';
11+
import { isGithubTarget } from './promptValidator.js';
1112

1213
export class PromptDocumentSemanticTokensProvider implements DocumentSemanticTokensProvider {
1314
/**
@@ -27,12 +28,17 @@ export class PromptDocumentSemanticTokensProvider implements DocumentSemanticTok
2728
return undefined;
2829
}
2930

30-
const parser = this.promptsService.getParsedPromptFile(model);
31-
if (!parser.body) {
31+
const promptAST = this.promptsService.getParsedPromptFile(model);
32+
if (!promptAST.body) {
3233
return undefined;
3334
}
3435

35-
const variableReferences = parser.body.variableReferences;
36+
if (isGithubTarget(promptType, promptAST.header?.target)) {
37+
// In GitHub Copilot mode, we don't provide variable semantic tokens to tool references
38+
return undefined;
39+
}
40+
41+
const variableReferences = promptAST.body.variableReferences;
3642
if (!variableReferences.length) {
3743
return undefined;
3844
}

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js';
1616
import { IPromptsService } from '../service/promptsService.js';
1717
import { Iterable } from '../../../../../../base/common/iterator.js';
1818
import { PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js';
19-
import { getValidAttributeNames } from './promptValidator.js';
19+
import { getValidAttributeNames, isGithubTarget, knownGithubCopilotTools } from './promptValidator.js';
2020
import { localize } from '../../../../../../nls.js';
2121

2222
export class PromptHeaderAutocompletion implements CompletionItemProvider {
@@ -55,13 +55,13 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
5555
return undefined;
5656
}
5757

58-
const parser = this.promptsService.getParsedPromptFile(model);
59-
const header = parser.header;
58+
const parsedAST = this.promptsService.getParsedPromptFile(model);
59+
const header = parsedAST.header;
6060
if (!header) {
6161
return undefined;
6262
}
6363

64-
const headerRange = parser.header.range;
64+
const headerRange = parsedAST.header.range;
6565
if (position.lineNumber < headerRange.startLineNumber || position.lineNumber >= headerRange.endLineNumber) {
6666
// if the position is not inside the header, we don't provide any completions
6767
return undefined;
@@ -72,42 +72,45 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
7272
const colonPosition = colonIndex !== -1 ? new Position(position.lineNumber, colonIndex + 1) : undefined;
7373

7474
if (!colonPosition || position.isBeforeOrEqual(colonPosition)) {
75-
return this.providePropertyCompletions(model, position, headerRange, colonPosition, promptType);
75+
return this.provideAttributeNameCompletions(model, position, header, colonPosition, promptType);
7676
} else if (colonPosition && colonPosition.isBefore(position)) {
7777
return this.provideValueCompletions(model, position, header, colonPosition, promptType);
7878
}
7979
return undefined;
8080
}
81-
private async providePropertyCompletions(
81+
private async provideAttributeNameCompletions(
8282
model: ITextModel,
8383
position: Position,
84-
headerRange: Range,
84+
header: PromptHeader,
8585
colonPosition: Position | undefined,
8686
promptType: PromptsType,
8787
): Promise<CompletionList | undefined> {
8888

8989
const suggestions: CompletionItem[] = [];
90-
const supportedProperties = new Set(getValidAttributeNames(promptType, false));
91-
this.removeUsedProperties(supportedProperties, model, headerRange, position);
9290

93-
const getInsertText = (property: string): string => {
91+
const isGitHubTarget = isGithubTarget(promptType, header.target);
92+
const attributesToPropose = new Set(getValidAttributeNames(promptType, false, isGitHubTarget));
93+
for (const attr of header.attributes) {
94+
attributesToPropose.delete(attr.key);
95+
}
96+
const getInsertText = (key: string): string => {
9497
if (colonPosition) {
95-
return property;
98+
return key;
9699
}
97-
const valueSuggestions = this.getValueSuggestions(promptType, property);
100+
const valueSuggestions = this.getValueSuggestions(promptType, key);
98101
if (valueSuggestions.length > 0) {
99-
return `${property}: \${0:${valueSuggestions[0]}}`;
102+
return `${key}: \${0:${valueSuggestions[0]}}`;
100103
} else {
101-
return `${property}: \$0`;
104+
return `${key}: \$0`;
102105
}
103106
};
104107

105108

106-
for (const property of supportedProperties) {
109+
for (const attribute of attributesToPropose) {
107110
const item: CompletionItem = {
108-
label: property,
111+
label: attribute,
109112
kind: CompletionItemKind.Property,
110-
insertText: getInsertText(property),
113+
insertText: getInsertText(attribute),
111114
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
112115
range: new Range(position.lineNumber, 1, position.lineNumber, !colonPosition ? model.getLineMaxColumn(position.lineNumber) : colonPosition.column),
113116
};
@@ -127,28 +130,29 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
127130

128131
const suggestions: CompletionItem[] = [];
129132
const lineContent = model.getLineContent(position.lineNumber);
130-
const property = lineContent.substring(0, colonPosition.column - 1).trim();
133+
const attribute = lineContent.substring(0, colonPosition.column - 1).trim();
131134

132-
if (!getValidAttributeNames(promptType, true).includes(property)) {
135+
const isGitHubTarget = isGithubTarget(promptType, header.target);
136+
if (!getValidAttributeNames(promptType, true, isGitHubTarget).includes(attribute)) {
133137
return undefined;
134138
}
135139

136140
if (promptType === PromptsType.prompt || promptType === PromptsType.agent) {
137141
// if the position is inside the tools metadata, we provide tool name completions
138-
const result = this.provideToolCompletions(model, position, header);
142+
const result = this.provideToolCompletions(model, position, header, isGitHubTarget);
139143
if (result) {
140144
return result;
141145
}
142146
}
143147

144148
const bracketIndex = lineContent.indexOf('[');
145149
if (bracketIndex !== -1 && bracketIndex <= position.column - 1) {
146-
// if the property is already inside a bracket, we don't provide value completions
150+
// if the value is already inside a bracket, we don't provide value completions
147151
return undefined;
148152
}
149153

150154
const whilespaceAfterColon = (lineContent.substring(colonPosition.column).match(/^\s*/)?.[0].length) ?? 0;
151-
const values = this.getValueSuggestions(promptType, property);
155+
const values = this.getValueSuggestions(promptType, attribute);
152156
for (const value of values) {
153157
const item: CompletionItem = {
154158
label: value,
@@ -158,7 +162,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
158162
};
159163
suggestions.push(item);
160164
}
161-
if (property === PromptHeaderAttributes.handOffs && (promptType === PromptsType.agent)) {
165+
if (attribute === PromptHeaderAttributes.handOffs && (promptType === PromptsType.agent)) {
162166
const value = [
163167
'',
164168
' - label: Start Implementation',
@@ -177,39 +181,39 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
177181
return { suggestions };
178182
}
179183

180-
private removeUsedProperties(properties: Set<string>, model: ITextModel, headerRange: Range, position: Position): void {
181-
for (let i = headerRange.startLineNumber; i <= headerRange.endLineNumber; i++) {
182-
if (i !== position.lineNumber) {
183-
const lineText = model.getLineContent(i);
184-
const colonIndex = lineText.indexOf(':');
185-
if (colonIndex !== -1) {
186-
const property = lineText.substring(0, colonIndex).trim();
187-
properties.delete(property);
184+
private getValueSuggestions(promptType: string, attribute: string): string[] {
185+
switch (attribute) {
186+
case PromptHeaderAttributes.applyTo:
187+
if (promptType === PromptsType.instructions) {
188+
return [`'**'`, `'**/*.ts, **/*.js'`, `'**/*.php'`, `'**/*.py'`];
189+
}
190+
break;
191+
case PromptHeaderAttributes.agent:
192+
case PromptHeaderAttributes.mode:
193+
if (promptType === PromptsType.prompt) {
194+
// Get all available agents (builtin + custom)
195+
const agents = this.chatModeService.getModes();
196+
const suggestions: string[] = [];
197+
for (const agent of Iterable.concat(agents.builtin, agents.custom)) {
198+
suggestions.push(agent.name);
199+
}
200+
return suggestions;
201+
}
202+
case PromptHeaderAttributes.target:
203+
if (promptType === PromptsType.agent) {
204+
return ['vscode', 'github-copilot'];
205+
}
206+
break;
207+
case PromptHeaderAttributes.tools:
208+
if (promptType === PromptsType.prompt || promptType === PromptsType.agent) {
209+
return ['[]', `['search', 'edit', 'fetch']`];
210+
}
211+
break;
212+
case PromptHeaderAttributes.model:
213+
if (promptType === PromptsType.prompt || promptType === PromptsType.agent) {
214+
return this.getModelNames(promptType === PromptsType.agent);
188215
}
189-
}
190-
}
191-
}
192-
193-
private getValueSuggestions(promptType: string, property: string): string[] {
194-
if (promptType === PromptsType.instructions && property === PromptHeaderAttributes.applyTo) {
195-
return [`'**'`, `'**/*.ts, **/*.js'`, `'**/*.php'`, `'**/*.py'`];
196-
}
197-
if (promptType === PromptsType.prompt && (property === PromptHeaderAttributes.agent || property === PromptHeaderAttributes.mode)) {
198-
// Get all available agents (builtin + custom)
199-
const agents = this.chatModeService.getModes();
200-
const suggestions: string[] = [];
201-
for (const agent of Iterable.concat(agents.builtin, agents.custom)) {
202-
suggestions.push(agent.name);
203-
}
204-
return suggestions;
205-
}
206-
if (property === PromptHeaderAttributes.tools && (promptType === PromptsType.prompt || promptType === PromptsType.agent)) {
207-
return ['[]', `['search', 'edit', 'fetch']`];
208-
}
209-
if (property === PromptHeaderAttributes.model && (promptType === PromptsType.prompt || promptType === PromptsType.agent)) {
210-
return this.getModelNames(promptType === PromptsType.agent);
211216
}
212-
213217
return [];
214218
}
215219

@@ -226,14 +230,15 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
226230
return result;
227231
}
228232

229-
private provideToolCompletions(model: ITextModel, position: Position, header: PromptHeader): CompletionList | undefined {
233+
private provideToolCompletions(model: ITextModel, position: Position, header: PromptHeader, isGitHubTarget: boolean): CompletionList | undefined {
230234
const toolsAttr = header.getAttribute(PromptHeaderAttributes.tools);
231235
if (!toolsAttr || toolsAttr.value.type !== 'array' || !toolsAttr.range.containsPosition(position)) {
232236
return undefined;
233237
}
234238
const getSuggestions = (toolRange: Range) => {
235239
const suggestions: CompletionItem[] = [];
236-
for (const toolName of this.languageModelToolsService.getQualifiedToolNames()) {
240+
const toolNames = isGitHubTarget ? Object.keys(knownGithubCopilotTools) : this.languageModelToolsService.getQualifiedToolNames();
241+
for (const toolName of toolNames) {
237242
let insertText: string;
238243
if (!toolRange.isEmpty()) {
239244
const firstChar = model.getValueInRange(toolRange).charCodeAt(0);

0 commit comments

Comments
 (0)