@@ -52,6 +52,13 @@ export class PromptsService extends Disposable implements IPromptsService {
5252
5353 private parsedPromptFileCache = new ResourceMap < [ number , ParsedPromptFile ] > ( ) ;
5454
55+ /**
56+ * Cache for parsed prompt files keyed by command name.
57+ */
58+ private promptFileByCommandCache = new Map < string , { value : ParsedPromptFile | undefined ; pendingPromise : Promise < ParsedPromptFile | undefined > | undefined } > ( ) ;
59+
60+ private onDidChangeParsedPromptFilesCacheEmitter = new Emitter < void > ( ) ;
61+
5562 /**
5663 * Contributed files from extensions keyed by prompt type then name.
5764 */
@@ -79,8 +86,32 @@ export class PromptsService extends Disposable implements IPromptsService {
7986 ) {
8087 super ( ) ;
8188
89+ this . onDidChangeParsedPromptFilesCacheEmitter = this . _register ( new Emitter < void > ( ) ) ;
90+
8291 this . fileLocator = this . _register ( this . instantiationService . createInstance ( PromptFilesLocator ) ) ;
8392
93+ const promptUpdateTracker = this . _register ( new PromptUpdateTracker ( this . fileLocator , this . modelService ) ) ;
94+ this . _register ( promptUpdateTracker . onDiDPromptChange ( ( event ) => {
95+ if ( event . kind === 'fileSystem' ) {
96+ this . promptFileByCommandCache . clear ( ) ;
97+ }
98+ else {
99+ // Clear cache for prompt files that match the changed URI\
100+ const pendingDeletes : string [ ] = [ ] ;
101+ for ( const [ key , value ] of this . promptFileByCommandCache ) {
102+ if ( isEqual ( value . value ?. uri , event . uri ) ) {
103+ pendingDeletes . push ( key ) ;
104+ }
105+ }
106+
107+ for ( const key of pendingDeletes ) {
108+ this . promptFileByCommandCache . delete ( key ) ;
109+ }
110+ }
111+
112+ this . onDidChangeParsedPromptFilesCacheEmitter . fire ( ) ;
113+ } ) ) ;
114+
84115 this . _register ( this . modelService . onModelRemoved ( ( model ) => {
85116 this . parsedPromptFileCache . delete ( model . uri ) ;
86117 } ) ) ;
@@ -101,13 +132,16 @@ export class PromptsService extends Disposable implements IPromptsService {
101132 return this . onDidChangeCustomAgentsEmitter . event ;
102133 }
103134
135+ public get onDidChangeParsedPromptFilesCache ( ) : Event < void > {
136+ return this . onDidChangeParsedPromptFilesCacheEmitter . event ;
137+ }
138+
104139 public getPromptFileType ( uri : URI ) : PromptsType | undefined {
105140 const model = this . modelService . getModel ( uri ) ;
106141 const languageId = model ? model . getLanguageId ( ) : this . languageService . guessLanguageIdByFilepathOrFirstLine ( uri ) ;
107142 return languageId ? getPromptsTypeForLanguageId ( languageId ) : undefined ;
108143 }
109144
110-
111145 public getParsedPromptFile ( textModel : ITextModel ) : ParsedPromptFile {
112146 const cached = this . parsedPromptFileCache . get ( textModel . uri ) ;
113147 if ( cached && cached [ 0 ] === textModel . getVersionId ( ) ) {
@@ -187,26 +221,56 @@ export class PromptsService extends Disposable implements IPromptsService {
187221 }
188222
189223 public async resolvePromptSlashCommand ( data : IChatPromptSlashCommand , token : CancellationToken ) : Promise < ParsedPromptFile | undefined > {
190- const promptUri = await this . getPromptPath ( data ) ;
224+ const promptUri = data . promptPath ?. uri ?? await this . getPromptPath ( data . command ) ;
191225 if ( ! promptUri ) {
192226 return undefined ;
193227 }
228+
194229 try {
195230 return await this . parseNew ( promptUri , token ) ;
196231 } catch ( error ) {
197232 this . logger . error ( `[resolvePromptSlashCommand] Failed to parse prompt file: ${ promptUri } ` , error ) ;
198233 return undefined ;
199234 }
235+ }
236+
237+ private async populatePromptCommandCache ( command : string ) : Promise < ParsedPromptFile | undefined > {
238+ let cache = this . promptFileByCommandCache . get ( command ) ;
239+ if ( cache && cache . pendingPromise ) {
240+ return cache . pendingPromise ;
241+ }
242+
243+ const newPromise = this . resolvePromptSlashCommand ( { command, detail : '' } , CancellationToken . None ) ;
244+ if ( cache ) {
245+ cache . pendingPromise = newPromise ;
246+ }
247+ else {
248+ cache = { value : undefined , pendingPromise : newPromise } ;
249+ this . promptFileByCommandCache . set ( command , cache ) ;
250+ }
251+
252+ const newValue = await newPromise . finally ( ( ) => cache . pendingPromise = undefined ) ;
200253
254+ // TODO: consider comparing the newValue and the old and only emit change event when there are value changes
255+ cache . value = newValue ;
256+ this . onDidChangeParsedPromptFilesCacheEmitter . fire ( ) ;
257+
258+ return newValue ;
201259 }
202260
203- private async getPromptPath ( data : IChatPromptSlashCommand ) : Promise < URI | undefined > {
204- if ( data . promptPath ) {
205- return data . promptPath . uri ;
261+ public resolvePromptSlashCommandFromCache ( command : string ) : ParsedPromptFile | undefined {
262+ const cache = this . promptFileByCommandCache . get ( command ) ;
263+ const value = cache ?. value ;
264+ if ( value === undefined ) {
265+ // kick off a async process to refresh the cache while we returns the current cached value
266+ void this . populatePromptCommandCache ( command ) . catch ( ( error ) => { } ) ;
206267 }
207268
269+ return value ;
270+ }
271+
272+ private async getPromptPath ( command : string ) : Promise < URI | undefined > {
208273 const promptPaths = await this . listPromptFiles ( PromptsType . prompt , CancellationToken . None ) ;
209- const command = data . command ;
210274 const result = promptPaths . find ( promptPath => getCommandNameFromPromptPath ( promptPath ) === command ) ;
211275 if ( result ) {
212276 return result . uri ;
@@ -443,7 +507,63 @@ export class UpdateTracker extends Disposable {
443507 this . listeners . forEach ( listener => listener . dispose ( ) ) ;
444508 this . listeners . clear ( ) ;
445509 }
510+ }
511+
512+ export type PromptUpdateKind = 'fileSystem' | 'textModel' ;
513+
514+ export interface IPromptUpdateEvent {
515+ kind : PromptUpdateKind ;
516+ uri ?: URI ;
517+ }
518+
519+ export class PromptUpdateTracker extends Disposable {
520+
521+ private static readonly PROMPT_UPDATE_DELAY_MS = 200 ;
522+
523+ private readonly listeners = new ResourceMap < IDisposable > ( ) ;
524+ private readonly onDidPromptModelChange : Emitter < IPromptUpdateEvent > ;
525+
526+ public get onDiDPromptChange ( ) : Event < IPromptUpdateEvent > {
527+ return this . onDidPromptModelChange . event ;
528+ }
446529
530+ constructor (
531+ fileLocator : PromptFilesLocator ,
532+ @IModelService modelService : IModelService ,
533+ ) {
534+ super ( ) ;
535+ this . onDidPromptModelChange = this . _register ( new Emitter < IPromptUpdateEvent > ( ) ) ;
536+ const delayer = this . _register ( new Delayer < void > ( PromptUpdateTracker . PROMPT_UPDATE_DELAY_MS ) ) ;
537+ const trigger = ( event : IPromptUpdateEvent ) => delayer . trigger ( ( ) => this . onDidPromptModelChange . fire ( event ) ) ;
538+
539+ const filesUpdatedEventRegistration = this . _register ( fileLocator . createFilesUpdatedEvent ( PromptsType . prompt ) ) ;
540+ this . _register ( filesUpdatedEventRegistration . event ( ( ) => trigger ( { kind : 'fileSystem' } ) ) ) ;
541+
542+ const onAdd = ( model : ITextModel ) => {
543+ if ( model . getLanguageId ( ) === PROMPT_LANGUAGE_ID ) {
544+ this . listeners . set ( model . uri , model . onDidChangeContent ( ( ) => trigger ( { kind : 'textModel' , uri : model . uri } ) ) ) ;
545+ }
546+ } ;
547+ const onRemove = ( languageId : string , uri : URI ) => {
548+ if ( languageId === PROMPT_LANGUAGE_ID ) {
549+ this . listeners . get ( uri ) ?. dispose ( ) ;
550+ this . listeners . delete ( uri ) ;
551+ trigger ( { kind : 'textModel' , uri } ) ;
552+ }
553+ } ;
554+ this . _register ( modelService . onModelAdded ( model => onAdd ( model ) ) ) ;
555+ this . _register ( modelService . onModelLanguageChanged ( e => {
556+ onRemove ( e . oldLanguageId , e . model . uri ) ;
557+ onAdd ( e . model ) ;
558+ } ) ) ;
559+ this . _register ( modelService . onModelRemoved ( model => onRemove ( model . getLanguageId ( ) , model . uri ) ) ) ;
560+ }
561+
562+ public override dispose ( ) : void {
563+ super . dispose ( ) ;
564+ this . listeners . forEach ( listener => listener . dispose ( ) ) ;
565+ this . listeners . clear ( ) ;
566+ }
447567}
448568
449569namespace IAgentSource {
0 commit comments