1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License. See License.txt in the project root for license information.
4
+ *--------------------------------------------------------------------------------------------*/
5
+
6
+ import * as vscode from 'vscode' ;
7
+
8
+ import { ConfigKey , IConfigurationService } from '../../../platform/configuration/common/configurationService' ;
9
+ import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider' ;
10
+ import { Copilot } from '../../../platform/inlineCompletions/vscode-node/api' ;
11
+ import { ILogService } from '../../../platform/log/common/logService' ;
12
+ import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService' ;
13
+ import { Disposable , DisposableStore , IDisposable } from '../../../util/vs/base/common/lifecycle' ;
14
+ import { autorun , IObservable } from '../../../util/vs/base/common/observableInternal' ;
15
+
16
+ const promptFileSelector = [ 'prompt' , 'instructions' , 'chatmode' ] ;
17
+
18
+ export class PromptFileContextContribution extends Disposable {
19
+
20
+ private readonly _enableCompletionContext : IObservable < boolean > ;
21
+ private registration : Promise < IDisposable > | undefined ;
22
+
23
+ private models : string [ ] = [ 'GPT-4.1' , 'GPT-4o' ] ;
24
+
25
+ constructor (
26
+ @IConfigurationService configurationService : IConfigurationService ,
27
+ @ILogService private readonly logService : ILogService ,
28
+ @IExperimentationService experimentationService : IExperimentationService ,
29
+ @IEndpointProvider private readonly endpointProvider : IEndpointProvider ,
30
+ ) {
31
+ super ( ) ;
32
+ this . _enableCompletionContext = configurationService . getExperimentBasedConfigObservable ( ConfigKey . Internal . PromptFileContext , experimentationService ) ;
33
+ this . _register ( autorun ( reader => {
34
+ if ( this . _enableCompletionContext . read ( reader ) ) {
35
+ this . registration = this . register ( ) ;
36
+ } else if ( this . registration ) {
37
+ this . registration . then ( disposable => disposable . dispose ( ) ) ;
38
+ this . registration = undefined ;
39
+ }
40
+ } ) ) ;
41
+
42
+ }
43
+
44
+ override dispose ( ) {
45
+ super . dispose ( ) ;
46
+ if ( this . registration ) {
47
+ this . registration . then ( disposable => disposable . dispose ( ) ) ;
48
+ this . registration = undefined ;
49
+ }
50
+ }
51
+
52
+ private async register ( ) : Promise < IDisposable > {
53
+ const disposables = new DisposableStore ( ) ;
54
+ try {
55
+ const copilotAPI = await this . getCopilotApi ( ) ;
56
+ if ( copilotAPI === undefined ) {
57
+ this . logService . logger . warn ( 'Copilot API is undefined, unable to register context provider.' ) ;
58
+ return disposables ;
59
+ }
60
+ const self = this ;
61
+ const resolver : Copilot . ContextResolver < Copilot . SupportedContextItem > = {
62
+ async resolve ( request : Copilot . ResolveRequest , token : vscode . CancellationToken ) : Promise < Copilot . SupportedContextItem [ ] > {
63
+ const [ document , position ] = self . getDocumentAndPosition ( request , token ) ;
64
+ if ( document === undefined || position === undefined ) {
65
+ return [ ] ;
66
+ }
67
+ const tokenBudget = self . getTokenBudget ( document ) ;
68
+ if ( tokenBudget <= 0 ) {
69
+ return [ ] ;
70
+ }
71
+ return self . getContext ( document . languageId ) ;
72
+ }
73
+ } ;
74
+
75
+ this . endpointProvider . getAllChatEndpoints ( ) . then ( endpoints => {
76
+ const modelNames = new Set < string > ( ) ;
77
+ for ( const endpoint of endpoints ) {
78
+ if ( endpoint . showInModelPicker ) {
79
+ modelNames . add ( endpoint . name ) ;
80
+ }
81
+ }
82
+ this . models = [ ...modelNames . keys ( ) ] ;
83
+ } ) ;
84
+
85
+ disposables . add ( copilotAPI . registerContextProvider ( {
86
+ id : 'promptfile-ai-context-provider' ,
87
+ selector : promptFileSelector ,
88
+ resolver : resolver
89
+ } ) ) ;
90
+ } catch ( error ) {
91
+ this . logService . logger . error ( 'Error regsistering prompt file context provider:' , error ) ;
92
+ }
93
+ return disposables ;
94
+ }
95
+
96
+ private getContext ( languageId : string ) : Copilot . SupportedContextItem [ ] {
97
+
98
+ switch ( languageId ) {
99
+ case 'prompt' :
100
+ return [
101
+ {
102
+ name : 'This is a prompt file that uses a frontmatter header with the following fields' ,
103
+ value : `mode, description, model, tools` ,
104
+ } ,
105
+ {
106
+ name : '`mode` is optional and must be one of the following values' ,
107
+ value : `ask, edit or agent` ,
108
+ } ,
109
+ {
110
+ name : '`model` is optional and must be one of the following values' ,
111
+ value : this . models . join ( ', ' ) ,
112
+ } ,
113
+ {
114
+ name : '`tools` is optional and is an array that can consist of any number of the following values' ,
115
+ value : `'changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'findTestFiles', 'githubRepo', 'new', 'openSimpleBrowser', 'problems', 'runCommands', 'runNotebooks', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI'`
116
+ } ,
117
+ {
118
+ name : 'Here is an example of a prompt file:' ,
119
+ value : [
120
+ `---` ,
121
+ `mode: 'agent'` ,
122
+ `description: This prompt is used to generate a new issue template for GitHub repositories.` ,
123
+ `model: ${ this . models [ 0 ] || 'GPT-4.1' } ` ,
124
+ `tools: ['changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'findTestFiles', 'githubRepo', 'new', 'openSimpleBrowser', 'problems', 'runCommands', 'runNotebooks', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI']` ,
125
+ `---` ,
126
+ `Generate a new issue template for a GitHub repository.` ,
127
+ ] . join ( '\n' ) ,
128
+ } ,
129
+ ] ;
130
+ case 'instructions' :
131
+ return [
132
+ {
133
+ name : 'This is an instructions file that uses a frontmatter header with the following fields' ,
134
+ value : `description, applyTo` ,
135
+ } ,
136
+ {
137
+ name : '`applyTo` is one or more glob patterns that specify which files the instructions apply to' ,
138
+ value : `**` ,
139
+ } ,
140
+ {
141
+ name : 'Here is an example of a instruction file:' ,
142
+ value : [
143
+ `---` ,
144
+ `description: This file describes the TypeScript code style for the project.` ,
145
+ `applyTo: **/*.ts, **/*.js` ,
146
+ `---` ,
147
+ `For private fields, start the field name with an underscore (_).` ,
148
+ ] . join ( '\n' ) ,
149
+ } ,
150
+ ] ;
151
+ case 'chatmode' :
152
+ return [
153
+ {
154
+ name : 'This is an custom mode file that uses a frontmatter header with the following fields' ,
155
+ value : `description, model, tools` ,
156
+ } ,
157
+ {
158
+ name : '`model` is optional and must be one of the following values' ,
159
+ value : this . models . join ( ', ' ) ,
160
+ } ,
161
+ {
162
+ name : '`tools` is optional and is an array that can consist of any number of the following values' ,
163
+ value : `'changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'findTestFiles', 'githubRepo', 'new', 'openSimpleBrowser', 'problems', 'runCommands', 'runNotebooks', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI'`
164
+ } ,
165
+ {
166
+ name : 'Here is an example of a mode file:' ,
167
+ value : [
168
+ `---` ,
169
+ `description: This mode is used to plan a new feature.` ,
170
+ `model: GPT-4.1` ,
171
+ `tools: ['changes', 'codebase','extensions', 'fetch', 'findTestFiles', 'githubRepo', 'openSimpleBrowser', 'problems', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI']` ,
172
+ `---` ,
173
+ `First come up with a plan for the new feature. Write a todo list of tasks to complete the feature.` ,
174
+ ] . join ( '\n' ) ,
175
+ } ,
176
+ ] ;
177
+ default :
178
+ return [ ] ;
179
+ }
180
+ }
181
+
182
+
183
+ private async getCopilotApi ( ) : Promise < Copilot . ContextProviderApiV1 | undefined > {
184
+ const copilotExtension = vscode . extensions . getExtension ( 'GitHub.copilot' ) ;
185
+ if ( copilotExtension === undefined ) {
186
+ this . logService . logger . error ( 'Copilot extension not found' ) ;
187
+ return undefined ;
188
+ }
189
+ try {
190
+ const api = await copilotExtension . activate ( ) ;
191
+ return api . getContextProviderAPI ( 'v1' ) ;
192
+ } catch ( error ) {
193
+ if ( error instanceof Error ) {
194
+ this . logService . logger . error ( 'Error activating Copilot extension:' , error . message ) ;
195
+ } else {
196
+ this . logService . logger . error ( 'Error activating Copilot extension: Unknown error.' ) ;
197
+ }
198
+ return undefined ;
199
+ }
200
+ }
201
+
202
+ public getTokenBudget ( document : vscode . TextDocument ) : number {
203
+ return Math . trunc ( ( 8 * 1024 ) - ( document . getText ( ) . length / 4 ) - 256 ) ;
204
+ }
205
+
206
+ private getDocumentAndPosition ( request : Copilot . ResolveRequest , token ?: vscode . CancellationToken ) : [ vscode . TextDocument | undefined , vscode . Position | undefined ] {
207
+ let document : vscode . TextDocument | undefined ;
208
+ if ( vscode . window . activeTextEditor ?. document . uri . toString ( ) === request . documentContext . uri ) {
209
+ document = vscode . window . activeTextEditor . document ;
210
+ } else {
211
+ document = vscode . workspace . textDocuments . find ( ( doc ) => doc . uri . toString ( ) === request . documentContext . uri ) ;
212
+ }
213
+ if ( document === undefined ) {
214
+ return [ undefined , undefined ] ;
215
+ }
216
+ const requestPos = request . documentContext . position ;
217
+ const position = requestPos !== undefined ? new vscode . Position ( requestPos . line , requestPos . character ) : document . positionAt ( request . documentContext . offset ) ;
218
+ if ( document . version > request . documentContext . version ) {
219
+ if ( ! token ?. isCancellationRequested ) {
220
+ }
221
+ return [ undefined , undefined ] ;
222
+ }
223
+ if ( document . version < request . documentContext . version ) {
224
+ return [ undefined , undefined ] ;
225
+ }
226
+ return [ document , position ] ;
227
+ }
228
+
229
+
230
+
231
+ }
0 commit comments