Path: blob/main/extensions/copilot/src/extension/promptFileContext/vscode-node/promptFileContextService.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as vscode from 'vscode';67import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';8import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';9import { Copilot } from '../../../platform/inlineCompletions/common/api';10import { ILanguageContextProviderService, ProviderTarget } from '../../../platform/languageContextProvider/common/languageContextProviderService';11import { ILogService } from '../../../platform/log/common/logService';12import { PromptFileLangageId, PromptHeaderAttributes } from '../../../platform/promptFiles/common/promptsService';13import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';14import { Disposable, DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';15import { autorun, IObservable } from '../../../util/vs/base/common/observableInternal';1617export const promptFileSelector = [PromptFileLangageId.prompt, PromptFileLangageId.instructions, PromptFileLangageId.agent];1819export class PromptFileContextContribution extends Disposable {2021private readonly _enableCompletionContext: IObservable<boolean>;22private registration: Promise<IDisposable> | undefined;2324private models: string[] = ['GPT-4.1', 'GPT-4o'];2526constructor(27@IConfigurationService configurationService: IConfigurationService,28@ILogService private readonly logService: ILogService,29@IExperimentationService experimentationService: IExperimentationService,30@IEndpointProvider private readonly endpointProvider: IEndpointProvider,31@ILanguageContextProviderService private readonly languageContextProviderService: ILanguageContextProviderService,32) {33super();34this._enableCompletionContext = configurationService.getExperimentBasedConfigObservable(ConfigKey.Advanced.PromptFileContext, experimentationService);35this._register(autorun(reader => {36if (this._enableCompletionContext.read(reader)) {37this.registration = this.register();38} else if (this.registration) {39this.registration.then(disposable => disposable.dispose());40this.registration = undefined;41}42}));4344}4546override dispose() {47super.dispose();48if (this.registration) {49this.registration.then(disposable => disposable.dispose());50this.registration = undefined;51}52}5354private async register(): Promise<IDisposable> {55const disposables = new DisposableStore();56try {57const self = this;58const resolver: Copilot.ContextResolver<Copilot.SupportedContextItem> = {59async resolve(request: Copilot.ResolveRequest, token: vscode.CancellationToken): Promise<Copilot.SupportedContextItem[]> {60const [document, position] = self.getDocumentAndPosition(request, token);61if (document === undefined || position === undefined) {62return [];63}64const tokenBudget = self.getTokenBudget(document);65if (tokenBudget <= 0) {66return [];67}68return self.getContext(document.languageId);69}70};7172this.endpointProvider.getAllChatEndpoints().then(endpoints => {73const modelNames = new Set<string>();74for (const endpoint of endpoints) {75if (endpoint.showInModelPicker) {76modelNames.add(endpoint.name);77}78}79this.models = [...modelNames.keys()];80});8182const provider: Copilot.ContextProvider<Copilot.SupportedContextItem> = {83id: 'promptfile-ai-context-provider',84selector: promptFileSelector,85resolver: resolver86};87const copilotAPI = await this.getCopilotApi();88if (copilotAPI) {89disposables.add(copilotAPI.registerContextProvider(provider));90}91disposables.add(this.languageContextProviderService.registerContextProvider(provider, [ProviderTarget.NES, ProviderTarget.Completions]));92} catch (error) {93this.logService.error('Error regsistering prompt file context provider:', error);94}95return disposables;96}9798private getContext(languageId: string): Copilot.SupportedContextItem[] {99100101switch (languageId) {102case PromptFileLangageId.prompt: {103const toolNamesList = this.getToolNames().join(', ');104return [105{106name: 'This is a prompt file. It uses markdown with a YAML front matter header that only supports a limited set of attributes and values. Do not suggest any other attributes',107value: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.agent, PromptHeaderAttributes.model, PromptHeaderAttributes.tools].join(', '),108},109{110name: '`agent` is optional and must be one of the following values',111value: `ask, edit or agent`,112},113{114name: '`model` is optional and must be one of the following values',115value: this.models.join(', '),116},117{118name: '`tools` is optional and must be an array of one or more of the following values. Do not make up any other tool names.',119value: toolNamesList120},121{122name: 'Here is an example of a prompt file',123value: [124``,125'```md',126`---`,127`agent: agent`,128`description: This prompt is used to generate a new issue template for GitHub repositories.`,129`model: ${this.models[0] || 'GPT-4.1'}`,130`tools: [${toolNamesList}]`,131`---`,132`Generate a new issue template for a GitHub repository.`,133'```',134].join('\n'),135},136];137}138case PromptFileLangageId.instructions: {139return [140{141name: 'This is a instructions file. It uses markdown with a YAML front matter header that only supports a limited set of attributes and values. Do not suggest any other properties',142value: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo].join(', ')143},144{145name: '`applyTo` is one or more glob patterns that specify which files the instructions apply to',146value: `**`,147},148{149name: 'Here is an example of an instruction file',150value: [151``,152'```md',153`---`,154`description: This file describes the TypeScript code style for the project.`,155`applyTo: **/*.ts, **/*.js`,156`---`,157`For private fields, start the field name with an underscore (_).`,158'```',159].join('\n'),160},161];162}163case PromptFileLangageId.agent: {164const toolNamesList = this.getToolNames().join(', ');165return [166{167name: 'This is a custom agent file. It uses markdown with a YAML front matter header that only supports a limited set of attributes and values. Do not suggest any other attributes',168value: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.handOffs].join(', '),169},170{171name: '`model` is optional and must be one of the following values',172value: this.models.join(', '),173},174{175name: '`tools` is optional and must be an array of one or more of the following values. Do not make up any other tool names.',176value: `[${toolNamesList}]`,177},178{179name: '`target` is optional and must be one of the following values',180value: `vscode, github-copilot`,181},182{183name: '`handoffs` is optional and is a sequence of mappings with `label`, `agent`, `prompt`, `send`, and `model` properties. The `model` property uses the format `Model Name (vendor)` (e.g., `GPT-4.1 (copilot)`)',184value: [185`handoffs:`,186` - label: Start Implementation`,187` agent: agent`,188` prompt: Implement the plan`,189` send: true`,190` model: GPT-4.1 (copilot)`,191].join('\n'),192},193{194name: 'Here is an example of a custom agent file',195value: [196``,197'```md',198`---`,199`description: This custom agent researches and plans new features for VS Code extensions.`,200`model: GPT-4.1`,201`tools: [${toolNamesList}]`,202`handoffs:`,203` - label: Start Implementation`,204` agent: agent`,205` prompt: Implement the plan`,206` send: true`,207` model: GPT-4.1 (copilot)`,208`---`,209`First come up with a plan for the new feature. Write a todo list of tasks to complete the feature.`,210'```',211].join('\n'),212},213];214}215default:216return [];217}218}219220private getToolNames(): string[] {221return ['execute', 'read', 'edit', 'search', 'web', 'agent', 'todo'];222}223224225private async getCopilotApi(): Promise<Copilot.ContextProviderApiV1 | undefined> {226const copilotExtension = vscode.extensions.getExtension('GitHub.copilot');227if (copilotExtension === undefined) {228return undefined;229}230this.logService.info('Copilot extension found');231try {232const api = await copilotExtension.activate();233return api.getContextProviderAPI('v1');234} catch (error) {235if (error instanceof Error) {236this.logService.error('Error activating Copilot extension:', error.message);237} else {238this.logService.error('Error activating Copilot extension: Unknown error.');239}240return undefined;241}242}243244public getTokenBudget(document: vscode.TextDocument): number {245return Math.trunc((8 * 1024) - (document.getText().length / 4) - 256);246}247248private getDocumentAndPosition(request: Copilot.ResolveRequest, token?: vscode.CancellationToken): [vscode.TextDocument | undefined, vscode.Position | undefined] {249let document: vscode.TextDocument | undefined;250if (vscode.window.activeTextEditor?.document.uri.toString() === request.documentContext.uri) {251document = vscode.window.activeTextEditor.document;252} else {253document = vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === request.documentContext.uri);254}255if (document === undefined) {256return [undefined, undefined];257}258const requestPos = request.documentContext.position;259const position = requestPos !== undefined ? new vscode.Position(requestPos.line, requestPos.character) : document.positionAt(request.documentContext.offset);260if (document.version > request.documentContext.version) {261if (!token?.isCancellationRequested) {262}263return [undefined, undefined];264}265if (document.version < request.documentContext.version) {266return [undefined, undefined];267}268return [document, position];269}270271272273}274275276