33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6+ import { CancellationTokenSource } from '../../../../base/common/cancellation.js' ;
67import { Disposable } from '../../../../base/common/lifecycle.js' ;
78import { generateUuid } from '../../../../base/common/uuid.js' ;
9+ import { localize } from '../../../../nls.js' ;
10+ import { ILogService } from '../../../../platform/log/common/log.js' ;
811import { IWorkbenchContribution } from '../../../common/contributions.js' ;
912import { IChatDebugResolvedEventContent , IChatDebugService } from '../common/chatDebugService.js' ;
10- import { IPromptDiscoveryInfo , IPromptsService } from '../common/promptSyntax/service/promptsService.js' ;
13+ import { IChatAgentService } from '../common/participants/chatAgents.js' ;
14+ import { PromptsType } from '../common/promptSyntax/promptTypes.js' ;
15+ import { IHookDiscoveryInfo , IPromptDiscoveryInfo , IPromptsService } from '../common/promptSyntax/service/promptsService.js' ;
1116
1217/**
13- * Bridges {@link IPromptsService} discovery log events to {@link IChatDebugService}.
14- *
15- * This contribution listens for discovery events emitted by the prompts service
16- * and forwards them as debug log entries. It also registers a resolve provider
17- * so expanding a discovery event in the Agent Debug Logs shows the full file list.
18+ * Bridges prompt discovery information to {@link IChatDebugService}.
1819 */
1920export class PromptsDebugContribution extends Disposable implements IWorkbenchContribution {
2021
@@ -29,59 +30,74 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
2930 private readonly _discoveryEventDetails = new Map < string , IPromptDiscoveryInfo > ( ) ;
3031
3132 constructor (
32- @IPromptsService promptsService : IPromptsService ,
33+ @IPromptsService private readonly promptsService : IPromptsService ,
34+ @IChatAgentService chatAgentService : IChatAgentService ,
3335 @IChatDebugService chatDebugService : IChatDebugService ,
36+ @ILogService logService : ILogService ,
3437 ) {
3538 super ( ) ;
3639
3740 // Forward discovery log events to the debug service.
38- this . _register ( promptsService . onDidLogDiscovery ( entry => {
39- let eventId : string | undefined ;
40-
41- if ( entry . discoveryInfo ) {
42- eventId = generateUuid ( ) ;
43- this . _discoveryEventDetails . set ( eventId , entry . discoveryInfo ) ;
44-
45- // Evict oldest entries when the map exceeds the cap.
46- if ( this . _discoveryEventDetails . size > PromptsDebugContribution . MAX_DISCOVERY_DETAILS ) {
47- const first = this . _discoveryEventDetails . keys ( ) . next ( ) . value ;
48- if ( first !== undefined ) {
49- this . _discoveryEventDetails . delete ( first ) ;
41+ this . _register ( chatAgentService . onWillInvokeAgent ( async e => {
42+ const sessionResource = e . request . sessionResource ;
43+ const cts = new CancellationTokenSource ( ) ;
44+
45+ try {
46+ const discoveryInfos = await Promise . all ( [ PromptsType . agent , PromptsType . instructions , PromptsType . prompt , PromptsType . skill , PromptsType . hook ] . map ( type => this . promptsService . getDiscoveryInfo ( type , cts . token ) ) ) ;
47+ for ( const discoveryInfo of discoveryInfos ) {
48+ const { name, details } = this . getDiscoveryLogEntry ( discoveryInfo ) ;
49+ const eventId = generateUuid ( ) ;
50+
51+ this . _discoveryEventDetails . set ( eventId , discoveryInfo ) ;
52+
53+ // Evict oldest entries when the map exceeds the cap.
54+ if ( this . _discoveryEventDetails . size > PromptsDebugContribution . MAX_DISCOVERY_DETAILS ) {
55+ const first = this . _discoveryEventDetails . keys ( ) . next ( ) . value ;
56+ if ( first !== undefined ) {
57+ this . _discoveryEventDetails . delete ( first ) ;
58+ }
5059 }
51- }
52- }
5360
54- // Enrich details with file paths so they appear in the event
55- // payload (e.g. forwarded via onDidReceiveChatDebugEvent to the
56- // extension's JSONL file logger).
57- let details = entry . details ;
58- if ( entry . discoveryInfo ) {
59- const info = entry . discoveryInfo ;
60- const loaded = info . files
61- . filter ( f => f . status === 'loaded' )
62- . map ( f => f . promptPath . name ?? f . promptPath . uri . path . split ( '/' ) . pop ( ) ?? f . promptPath . uri . toString ( ) ) ;
63- const skipped = info . files . filter ( f => f . status === 'skipped' ) . map ( f => {
64- const label = f . promptPath . uri . toString ( ) ;
65- return f . skipReason ? `${ label } (${ f . skipReason } )` : label ;
66- } ) ;
67- const folders = info . sourceFolders ?. map ( sf => sf . uri . path ) ?? [ ] ;
68- const parts : string [ ] = [ ] ;
69- if ( details ) { parts . push ( details ) ; }
70- if ( loaded . length > 0 ) { parts . push ( `loaded: [${ truncateList ( loaded ) } ]` ) ; }
71- if ( skipped . length > 0 ) { parts . push ( `skipped: [${ truncateList ( skipped ) } ]` ) ; }
72- if ( folders . length > 0 ) { parts . push ( `folders: [${ truncateList ( folders ) } ]` ) ; }
73- details = parts . join ( ' | ' ) || undefined ;
61+ // Enrich details with file paths so they appear in the event
62+ // payload (e.g. forwarded via onDidReceiveChatDebugEvent to the
63+ // extension's JSONL file logger).
64+ const loaded = discoveryInfo . files
65+ . filter ( f => f . status === 'loaded' )
66+ . map ( f => f . promptPath . name ?? f . promptPath . uri . path . split ( '/' ) . pop ( ) ?? f . promptPath . uri . toString ( ) ) ;
67+ const skipped = discoveryInfo . files . filter ( f => f . status === 'skipped' ) . map ( f => {
68+ const label = f . promptPath . uri . toString ( ) ;
69+ return f . skipReason ? `${ label } (${ f . skipReason } )` : label ;
70+ } ) ;
71+ const folders = discoveryInfo . sourceFolders ?. map ( sf => sf . uri . path ) ?? [ ] ;
72+ const parts : string [ ] = [ ] ;
73+ if ( details ) {
74+ parts . push ( details ) ;
75+ }
76+ if ( loaded . length > 0 ) {
77+ parts . push ( `loaded: [${ truncateList ( loaded ) } ]` ) ;
78+ }
79+ if ( skipped . length > 0 ) {
80+ parts . push ( `skipped: [${ truncateList ( skipped ) } ]` ) ;
81+ }
82+ if ( folders . length > 0 ) {
83+ parts . push ( `folders: [${ truncateList ( folders ) } ]` ) ;
84+ }
85+ const newDetails = parts . join ( ' | ' ) || undefined ;
86+
87+ chatDebugService . log (
88+ sessionResource ,
89+ name ,
90+ newDetails ,
91+ undefined ,
92+ { id : eventId , category : 'discovery' } ,
93+ ) ;
94+ }
95+ } catch ( error ) {
96+ logService . error ( 'Error while logging prompt discovery info to chat debug service' , error ) ;
97+ } finally {
98+ cts . dispose ( ) ;
7499 }
75-
76- chatDebugService . log (
77- entry . sessionResource ,
78- entry . name ,
79- details ,
80- undefined ,
81- { id : eventId , category : entry . category } ,
82- ) ;
83100 } ) ) ;
84-
85101 // Register a resolve provider so expanding a discovery event
86102 // in the Agent Debug Logs shows the full file list.
87103 this . _register ( chatDebugService . registerProvider ( {
@@ -92,6 +108,59 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
92108 } ) ) ;
93109 }
94110
111+ private getDiscoveryLogEntry ( discoveryInfo : IPromptDiscoveryInfo ) : { readonly name : string ; readonly details ?: string } {
112+
113+ const durationInMillis = discoveryInfo . durationInMillis . toFixed ( 1 ) ;
114+ const loadedCount = discoveryInfo . files . filter ( file => file . status === 'loaded' ) . length ;
115+ const skippedCount = discoveryInfo . files . length - loadedCount ;
116+
117+ switch ( discoveryInfo . type ) {
118+ case PromptsType . prompt :
119+ return {
120+ name : localize ( 'promptsService.loadSlashCommands' , 'Load Slash Commands' ) ,
121+ details : loadedCount === 1
122+ ? localize ( 'promptsDebugContribution.resolvedSlashCommand' , 'Resolved {0} slash command in {1}ms' , loadedCount , durationInMillis )
123+ : localize ( 'promptsDebugContribution.resolvedSlashCommands' , 'Resolved {0} slash commands in {1}ms' , loadedCount , durationInMillis )
124+ } ;
125+ case PromptsType . agent :
126+ return {
127+ name : localize ( 'promptsService.loadAgents' , 'Load Agents' ) ,
128+ details : loadedCount === 1
129+ ? localize ( 'promptsDebugContribution.resolvedAgent' , 'Resolved {0} agent in {1}ms' , loadedCount , durationInMillis )
130+ : localize ( 'promptsDebugContribution.resolvedAgents' , 'Resolved {0} agents in {1}ms' , loadedCount , durationInMillis )
131+ } ;
132+ case PromptsType . skill :
133+ return {
134+ name : localize ( 'promptsService.loadSkills' , 'Load Skills' ) ,
135+ details : loadedCount === 1
136+ ? localize ( 'promptsDebugContribution.resolvedSkill' , 'Resolved {0} skill in {1}ms' , loadedCount , durationInMillis )
137+ : localize ( 'promptsDebugContribution.resolvedSkills' , 'Resolved {0} skills in {1}ms' , loadedCount , durationInMillis )
138+ } ;
139+ case PromptsType . instructions :
140+ return {
141+ name : localize ( 'promptsService.loadInstructions' , 'Load Instructions' ) ,
142+ details : loadedCount === 1
143+ ? localize ( 'promptsDebugContribution.resolvedInstruction' , 'Resolved {0} instruction in {1}ms' , loadedCount , durationInMillis )
144+ : localize ( 'promptsDebugContribution.resolvedInstructions' , 'Resolved {0} instructions in {1}ms' , loadedCount , durationInMillis )
145+ } ;
146+ case PromptsType . hook : {
147+ const hookDiscoveryInfo = discoveryInfo as IHookDiscoveryInfo ;
148+ const hookCount = hookDiscoveryInfo . hooksInfo
149+ ? Object . values ( hookDiscoveryInfo . hooksInfo . hooks ) . reduce ( ( total , hooks ) => total + hooks . length , 0 )
150+ : loadedCount ;
151+ const details = skippedCount > 0
152+ ? localize ( 'promptsDebugContribution.resolvedHooksWithSkipped' , 'Resolved {0} hooks from {1} files in {2}ms, skipped {3}' , hookCount , loadedCount , durationInMillis , skippedCount )
153+ : hookCount === 1
154+ ? localize ( 'promptsDebugContribution.resolvedHook' , 'Resolved {0} hook in {1}ms' , hookCount , durationInMillis )
155+ : localize ( 'promptsDebugContribution.resolvedHooks' , 'Resolved {0} hooks in {1}ms' , hookCount , durationInMillis ) ;
156+ return {
157+ name : localize ( 'promptsService.loadHooks' , 'Load Hooks' ) ,
158+ details
159+ } ;
160+ }
161+ }
162+ }
163+
95164 private _resolveDiscoveryEvent ( eventId : string ) : IChatDebugResolvedEventContent | undefined {
96165 const info = this . _discoveryEventDetails . get ( eventId ) ;
97166 if ( ! info ) {
@@ -101,6 +170,7 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
101170 return {
102171 kind : 'fileList' ,
103172 discoveryType : info . type ,
173+ durationInMillis : info . durationInMillis ,
104174 files : info . files . map ( f => ( {
105175 uri : f . promptPath . uri ,
106176 name : f . promptPath . name ,
@@ -129,5 +199,6 @@ function truncateList(items: string[]): string {
129199 if ( items . length <= MAX_LIST_ITEMS ) {
130200 return items . join ( ', ' ) ;
131201 }
202+
132203 return items . slice ( 0 , MAX_LIST_ITEMS ) . join ( ', ' ) + ` (+${ items . length - MAX_LIST_ITEMS } more)` ;
133204}
0 commit comments