44 *--------------------------------------------------------------------------------------------*/
55
66import { localize } from '../../../../../../nls.js' ;
7- import { getPromptsTypeForLanguageId , PROMPT_LANGUAGE_ID , PromptsType } from '../promptTypes.js' ;
7+ import { getPromptsTypeForLanguageId , MODE_LANGUAGE_ID , PROMPT_LANGUAGE_ID , PromptsType } from '../promptTypes.js' ;
88import { type URI } from '../../../../../../base/common/uri.js' ;
99import { basename } from '../../../../../../base/common/path.js' ;
1010import { PromptFilesLocator } from '../utils/promptFilesLocator.js' ;
11- import { Disposable } from '../../../../../../base/common/lifecycle.js' ;
12- import { Event } from '../../../../../../base/common/event.js' ;
11+ import { Disposable , IDisposable } from '../../../../../../base/common/lifecycle.js' ;
12+ import { Emitter , Event } from '../../../../../../base/common/event.js' ;
1313import { type ITextModel } from '../../../../../../editor/common/model.js' ;
1414import { ILogService } from '../../../../../../platform/log/common/log.js' ;
1515import { ILabelService } from '../../../../../../platform/label/common/label.js' ;
@@ -30,6 +30,7 @@ import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetR
3030import { IChatModeInstructions , IVariableReference } from '../../chatModes.js' ;
3131import { dirname } from '../../../../../../base/common/resources.js' ;
3232import { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js' ;
33+ import { Delayer } from '../../../../../../base/common/async.js' ;
3334
3435/**
3536 * Provides prompt services.
@@ -62,7 +63,7 @@ export class PromptsService extends Disposable implements IPromptsService {
6263 /**
6364 * Lazily created event that is fired when the custom chat modes change.
6465 */
65- private onDidChangeCustomChatModesEvent : Event < void > | undefined ;
66+ private onDidChangeCustomChatModesEmitter : Emitter < void > | undefined ;
6667
6768 constructor (
6869 @ILogService public readonly logger : ILogService ,
@@ -87,13 +88,15 @@ export class PromptsService extends Disposable implements IPromptsService {
8788 * Emitter for the custom chat modes change event.
8889 */
8990 public get onDidChangeCustomChatModes ( ) : Event < void > {
90- if ( ! this . onDidChangeCustomChatModesEvent ) {
91- this . onDidChangeCustomChatModesEvent = this . _register ( this . fileLocator . createFilesUpdatedEvent ( PromptsType . mode ) ) . event ;
92- this . _register ( this . onDidChangeCustomChatModesEvent ( ( ) => {
91+ if ( ! this . onDidChangeCustomChatModesEmitter ) {
92+ const emitter = this . onDidChangeCustomChatModesEmitter = this . _register ( new Emitter < void > ( ) ) ;
93+ const chatModelTracker = this . _register ( new ChatModeUpdateTracker ( this . fileLocator , this . modelService ) ) ;
94+ this . _register ( chatModelTracker . onDidChangeContent ( ( ) => {
9395 this . cachedCustomChatModes = undefined ; // reset cached custom chat modes
96+ emitter . fire ( ) ;
9497 } ) ) ;
9598 }
96- return this . onDidChangeCustomChatModesEvent ;
99+ return this . onDidChangeCustomChatModesEmitter . event ;
97100 }
98101
99102 public getPromptFileType ( uri : URI ) : PromptsType | undefined {
@@ -215,7 +218,7 @@ export class PromptsService extends Disposable implements IPromptsService {
215218 public async getCustomChatModes ( token : CancellationToken ) : Promise < readonly ICustomChatMode [ ] > {
216219 if ( ! this . cachedCustomChatModes ) {
217220 const customChatModes = this . computeCustomChatModes ( token ) ;
218- if ( ! this . onDidChangeCustomChatModesEvent ) {
221+ if ( ! this . onDidChangeCustomChatModesEmitter ) {
219222 return customChatModes ;
220223 }
221224 this . cachedCustomChatModes = customChatModes ;
@@ -227,7 +230,7 @@ export class PromptsService extends Disposable implements IPromptsService {
227230 const modeFiles = await this . listPromptFiles ( PromptsType . mode , token ) ;
228231
229232 const customChatModes = await Promise . all (
230- modeFiles . map ( async ( { uri } ) : Promise < ICustomChatMode > => {
233+ modeFiles . map ( async ( { uri, name : modeName } ) : Promise < ICustomChatMode > => {
231234 const ast = await this . parseNew ( uri , token ) ;
232235
233236 let metadata : any | undefined ;
@@ -259,7 +262,7 @@ export class PromptsService extends Disposable implements IPromptsService {
259262 metadata,
260263 } satisfies IChatModeInstructions ;
261264
262- const name = getCleanPromptName ( uri ) ;
265+ const name = modeName ?? getCleanPromptName ( uri ) ;
263266 if ( ! ast . header ) {
264267 return { uri, name, modeInstructions } ;
265268 }
@@ -290,12 +293,18 @@ export class PromptsService extends Disposable implements IPromptsService {
290293 return Disposable . None ;
291294 }
292295 bucket . set ( uri , { uri, name, description, storage : PromptsStorage . extension , type, extension } satisfies IExtensionPromptPath ) ;
293- if ( type === PromptsType . mode ) {
294- this . cachedCustomChatModes = undefined ;
295- }
296+
297+ const updateModesIfRequired = ( ) => {
298+ if ( type === PromptsType . mode ) {
299+ this . cachedCustomChatModes = undefined ;
300+ this . onDidChangeCustomChatModesEmitter ?. fire ( ) ;
301+ }
302+ } ;
303+ updateModesIfRequired ( ) ;
296304 return {
297305 dispose : ( ) => {
298306 bucket . delete ( uri ) ;
307+ updateModesIfRequired ( ) ;
299308 }
300309 } ;
301310 }
@@ -320,3 +329,54 @@ export function getPromptCommandName(path: string): string {
320329 const name = basename ( path , PROMPT_FILE_EXTENSION ) ;
321330 return name ;
322331}
332+
333+ export class ChatModeUpdateTracker extends Disposable {
334+
335+ private static readonly CHAT_MODE_UPDATE_DELAY_MS = 200 ;
336+
337+ private readonly listeners = new ResourceMap < IDisposable > ( ) ;
338+ private readonly onDidChatModeModelChange : Emitter < void > ;
339+
340+ public get onDidChangeContent ( ) : Event < void > {
341+ return this . onDidChatModeModelChange . event ;
342+ }
343+
344+ constructor (
345+ fileLocator : PromptFilesLocator ,
346+ @IModelService modelService : IModelService ,
347+ ) {
348+ super ( ) ;
349+ this . onDidChatModeModelChange = this . _register ( new Emitter < void > ( ) ) ;
350+ const delayer = this . _register ( new Delayer < void > ( ChatModeUpdateTracker . CHAT_MODE_UPDATE_DELAY_MS ) ) ;
351+ const trigger = ( ) => delayer . trigger ( ( ) => this . onDidChatModeModelChange . fire ( ) ) ;
352+
353+ const filesUpdatedEventRegistration = this . _register ( fileLocator . createFilesUpdatedEvent ( PromptsType . mode ) ) ;
354+ this . _register ( filesUpdatedEventRegistration . event ( ( ) => trigger ( ) ) ) ;
355+
356+ const onAdd = ( model : ITextModel ) => {
357+ if ( model . getLanguageId ( ) === MODE_LANGUAGE_ID ) {
358+ this . listeners . set ( model . uri , model . onDidChangeContent ( ( ) => trigger ( ) ) ) ;
359+ }
360+ } ;
361+ const onRemove = ( languageId : string , uri : URI ) => {
362+ if ( languageId === MODE_LANGUAGE_ID ) {
363+ this . listeners . get ( uri ) ?. dispose ( ) ;
364+ this . listeners . delete ( uri ) ;
365+ trigger ( ) ;
366+ }
367+ } ;
368+ this . _register ( modelService . onModelAdded ( model => onAdd ( model ) ) ) ;
369+ this . _register ( modelService . onModelLanguageChanged ( e => {
370+ onRemove ( e . oldLanguageId , e . model . uri ) ;
371+ onAdd ( e . model ) ;
372+ } ) ) ;
373+ this . _register ( modelService . onModelRemoved ( model => onRemove ( model . getLanguageId ( ) , model . uri ) ) ) ;
374+ }
375+
376+ public override dispose ( ) : void {
377+ super . dispose ( ) ;
378+ this . listeners . forEach ( listener => listener . dispose ( ) ) ;
379+ this . listeners . clear ( ) ;
380+ }
381+
382+ }
0 commit comments