@theia/ai-core 1.61.0 → 1.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/lib/browser/ai-core-frontend-module.js +2 -2
  2. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  3. package/lib/browser/frontend-language-model-service.d.ts +1 -1
  4. package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
  5. package/lib/browser/frontend-language-model-service.js.map +1 -1
  6. package/lib/browser/frontend-prompt-customization-service.d.ts +142 -48
  7. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  8. package/lib/browser/frontend-prompt-customization-service.js +452 -153
  9. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  10. package/lib/browser/prompttemplate-contribution.d.ts +1 -2
  11. package/lib/browser/prompttemplate-contribution.d.ts.map +1 -1
  12. package/lib/browser/prompttemplate-contribution.js +5 -9
  13. package/lib/browser/prompttemplate-contribution.js.map +1 -1
  14. package/lib/common/agent-service.d.ts.map +1 -1
  15. package/lib/common/agent-service.js +14 -2
  16. package/lib/common/agent-service.js.map +1 -1
  17. package/lib/common/agent.d.ts +8 -3
  18. package/lib/common/agent.d.ts.map +1 -1
  19. package/lib/common/agent.js.map +1 -1
  20. package/lib/common/index.d.ts +0 -1
  21. package/lib/common/index.d.ts.map +1 -1
  22. package/lib/common/index.js +0 -1
  23. package/lib/common/index.js.map +1 -1
  24. package/lib/common/language-model-interaction-model.d.ts +74 -0
  25. package/lib/common/language-model-interaction-model.d.ts.map +1 -0
  26. package/lib/common/language-model-interaction-model.js +3 -0
  27. package/lib/common/language-model-interaction-model.js.map +1 -0
  28. package/lib/common/language-model-service.d.ts +26 -3
  29. package/lib/common/language-model-service.d.ts.map +1 -1
  30. package/lib/common/language-model-service.js +83 -6
  31. package/lib/common/language-model-service.js.map +1 -1
  32. package/lib/common/language-model-util.d.ts +3 -2
  33. package/lib/common/language-model-util.d.ts.map +1 -1
  34. package/lib/common/language-model-util.js +8 -0
  35. package/lib/common/language-model-util.js.map +1 -1
  36. package/lib/common/language-model.d.ts +25 -2
  37. package/lib/common/language-model.d.ts.map +1 -1
  38. package/lib/common/language-model.js +3 -1
  39. package/lib/common/language-model.js.map +1 -1
  40. package/lib/common/prompt-service.d.ts +332 -126
  41. package/lib/common/prompt-service.d.ts.map +1 -1
  42. package/lib/common/prompt-service.js +363 -102
  43. package/lib/common/prompt-service.js.map +1 -1
  44. package/lib/common/prompt-service.spec.js +104 -114
  45. package/lib/common/prompt-service.spec.js.map +1 -1
  46. package/lib/common/prompt-variable-contribution.d.ts +1 -2
  47. package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
  48. package/lib/common/prompt-variable-contribution.js +17 -26
  49. package/lib/common/prompt-variable-contribution.js.map +1 -1
  50. package/package.json +10 -10
  51. package/src/browser/ai-core-frontend-module.ts +4 -4
  52. package/src/browser/frontend-language-model-service.ts +1 -1
  53. package/src/browser/frontend-prompt-customization-service.ts +574 -183
  54. package/src/browser/prompttemplate-contribution.ts +6 -9
  55. package/src/common/agent-service.ts +14 -4
  56. package/src/common/agent.ts +9 -3
  57. package/src/common/index.ts +0 -1
  58. package/src/common/language-model-interaction-model.ts +98 -0
  59. package/src/common/language-model-service.ts +115 -6
  60. package/src/common/language-model-util.ts +10 -2
  61. package/src/common/language-model.ts +28 -2
  62. package/src/common/prompt-service.spec.ts +108 -114
  63. package/src/common/prompt-service.ts +694 -221
  64. package/src/common/prompt-variable-contribution.ts +22 -27
  65. package/lib/common/communication-recording-service.d.ts +0 -30
  66. package/lib/common/communication-recording-service.d.ts.map +0 -1
  67. package/lib/common/communication-recording-service.js +0 -20
  68. package/lib/common/communication-recording-service.js.map +0 -1
  69. package/src/common/communication-recording-service.ts +0 -55
@@ -17,7 +17,7 @@
17
17
  import { DisposableCollection, URI, Event, Emitter } from '@theia/core';
18
18
  import { OpenerService } from '@theia/core/lib/browser';
19
19
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
20
- import { PromptCustomizationService, CustomAgentDescription } from '../common';
20
+ import { PromptFragmentCustomizationService, CustomAgentDescription, CustomizedPromptFragment } from '../common';
21
21
  import { BinaryBuffer } from '@theia/core/lib/common/buffer';
22
22
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
23
23
  import { FileChangesEvent } from '@theia/filesystem/lib/common/files';
@@ -26,49 +26,97 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
26
26
  import { load, dump } from 'js-yaml';
27
27
  import { PROMPT_TEMPLATE_EXTENSION } from './prompttemplate-contribution';
28
28
 
29
- const templateEntry = {
29
+ /**
30
+ * Default template entry for creating custom agents
31
+ */
32
+ const newCustomAgentEntry = {
30
33
  id: 'my_agent',
31
34
  name: 'My Agent',
32
35
  description: 'This is an example agent. Please adapt the properties to fit your needs.',
33
- prompt: 'You are an example agent. Be nice and helpful to the user.',
36
+ prompt: `{{!-- Note: The context section below will resolve all context elements (e.g. files) to their full content
37
+ in the system prompt. Context elements can be added by the user in the default chat view (e.g. via DnD or the "+" button).
38
+ If you want a more fine-grained, on demand resolvement of context elements, you can also resolve files to their paths only
39
+ and equip the agent with functions so that the LLM can retrieve files on demand. See the Coder Agent prompt for an example.--}}
40
+
41
+ # Role
42
+ You are an example agent. Be nice and helpful to the user.
43
+
44
+ ## Current Context
45
+ Some files and other pieces of data may have been added by the user to the context of the chat. If any have, the details can be found below.
46
+ {{contextDetails}}`,
34
47
  defaultLLM: 'openai/gpt-4o'
35
48
  };
36
49
 
37
- // Template source priorities (higher number = higher priority)
38
- // Higher priority will "override" lower priority templates
39
- const enum TemplatePriority {
40
- GLOBAL_TEMPLATE_DIR = 1,
41
- ADDITIONAL_TEMPLATE_DIR = 2,
42
- TEMPLATE_FILE = 3
50
+ export enum CustomizationSource {
51
+ CUSTOMIZED = 1,
52
+ FOLDER = 2,
53
+ FILE = 3,
54
+ }
55
+
56
+ export function getCustomizationSourceString(origin: CustomizationSource): string {
57
+ switch (origin) {
58
+ case CustomizationSource.FILE:
59
+ return 'Workspace Template Files';
60
+ case CustomizationSource.FOLDER:
61
+ return 'Workspace Template Directories';
62
+ default:
63
+ return 'Prompt Templates Folder';
64
+ }
43
65
  }
44
66
 
45
67
  /**
46
- * Interface defining properties that can be updated in the service
68
+ * Interface defining properties that can be updated in the customization service
47
69
  */
48
- export interface PromptCustomizationProperties {
70
+ export interface PromptFragmentCustomizationProperties {
49
71
  /** Array of directory paths to load templates from */
50
72
  directoryPaths?: string[];
73
+
51
74
  /** Array of file paths to treat as templates */
52
75
  filePaths?: string[];
76
+
53
77
  /** Array of file extensions to consider as template files */
54
78
  extensions?: string[];
55
79
  }
56
80
 
57
- interface TemplateEntry {
58
- content: string;
59
- priority: number;
81
+ /**
82
+ * Internal representation of a fragment entry in the customization service
83
+ */
84
+ interface PromptFragmentCustomization {
85
+ /** The template content */
86
+ template: string;
87
+
88
+ /** Source URI where this template is stored */
60
89
  sourceUri: string;
90
+
91
+ /** Source type of the customization */
92
+ origin: CustomizationSource;
93
+
94
+ /** Priority level (higher values override lower ones) */
95
+ priority: number;
96
+
97
+ /** Fragment ID */
61
98
  id: string;
99
+
100
+ /** Unique customization ID */
101
+ customizationId: string;
62
102
  }
63
103
 
104
+ /**
105
+ * Information about a template file being watched for changes
106
+ */
64
107
  interface WatchedFileInfo {
108
+ /** The URI of the watched file */
65
109
  uri: URI;
66
- templateId: string;
110
+
111
+ /** The fragment ID associated with this file */
112
+ fragmentId: string;
113
+
114
+ /** The customization ID for this file */
115
+ customizationId: string;
67
116
  }
68
117
 
69
118
  @injectable()
70
- export class FrontendPromptCustomizationServiceImpl implements PromptCustomizationService {
71
-
119
+ export class DefaultPromptFragmentCustomizationService implements PromptFragmentCustomizationService {
72
120
  @inject(EnvVariablesServer)
73
121
  protected readonly envVariablesServer: EnvVariablesServer;
74
122
 
@@ -83,32 +131,36 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
83
131
 
84
132
  /** Stores URI strings of template files from directories currently being monitored for changes. */
85
133
  protected trackedTemplateURIs = new Set<string>();
86
- /** Contains the currently active templates, mapped by template ID. */
87
- protected effectiveTemplates = new Map<string, TemplateEntry>();
88
- /** Tracks all loaded templates, including overridden ones, mapped by source URI. */
89
- protected allLoadedTemplates = new Map<string, TemplateEntry>();
134
+
135
+ /** Contains the currently active customization, mapped by prompt fragment ID. */
136
+ protected activeCustomizations = new Map<string, PromptFragmentCustomization>();
137
+
138
+ /** Tracks all loaded customizations, including overridden ones, mapped by source URI. */
139
+ protected allCustomizations = new Map<string, PromptFragmentCustomization>();
140
+
90
141
  /** Stores additional directory paths for loading template files. */
91
142
  protected additionalTemplateDirs = new Set<string>();
143
+
92
144
  /** Contains file extensions that identify prompt template files. */
93
145
  protected templateExtensions = new Set<string>([PROMPT_TEMPLATE_EXTENSION]);
94
- /** Stores specific file paths that should be treated as templates. */
95
- protected templateFiles = new Set<string>();
146
+
147
+ /** Stores specific file paths, provided by the settings, that should be treated as templates. */
148
+ protected workspaceTemplateFiles = new Set<string>();
149
+
96
150
  /** Maps URI strings to WatchedFileInfo objects for individually watched template files. */
97
151
  protected watchedFiles = new Map<string, WatchedFileInfo>();
152
+
98
153
  /** Collection of disposable resources for cleanup when the service updates or is disposed. */
99
154
  protected toDispose = new DisposableCollection();
100
155
 
101
- private readonly onDidChangePromptEmitter = new Emitter<string>();
102
- readonly onDidChangePrompt: Event<string> = this.onDidChangePromptEmitter.event;
156
+ protected readonly onDidChangePromptFragmentCustomizationEmitter = new Emitter<string[]>();
157
+ readonly onDidChangePromptFragmentCustomization: Event<string[]> = this.onDidChangePromptFragmentCustomizationEmitter.event;
103
158
 
104
- private readonly onDidChangeCustomAgentsEmitter = new Emitter<void>();
105
- readonly onDidChangeCustomAgents = this.onDidChangeCustomAgentsEmitter.event;
159
+ protected readonly onDidChangeCustomAgentsEmitter = new Emitter<void>();
160
+ readonly onDidChangeCustomAgents: Event<void> = this.onDidChangeCustomAgentsEmitter.event;
106
161
 
107
162
  @postConstruct()
108
163
  protected init(): void {
109
- // Ensure PROMPT_TEMPLATE_EXTENSION is always included in templateExtensions as a default
110
- this.templateExtensions.add(PROMPT_TEMPLATE_EXTENSION);
111
-
112
164
  this.preferences.onPreferenceChanged(event => {
113
165
  if (event.preferenceName === PREFERENCE_NAME_PROMPT_TEMPLATES) {
114
166
  this.update();
@@ -117,147 +169,210 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
117
169
  this.update();
118
170
  }
119
171
 
172
+ /**
173
+ * Updates the service by reloading all template files and watching for changes
174
+ */
120
175
  protected async update(): Promise<void> {
121
176
  this.toDispose.dispose();
122
177
  // we need to assign local variables, so that updates running in parallel don't interfere with each other
123
- const templatesMap = new Map<string, TemplateEntry>();
124
- const trackedURIs = new Set<string>();
125
- const loadedTemplates = new Map<string, TemplateEntry>();
126
- const watchedFilesMap = new Map<string, WatchedFileInfo>();
178
+ const activeCustomizationsCopy = new Map<string, PromptFragmentCustomization>();
179
+ const trackedTemplateURIsCopy = new Set<string>();
180
+ const allCustomizationsCopy = new Map<string, PromptFragmentCustomization>();
181
+ const watchedFilesCopy = new Map<string, WatchedFileInfo>();
127
182
 
128
183
  // Process in order of priority (lowest to highest)
129
- // First process the main template directory (lowest priority)
130
- const templateURI = await this.getTemplatesDirectoryURI();
131
- await this.processTemplateDirectory(templatesMap, trackedURIs, loadedTemplates, templateURI, TemplatePriority.GLOBAL_TEMPLATE_DIR);
184
+ // First process the main templates directory (lowest priority)
185
+ const templatesURI = await this.getTemplatesDirectoryURI();
186
+ await this.processTemplateDirectory(
187
+ activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, templatesURI, 1, CustomizationSource.CUSTOMIZED); // Priority 1 for customized fragments
132
188
 
133
189
  // Process additional template directories (medium priority)
134
190
  for (const dirPath of this.additionalTemplateDirs) {
135
191
  const dirURI = URI.fromFilePath(dirPath);
136
- await this.processTemplateDirectory(templatesMap, trackedURIs, loadedTemplates, dirURI, TemplatePriority.ADDITIONAL_TEMPLATE_DIR);
192
+ await this.processTemplateDirectory(
193
+ activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, 2, CustomizationSource.FOLDER); // Priority 2 for folder fragments
137
194
  }
138
195
 
139
196
  // Process specific template files (highest priority)
140
- await this.processTemplateFiles(templatesMap, trackedURIs, loadedTemplates, watchedFilesMap);
197
+ await this.processTemplateFiles(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, watchedFilesCopy);
141
198
 
142
- this.effectiveTemplates = templatesMap;
143
- this.trackedTemplateURIs = trackedURIs;
144
- this.allLoadedTemplates = loadedTemplates;
145
- this.watchedFiles = watchedFilesMap;
199
+ this.activeCustomizations = activeCustomizationsCopy;
200
+ this.trackedTemplateURIs = trackedTemplateURIsCopy;
201
+ this.allCustomizations = allCustomizationsCopy;
202
+ this.watchedFiles = watchedFilesCopy;
146
203
 
147
204
  this.onDidChangeCustomAgentsEmitter.fire();
148
205
  }
149
206
 
150
207
  /**
151
- * Adds a template to the templates map, handling conflicts based on priority
152
- * @param templatesMap The map to add the template to
153
- * @param id The template ID
154
- * @param content The template content
155
- * @param priority The template priority
208
+ * Adds a template to the customizations map, handling conflicts based on priority
209
+ * @param activeCustomizationsCopy The map to add the customization to
210
+ * @param id The fragment ID
211
+ * @param template The template content
156
212
  * @param sourceUri The URI of the source file (used to distinguish updates from conflicts)
157
- * @param loadedTemplates The map to track all loaded templates
213
+ * @param allCustomizationsCopy The map to track all loaded customizations
214
+ * @param priority The customization priority
215
+ * @param origin The source type of the customization
158
216
  */
159
217
  protected addTemplate(
160
- templatesMap: Map<string, TemplateEntry>,
218
+ activeCustomizationsCopy: Map<string, PromptFragmentCustomization>,
161
219
  id: string,
162
- content: string,
163
- priority: number,
220
+ template: string,
164
221
  sourceUri: string,
165
- loadedTemplates: Map<string, TemplateEntry>
222
+ allCustomizationsCopy: Map<string, PromptFragmentCustomization>,
223
+ priority: number,
224
+ origin: CustomizationSource
166
225
  ): void {
167
- // Always add to loadedTemplates to keep track of all templates including overridden ones
226
+ // Generate a unique customization ID based on source URI and priority
227
+ const customizationId = this.generateCustomizationId(id, sourceUri);
228
+
229
+ // Always add to allCustomizationsCopy to keep track of all customizations including overridden ones
168
230
  if (sourceUri) {
169
- loadedTemplates.set(sourceUri, { id, content, priority, sourceUri });
231
+ allCustomizationsCopy.set(sourceUri, { id, template, sourceUri, priority, customizationId, origin });
170
232
  }
171
233
 
172
- const existingEntry = templatesMap.get(id);
234
+ const existingEntry = activeCustomizationsCopy.get(id);
173
235
 
174
236
  if (existingEntry) {
175
237
  // If this is an update to the same file (same source URI)
176
238
  if (sourceUri && existingEntry.sourceUri === sourceUri) {
177
239
  // Update the content while keeping the same priority and source
178
- templatesMap.set(id, { id, content, priority, sourceUri });
240
+ activeCustomizationsCopy.set(id, { id, template, sourceUri, priority, customizationId, origin });
179
241
  return;
180
242
  }
181
243
 
182
- // If the new template has higher priority, replace the existing one
244
+ // If the new customization has higher priority, replace the existing one
183
245
  if (priority > existingEntry.priority) {
184
- templatesMap.set(id, { id, content, priority, sourceUri });
246
+ activeCustomizationsCopy.set(id, { id, template, sourceUri, priority, customizationId, origin });
185
247
  return;
186
248
  } else if (priority === existingEntry.priority) {
187
- // There is a conflict with the same priority, we ignore the new template
249
+ // There is a conflict with the same priority, we ignore the new customization
188
250
  const conflictSourceUri = existingEntry.sourceUri ? ` (Existing source: ${existingEntry.sourceUri}, New source: ${sourceUri})` : '';
189
- console.warn(`Template conflict detected for ID '${id}' with equal priority.${conflictSourceUri}`);
251
+ console.warn(`Fragment conflict detected for ID '${id}' with equal priority.${conflictSourceUri}`);
190
252
  }
191
253
  return;
192
254
  }
193
255
 
194
- // No conflict at all, add the template
195
- templatesMap.set(id, { id, content, priority, sourceUri });
256
+ // No conflict at all, add the customization
257
+ activeCustomizationsCopy.set(id, { id, template, sourceUri, priority, customizationId, origin });
196
258
  }
197
259
 
198
260
  /**
199
- * Removes a template from the templates map based on the source URI and fires change event.
200
- * Also checks for any lower-priority templates with the same ID that might need to be loaded.
261
+ * Generates a unique customization ID based on the fragment ID, source URI, and priority
262
+ * @param id The fragment ID
263
+ * @param sourceUri The source URI of the template
264
+ * @returns A unique customization ID
265
+ */
266
+ protected generateCustomizationId(id: string, sourceUri: string): string {
267
+ // Create a customization ID that contains information about the source and priority
268
+ // This ensures uniqueness across different customization sources
269
+ const sourceHash = this.hashString(sourceUri);
270
+ return `${id}_${sourceHash}`;
271
+ }
272
+
273
+ /**
274
+ * Simple hash function to generate a short identifier from a string
275
+ * @param str The string to hash
276
+ * @returns A string hash
277
+ */
278
+ protected hashString(str: string): string {
279
+ let hash = 0;
280
+ for (let i = 0; i < str.length; i++) {
281
+ const char = str.charCodeAt(i);
282
+ hash = ((hash << 5) - hash) + char;
283
+ hash = hash & hash; // Convert to 32bit integer
284
+ }
285
+ return Math.abs(hash).toString(36).substring(0, 8);
286
+ }
287
+
288
+ /**
289
+ * Removes a customization from customizations maps based on the source URI.
290
+ * Also checks for any lower-priority customizations with the same ID that might need to be loaded.
201
291
  * @param sourceUri The URI of the source file being removed
202
- * @param loadedTemplates The map of all loaded templates
292
+ * @param allCustomizationsCopy The map of all loaded customizations
293
+ * @param activeCustomizationsCopy The map of active customizations
294
+ * @param trackedTemplateURIsCopy Optional set of tracked URIs to update
295
+ * @returns The fragment ID that was removed, or undefined if no customization was found
203
296
  */
204
- protected removeTemplateBySourceUri(
297
+ protected removeCustomizationFromMaps(
205
298
  sourceUri: string,
206
- loadedTemplates: Map<string, TemplateEntry>
207
- ): void {
208
- // Get the template entry from loadedTemplates
209
- const removedTemplate = loadedTemplates.get(sourceUri);
210
- if (!removedTemplate) {
211
- return;
299
+ allCustomizationsCopy: Map<string, PromptFragmentCustomization>,
300
+ activeCustomizationsCopy: Map<string, PromptFragmentCustomization>,
301
+ trackedTemplateURIsCopy: Set<string>
302
+ ): string | undefined {
303
+ // Get the customization entry from allCustomizationsCopy
304
+ const removedCustomization = allCustomizationsCopy.get(sourceUri);
305
+ if (!removedCustomization) {
306
+ return undefined;
212
307
  }
213
- const templateId = removedTemplate.id;
214
- loadedTemplates.delete(sourceUri);
215
-
216
- // If the template is in the active templates map, we check if there is another template previously conflicting with it
217
- const activeTemplate = this.effectiveTemplates.get(templateId);
218
- if (activeTemplate && activeTemplate.sourceUri === sourceUri) {
219
- this.effectiveTemplates.delete(templateId);
220
- // Find any lower-priority templates with the same ID that were previously ignored
221
- const lowerPriorityTemplates = Array.from(loadedTemplates.values())
222
- .filter(t => t.id === templateId)
308
+ const fragmentId = removedCustomization.id;
309
+ allCustomizationsCopy.delete(sourceUri);
310
+ trackedTemplateURIsCopy.delete(sourceUri);
311
+
312
+ // If the customization is in the active customizations map, we check if there is another customization previously conflicting with it
313
+ const activeCustomization = activeCustomizationsCopy.get(fragmentId);
314
+ if (activeCustomization && activeCustomization.sourceUri === sourceUri) {
315
+ activeCustomizationsCopy.delete(fragmentId);
316
+ // Find any lower-priority customizations with the same ID that were previously ignored
317
+ const lowerPriorityCustomizations = Array.from(allCustomizationsCopy.values())
318
+ .filter(t => t.id === fragmentId)
223
319
  .sort((a, b) => b.priority - a.priority); // Sort by priority (highest first)
224
320
 
225
- // If there are any lower-priority templates, add the highest priority one
226
- if (lowerPriorityTemplates.length > 0) {
227
- const highestRemainingTemplate = lowerPriorityTemplates[0];
228
- this.effectiveTemplates.set(templateId, highestRemainingTemplate);
321
+ // If there are any lower-priority customizations, add the highest priority one
322
+ if (lowerPriorityCustomizations.length > 0) {
323
+ const highestRemainingCustomization = lowerPriorityCustomizations[0];
324
+ activeCustomizationsCopy.set(fragmentId, highestRemainingCustomization);
229
325
  }
230
- this.onDidChangePromptEmitter.fire(templateId);
326
+
231
327
  }
328
+
329
+ return fragmentId;
232
330
  }
233
331
 
234
332
  /**
235
333
  * Process the template files specified by path, watching for changes
236
- * and loading their content into the templates map
334
+ * and loading their content into the customizations map
335
+ * @param activeCustomizationsCopy Map to store active customizations
336
+ * @param trackedTemplateURIsCopy Set to track URIs being monitored
337
+ * @param allCustomizationsCopy Map to store all loaded customizations
338
+ * @param watchedFilesCopy Map to store file watch information
237
339
  */
238
340
  protected async processTemplateFiles(
239
- templatesMap: Map<string, TemplateEntry>,
240
- trackedURIs: Set<string>,
241
- loadedTemplates: Map<string, TemplateEntry>,
242
- watchedFilesMap: Map<string, WatchedFileInfo>
341
+ activeCustomizationsCopy: Map<string, PromptFragmentCustomization>,
342
+ trackedTemplateURIsCopy: Set<string>,
343
+ allCustomizationsCopy: Map<string, PromptFragmentCustomization>,
344
+ watchedFilesCopy: Map<string, WatchedFileInfo>
243
345
  ): Promise<void> {
346
+ const priority = 3; // Highest priority for specific files
244
347
 
245
- for (const filePath of this.templateFiles) {
348
+ const parsedPromptFragments = new Set<string>();
349
+
350
+ for (const filePath of this.workspaceTemplateFiles) {
246
351
  const fileURI = URI.fromFilePath(filePath);
247
- const templateId = this.getTemplateIdFromFilePath(filePath);
352
+ const fragmentId = this.getFragmentIdFromFilePath(filePath);
248
353
  const uriString = fileURI.toString();
354
+ const customizationId = this.generateCustomizationId(fragmentId, uriString);
249
355
 
250
- watchedFilesMap.set(uriString, { uri: fileURI, templateId });
356
+ watchedFilesCopy.set(uriString, { uri: fileURI, fragmentId, customizationId });
251
357
  this.toDispose.push(this.fileService.watch(fileURI, { recursive: false, excludes: [] }));
252
358
 
253
359
  if (await this.fileService.exists(fileURI)) {
254
- trackedURIs.add(uriString);
360
+ trackedTemplateURIsCopy.add(uriString);
255
361
  const fileContent = await this.fileService.read(fileURI);
256
- this.addTemplate(templatesMap, templateId, fileContent.value, TemplatePriority.TEMPLATE_FILE, uriString, loadedTemplates);
362
+ this.addTemplate(activeCustomizationsCopy, fragmentId, fileContent.value, uriString, allCustomizationsCopy, priority, CustomizationSource.FILE);
363
+ parsedPromptFragments.add(fragmentId);
257
364
  }
258
365
  }
259
366
 
367
+ this.onDidChangePromptFragmentCustomizationEmitter.fire(Array.from(parsedPromptFragments));
368
+
260
369
  this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => {
370
+ // Only watch for changes that are in the watchedFiles map
371
+ if (!event.changes.some(change => this.watchedFiles.get(change.resource.toString()))) {
372
+ return;
373
+ }
374
+ // Track changes for batched notification
375
+ const changedFragmentIds = new Set<string>();
261
376
 
262
377
  // Handle deleted files
263
378
  for (const deletedFile of event.getDeleted()) {
@@ -265,9 +380,10 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
265
380
  const fileInfo = this.watchedFiles.get(fileUriString);
266
381
 
267
382
  if (fileInfo) {
268
- this.removeTemplateBySourceUri(fileUriString, loadedTemplates);
269
- this.trackedTemplateURIs.delete(fileUriString);
270
- this.onDidChangePromptEmitter.fire(fileInfo.templateId);
383
+ const removedFragmentId = this.removeCustomizationFromMaps(fileUriString, allCustomizationsCopy, activeCustomizationsCopy, trackedTemplateURIsCopy);
384
+ if (removedFragmentId) {
385
+ changedFragmentIds.add(removedFragmentId);
386
+ }
271
387
  }
272
388
  }
273
389
 
@@ -279,14 +395,15 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
279
395
  if (fileInfo) {
280
396
  const fileContent = await this.fileService.read(fileInfo.uri);
281
397
  this.addTemplate(
282
- this.effectiveTemplates,
283
- fileInfo.templateId,
398
+ this.activeCustomizations,
399
+ fileInfo.fragmentId,
284
400
  fileContent.value,
285
- TemplatePriority.TEMPLATE_FILE,
286
401
  fileUriString,
287
- loadedTemplates
402
+ this.allCustomizations,
403
+ priority,
404
+ CustomizationSource.FILE
288
405
  );
289
- this.onDidChangePromptEmitter.fire(fileInfo.templateId);
406
+ changedFragmentIds.add(fileInfo.fragmentId);
290
407
  }
291
408
  }
292
409
 
@@ -298,60 +415,110 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
298
415
  if (fileInfo) {
299
416
  const fileContent = await this.fileService.read(fileInfo.uri);
300
417
  this.addTemplate(
301
- this.effectiveTemplates,
302
- fileInfo.templateId,
418
+ this.activeCustomizations,
419
+ fileInfo.fragmentId,
303
420
  fileContent.value,
304
- TemplatePriority.TEMPLATE_FILE,
305
421
  fileUriString,
306
- loadedTemplates
422
+ this.allCustomizations,
423
+ priority,
424
+ CustomizationSource.FILE
307
425
  );
308
426
  this.trackedTemplateURIs.add(fileUriString);
309
- this.onDidChangePromptEmitter.fire(fileInfo.templateId);
427
+ changedFragmentIds.add(fileInfo.fragmentId);
310
428
  }
311
429
  }
430
+
431
+ const changedFragmentIdsArray = Array.from(changedFragmentIds);
432
+ if (changedFragmentIdsArray.length > 0) {
433
+ this.onDidChangePromptFragmentCustomizationEmitter.fire(changedFragmentIdsArray);
434
+ };
312
435
  }));
313
436
  }
314
437
 
315
438
  /**
316
- * Extract a template ID from a file path
439
+ * Extract a fragment ID from a file path
317
440
  * @param filePath The path to the template file
318
- * @returns A template ID derived from the file name
441
+ * @returns A fragment ID derived from the file name
319
442
  */
320
- protected getTemplateIdFromFilePath(filePath: string): string {
443
+ protected getFragmentIdFromFilePath(filePath: string): string {
321
444
  const uri = URI.fromFilePath(filePath);
322
445
  return this.removePromptTemplateSuffix(uri.path.name);
323
446
  }
324
447
 
448
+ /**
449
+ * Processes a directory for template files, adding them to the customizations map
450
+ * and setting up file watching
451
+ * @param activeCustomizationsCopy Map to store active customizations
452
+ * @param trackedTemplateURIsCopy Set to track URIs being monitored
453
+ * @param allCustomizationsCopy Map to store all loaded customizations
454
+ * @param dirURI URI of the directory to process
455
+ * @param priority Priority level for customizations in this directory
456
+ * @param customizationSource Source type of the customization
457
+ */
325
458
  protected async processTemplateDirectory(
326
- templatesMap: Map<string, TemplateEntry>,
327
- trackedURIs: Set<string>,
328
- loadedTemplates: Map<string, TemplateEntry>,
459
+ activeCustomizationsCopy: Map<string, PromptFragmentCustomization>,
460
+ trackedTemplateURIsCopy: Set<string>,
461
+ allCustomizationsCopy: Map<string, PromptFragmentCustomization>,
329
462
  dirURI: URI,
330
- priority: TemplatePriority
463
+ priority: number,
464
+ customizationSource: CustomizationSource
331
465
  ): Promise<void> {
466
+ if (!(await this.fileService.exists(dirURI))) {
467
+ return;
468
+ }
469
+ const stat = await this.fileService.resolve(dirURI);
470
+ if (stat.children === undefined) {
471
+ return;
472
+ }
473
+ const parsedPromptFragments = new Set<string>();
474
+ for (const file of stat.children) {
475
+ if (!file.isFile) {
476
+ continue;
477
+ }
478
+ const fileURI = file.resource;
479
+ if (this.isPromptTemplateExtension(fileURI.path.ext)) {
480
+ trackedTemplateURIsCopy.add(fileURI.toString());
481
+ const fileContent = await this.fileService.read(fileURI);
482
+ const fragmentId = this.removePromptTemplateSuffix(file.name);
483
+ this.addTemplate(activeCustomizationsCopy, fragmentId, fileContent.value, fileURI.toString(), allCustomizationsCopy, priority, customizationSource);
484
+ parsedPromptFragments.add(fragmentId);
485
+ }
486
+ }
487
+ this.onDidChangePromptFragmentCustomizationEmitter.fire(Array.from(parsedPromptFragments));
488
+ this.onDidChangeCustomAgentsEmitter.fire();
489
+
332
490
  this.toDispose.push(this.fileService.watch(dirURI, { recursive: true, excludes: [] }));
333
491
  this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => {
492
+ // Only watch for changes within provided dir
493
+ if (!event.changes.some(change => change.resource.toString().startsWith(dirURI.toString()))) {
494
+ return;
495
+ }
334
496
  if (event.changes.some(change => change.resource.toString().endsWith('customAgents.yml'))) {
335
497
  this.onDidChangeCustomAgentsEmitter.fire();
336
498
  }
337
499
 
500
+ // Track changes for batched notification
501
+ const changedFragmentIds = new Set<string>();
502
+
338
503
  // check deleted templates
339
504
  for (const deletedFile of event.getDeleted()) {
340
- if (this.trackedTemplateURIs.has(deletedFile.resource.toString())) {
341
- this.trackedTemplateURIs.delete(deletedFile.resource.toString());
342
- const templateId = this.removePromptTemplateSuffix(deletedFile.resource.path.name);
343
- this.removeTemplateBySourceUri(deletedFile.resource.toString(), loadedTemplates);
344
- this.onDidChangePromptEmitter.fire(templateId);
505
+ const uriString = deletedFile.resource.toString();
506
+ if (this.trackedTemplateURIs.has(uriString)) {
507
+ const removedFragmentId = this.removeCustomizationFromMaps(uriString, this.allCustomizations, this.activeCustomizations, this.trackedTemplateURIs);
508
+ if (removedFragmentId) {
509
+ changedFragmentIds.add(removedFragmentId);
510
+ }
345
511
  }
346
512
  }
347
513
 
348
514
  // check updated templates
349
515
  for (const updatedFile of event.getUpdated()) {
350
- if (this.trackedTemplateURIs.has(updatedFile.resource.toString())) {
516
+ const uriString = updatedFile.resource.toString();
517
+ if (this.trackedTemplateURIs.has(uriString)) {
351
518
  const fileContent = await this.fileService.read(updatedFile.resource);
352
- const templateId = this.removePromptTemplateSuffix(updatedFile.resource.path.name);
353
- this.addTemplate(this.effectiveTemplates, templateId, fileContent.value, priority, updatedFile.resource.toString(), loadedTemplates);
354
- this.onDidChangePromptEmitter.fire(templateId);
519
+ const fragmentId = this.removePromptTemplateSuffix(updatedFile.resource.path.name);
520
+ this.addTemplate(this.activeCustomizations, fragmentId, fileContent.value, uriString, this.allCustomizations, priority, customizationSource);
521
+ changedFragmentIds.add(fragmentId);
355
522
  }
356
523
  }
357
524
 
@@ -359,37 +526,20 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
359
526
  for (const addedFile of event.getAdded()) {
360
527
  if (addedFile.resource.parent.toString() === dirURI.toString() &&
361
528
  this.isPromptTemplateExtension(addedFile.resource.path.ext)) {
362
- this.trackedTemplateURIs.add(addedFile.resource.toString());
529
+ const uriString = addedFile.resource.toString();
530
+ this.trackedTemplateURIs.add(uriString);
363
531
  const fileContent = await this.fileService.read(addedFile.resource);
364
- const templateId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
365
- this.addTemplate(this.effectiveTemplates, templateId, fileContent.value, priority, addedFile.resource.toString(), loadedTemplates);
366
- this.onDidChangePromptEmitter.fire(templateId);
532
+ const fragmentId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
533
+ this.addTemplate(this.activeCustomizations, fragmentId, fileContent.value, uriString, this.allCustomizations, priority, customizationSource);
534
+ changedFragmentIds.add(fragmentId);
367
535
  }
368
536
  }
369
- }));
370
537
 
371
- if (!(await this.fileService.exists(dirURI))) {
372
- return;
373
- }
374
- const stat = await this.fileService.resolve(dirURI);
375
- if (stat.children === undefined) {
376
- return;
377
- }
378
-
379
- for (const file of stat.children) {
380
- if (!file.isFile) {
381
- continue;
382
- }
383
- const fileURI = file.resource;
384
- if (this.isPromptTemplateExtension(fileURI.path.ext)) {
385
- trackedURIs.add(fileURI.toString());
386
- const fileContent = await this.fileService.read(fileURI);
387
- const templateId = this.removePromptTemplateSuffix(file.name);
388
- this.addTemplate(templatesMap, templateId, fileContent.value, priority, fileURI.toString(), loadedTemplates);
389
- this.onDidChangePromptEmitter.fire(templateId);
390
- }
391
- }
392
- this.onDidChangeCustomAgentsEmitter.fire();
538
+ const changedFragmentIdsArray = Array.from(changedFragmentIds);
539
+ if (changedFragmentIdsArray.length > 0) {
540
+ this.onDidChangePromptFragmentCustomizationEmitter.fire(changedFragmentIdsArray);
541
+ };
542
+ }));
393
543
  }
394
544
 
395
545
  /**
@@ -422,16 +572,15 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
422
572
  * @returns Array of file paths
423
573
  */
424
574
  getTemplateFiles(): string[] {
425
- return Array.from(this.templateFiles);
575
+ return Array.from(this.workspaceTemplateFiles);
426
576
  }
427
577
 
428
578
  /**
429
579
  * Updates multiple configuration properties at once, triggering only a single update process.
430
- *
431
580
  * @param properties An object containing the properties to update
432
581
  * @returns Promise that resolves when the update is complete
433
582
  */
434
- async updateConfiguration(properties: PromptCustomizationProperties): Promise<void> {
583
+ async updateConfiguration(properties: PromptFragmentCustomizationProperties): Promise<void> {
435
584
  if (properties.directoryPaths !== undefined) {
436
585
  this.additionalTemplateDirs.clear();
437
586
  for (const path of properties.directoryPaths) {
@@ -449,9 +598,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
449
598
  }
450
599
 
451
600
  if (properties.filePaths !== undefined) {
452
- this.templateFiles.clear();
601
+ this.workspaceTemplateFiles.clear();
453
602
  for (const path of properties.filePaths) {
454
- this.templateFiles.add(path);
603
+ this.workspaceTemplateFiles.add(path);
455
604
  }
456
605
  }
457
606
 
@@ -459,6 +608,10 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
459
608
  await this.update();
460
609
  }
461
610
 
611
+ /**
612
+ * Gets the URI of the templates directory
613
+ * @returns URI of the templates directory
614
+ */
462
615
  protected async getTemplatesDirectoryURI(): Promise<URI> {
463
616
  const templatesFolder = this.preferences[PREFERENCE_NAME_PROMPT_TEMPLATES];
464
617
  if (templatesFolder && templatesFolder.trim().length > 0) {
@@ -468,10 +621,20 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
468
621
  return new URI(theiaConfigDir).resolve('prompt-templates');
469
622
  }
470
623
 
471
- protected async getTemplateURI(templateId: string): Promise<URI> {
472
- return (await this.getTemplatesDirectoryURI()).resolve(`${templateId}${PROMPT_TEMPLATE_EXTENSION}`);
624
+ /**
625
+ * Gets the URI for a specific template file
626
+ * @param fragmentId The fragment ID
627
+ * @returns URI for the template file
628
+ */
629
+ protected async getTemplateURI(fragmentId: string): Promise<URI> {
630
+ return (await this.getTemplatesDirectoryURI()).resolve(`${fragmentId}${PROMPT_TEMPLATE_EXTENSION}`);
473
631
  }
474
632
 
633
+ /**
634
+ * Removes the prompt template extension from a filename
635
+ * @param filename The filename with extension
636
+ * @returns The filename without the extension
637
+ */
475
638
  protected removePromptTemplateSuffix(filename: string): string {
476
639
  for (const ext of this.templateExtensions) {
477
640
  if (filename.endsWith(ext)) {
@@ -481,43 +644,271 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
481
644
  return filename;
482
645
  }
483
646
 
484
- isPromptTemplateCustomized(id: string): boolean {
485
- return this.effectiveTemplates.has(id);
647
+ // PromptFragmentCustomizationService interface implementation
648
+
649
+ isPromptFragmentCustomized(id: string): boolean {
650
+ return this.activeCustomizations.has(id);
651
+ }
652
+
653
+ getActivePromptFragmentCustomization(id: string): CustomizedPromptFragment | undefined {
654
+ const entry = this.activeCustomizations.get(id);
655
+ if (!entry) {
656
+ return undefined;
657
+ }
658
+
659
+ return {
660
+ id: entry.id,
661
+ template: entry.template,
662
+ customizationId: entry.customizationId,
663
+ priority: entry.priority
664
+ };
665
+ }
666
+
667
+ getAllCustomizations(id: string): CustomizedPromptFragment[] {
668
+ const fragments: CustomizedPromptFragment[] = [];
669
+
670
+ // Collect all customizations with matching ID
671
+ this.allCustomizations.forEach(value => {
672
+ if (value.id === id) {
673
+ fragments.push({
674
+ id: value.id,
675
+ template: value.template,
676
+ customizationId: value.customizationId,
677
+ priority: value.priority
678
+ });
679
+ }
680
+ });
681
+
682
+ // Sort by priority (highest first)
683
+ return fragments.sort((a, b) => b.priority - a.priority);
684
+ }
685
+
686
+ getCustomizedPromptFragmentIds(): string[] {
687
+ return Array.from(this.activeCustomizations.keys());
688
+ }
689
+
690
+ async createPromptFragmentCustomization(id: string, defaultContent?: string): Promise<void> {
691
+ await this.editTemplate(id, defaultContent);
486
692
  }
487
693
 
488
- getCustomizedPromptTemplate(id: string): string | undefined {
489
- const entry = this.effectiveTemplates.get(id);
490
- return entry ? entry.content : undefined;
694
+ async createBuiltInPromptFragmentCustomization(id: string, defaultContent?: string): Promise<void> {
695
+ await this.createPromptFragmentCustomization(id, defaultContent);
491
696
  }
492
697
 
493
- getCustomPromptTemplateIDs(): string[] {
494
- return Array.from(this.effectiveTemplates.keys());
698
+ async editPromptFragmentCustomization(id: string, customizationId: string): Promise<void> {
699
+ // Find the customization with the given customization ID
700
+ const customization = Array.from(this.allCustomizations.values()).find(t =>
701
+ t.id === id && t.customizationId === customizationId
702
+ );
703
+
704
+ if (customization) {
705
+ const uri = new URI(customization.sourceUri);
706
+ const openHandler = await this.openerService.getOpener(uri);
707
+ openHandler.open(uri);
708
+ } else {
709
+ // Fall back to editing by fragment ID if customization ID not found
710
+ await this.editTemplate(id);
711
+ }
495
712
  }
496
713
 
497
- async editTemplate(id: string, defaultContent?: string): Promise<void> {
714
+ /**
715
+ * Edits a template by opening it in the editor, creating it if it doesn't exist
716
+ * @param id The fragment ID
717
+ * @param defaultContent Optional default content for new templates
718
+ */
719
+ protected async editTemplate(id: string, defaultContent?: string): Promise<void> {
498
720
  const editorUri = await this.getTemplateURI(id);
499
- if (! await this.fileService.exists(editorUri)) {
721
+ if (!(await this.fileService.exists(editorUri))) {
500
722
  await this.fileService.createFile(editorUri, BinaryBuffer.fromString(defaultContent ?? ''));
501
723
  }
502
724
  const openHandler = await this.openerService.getOpener(editorUri);
503
725
  openHandler.open(editorUri);
504
726
  }
505
727
 
506
- async resetTemplate(id: string): Promise<void> {
507
- const editorUri = await this.getTemplateURI(id);
508
- if (await this.fileService.exists(editorUri)) {
509
- await this.fileService.delete(editorUri);
728
+ async removePromptFragmentCustomization(id: string, customizationId: string): Promise<void> {
729
+ // Find the customization with the given customization ID
730
+ const customization = Array.from(this.allCustomizations.values()).find(t =>
731
+ t.id === id && t.customizationId === customizationId
732
+ );
733
+
734
+ if (customization) {
735
+ const sourceUri = customization.sourceUri;
736
+
737
+ // Delete the file if it exists
738
+ const uri = new URI(sourceUri);
739
+ if (await this.fileService.exists(uri)) {
740
+ await this.fileService.delete(uri);
741
+ }
742
+ }
743
+ }
744
+
745
+ async removeAllPromptFragmentCustomizations(id: string): Promise<void> {
746
+ // Get all customizations for this fragment ID
747
+ const customizations = this.getAllCustomizations(id);
748
+
749
+ if (customizations.length === 0) {
750
+ return; // Nothing to reset
751
+ }
752
+
753
+ // Find and delete all customization files
754
+ for (const customization of customizations) {
755
+ const fragment = Array.from(this.allCustomizations.values()).find(t =>
756
+ t.id === id && t.customizationId === customization.customizationId
757
+ );
758
+
759
+ if (fragment) {
760
+ const sourceUri = fragment.sourceUri;
761
+ // Delete the file if it exists
762
+ const uri = new URI(sourceUri);
763
+ if (await this.fileService.exists(uri)) {
764
+ await this.fileService.delete(uri);
765
+ }
766
+ }
767
+ }
768
+ }
769
+
770
+ async resetToCustomization(id: string, customizationId: string): Promise<void> {
771
+ const customization = Array.from(this.allCustomizations.values()).find(t =>
772
+ t.id === id && t.customizationId === customizationId
773
+ );
774
+
775
+ if (customization) {
776
+ // Get all customizations for this fragment ID
777
+ const customizations = this.getAllCustomizations(id);
778
+
779
+ if (customizations.length === 0) {
780
+ return; // Nothing to reset
781
+ }
782
+
783
+ // Find the target customization
784
+ const targetCustomization = customizations.find(c => c.customizationId === customizationId);
785
+ if (!targetCustomization) {
786
+ return; // Target customization not found
787
+ }
788
+
789
+ // Find and delete all higher-priority customization files
790
+ for (const cust of customizations) {
791
+ if (cust.priority > targetCustomization.priority) {
792
+ const fragmentToDelete = Array.from(this.allCustomizations.values()).find(t =>
793
+ t.id === cust.id && t.customizationId === cust.customizationId
794
+ );
795
+ if (fragmentToDelete) {
796
+ const sourceUri = fragmentToDelete.sourceUri;
797
+
798
+ // Delete the file if it exists
799
+ const uri = new URI(sourceUri);
800
+ if (await this.fileService.exists(uri)) {
801
+ await this.fileService.delete(uri);
802
+ }
803
+ }
804
+ }
805
+ }
806
+ }
807
+ }
808
+
809
+ async getPromptFragmentCustomizationDescription(id: string, customizationId: string): Promise<string | undefined> {
810
+ // Find the customization with the given customization ID
811
+ const customization = Array.from(this.allCustomizations.values()).find(t =>
812
+ t.id === id && t.customizationId === customizationId
813
+ );
814
+
815
+ if (customization) {
816
+ return customization.sourceUri;
817
+ }
818
+
819
+ return undefined;
820
+ }
821
+
822
+ async getPromptFragmentCustomizationType(id: string, customizationId: string): Promise<string | undefined> {
823
+ // Find the customization with the given customization ID
824
+ const customization = Array.from(this.allCustomizations.values()).find(t =>
825
+ t.id === id && t.customizationId === customizationId
826
+ );
827
+
828
+ if (customization) {
829
+ return getCustomizationSourceString(customization.origin);
830
+ }
831
+
832
+ return undefined;
833
+ }
834
+
835
+ async editBuiltIn(id: string, defaultContent = ''): Promise<void> {
836
+ // Find an existing built-in customization (those with priority 1)
837
+ const builtInCustomization = Array.from(this.allCustomizations.values()).find(t =>
838
+ t.id === id && t.priority === 1
839
+ );
840
+
841
+ if (builtInCustomization) {
842
+ // Edit the existing built-in customization
843
+ const uri = new URI(builtInCustomization.sourceUri);
844
+ const openHandler = await this.openerService.getOpener(uri);
845
+ openHandler.open(uri);
846
+ } else {
847
+ // Create a new built-in customization
848
+ // Get the template URI in the main templates directory (priority 1)
849
+ const templateUri = await this.getTemplateURI(id);
850
+
851
+ // If template doesn't exist, create it with default content
852
+ if (!(await this.fileService.exists(templateUri))) {
853
+ await this.fileService.createFile(templateUri, BinaryBuffer.fromString(defaultContent));
854
+ }
855
+
856
+ // Open the template in the editor
857
+ const openHandler = await this.openerService.getOpener(templateUri);
858
+ openHandler.open(templateUri);
510
859
  }
511
860
  }
512
861
 
513
- getTemplateIDFromURI(uri: URI): string | undefined {
862
+ async resetBuiltInCustomization(id: string): Promise<void> {
863
+ // Find a built-in customization (those with priority 1)
864
+ const builtInCustomization = Array.from(this.allCustomizations.values()).find(t =>
865
+ t.id === id && t.priority === 1
866
+ );
867
+
868
+ if (!builtInCustomization) {
869
+ return; // No built-in customization found
870
+ }
871
+
872
+ const sourceUri = builtInCustomization.sourceUri;
873
+
874
+ // Delete the file if it exists
875
+ const uri = new URI(sourceUri);
876
+ if (await this.fileService.exists(uri)) {
877
+ await this.fileService.delete(uri);
878
+ }
879
+ }
880
+
881
+ async editBuiltInPromptFragmentCustomization(id: string, defaultContent?: string): Promise<void> {
882
+ return this.editBuiltIn(id, defaultContent);
883
+ }
884
+
885
+ /**
886
+ * Gets the fragment ID from a URI
887
+ * @param uri URI to check
888
+ * @returns Fragment ID or undefined if not found
889
+ */
890
+ protected getFragmentIDFromURI(uri: URI): string | undefined {
514
891
  const id = this.removePromptTemplateSuffix(uri.path.name);
515
- if (this.effectiveTemplates.has(id)) {
892
+ if (this.activeCustomizations.has(id)) {
516
893
  return id;
517
894
  }
518
895
  return undefined;
519
896
  }
520
897
 
898
+ /**
899
+ * Implementation of the generic getPromptFragmentIDFromResource method in the interface
900
+ * Accepts any resource identifier but only processes URIs
901
+ * @param resourceId Resource to check
902
+ * @returns Fragment ID or undefined if not found
903
+ */
904
+ getPromptFragmentIDFromResource(resourceId: unknown): string | undefined {
905
+ // Check if the resource is a URI
906
+ if (resourceId instanceof URI) {
907
+ return this.getFragmentIDFromURI(resourceId);
908
+ }
909
+ return undefined;
910
+ }
911
+
521
912
  async getCustomAgents(): Promise<CustomAgentDescription[]> {
522
913
  const agentsById = new Map<string, CustomAgentDescription>();
523
914
  // First, process additional (workspace) template directories to give them precedence
@@ -525,9 +916,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
525
916
  const dirURI = URI.fromFilePath(dirPath);
526
917
  await this.loadCustomAgentsFromDirectory(dirURI, agentsById);
527
918
  }
528
- // Then process global template directory (only adding agents that don't conflict)
529
- const globalTemplateDir = await this.getTemplatesDirectoryURI();
530
- await this.loadCustomAgentsFromDirectory(globalTemplateDir, agentsById);
919
+ // Then process global templates directory (only adding agents that don't conflict)
920
+ const globalTemplatesDir = await this.getTemplatesDirectoryURI();
921
+ await this.loadCustomAgentsFromDirectory(globalTemplatesDir, agentsById);
531
922
  // Return the merged list of agents
532
923
  return Array.from(agentsById.values());
533
924
  }
@@ -577,9 +968,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
577
968
  */
578
969
  async getCustomAgentsLocations(): Promise<{ uri: URI, exists: boolean }[]> {
579
970
  const locations: { uri: URI, exists: boolean }[] = [];
580
- // Check global template directory
581
- const globalTemplateDir = await this.getTemplatesDirectoryURI();
582
- const globalAgentsUri = globalTemplateDir.resolve('customAgents.yml');
971
+ // Check global templates directory
972
+ const globalTemplatesDir = await this.getTemplatesDirectoryURI();
973
+ const globalAgentsUri = globalTemplatesDir.resolve('customAgents.yml');
583
974
  const globalExists = await this.fileService.exists(globalAgentsUri);
584
975
  locations.push({ uri: globalAgentsUri, exists: globalExists });
585
976
  // Check additional (workspace) template directories
@@ -598,7 +989,7 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
598
989
  * @param uri The URI of the customAgents.yml file to open or create
599
990
  */
600
991
  async openCustomAgentYaml(uri: URI): Promise<void> {
601
- const content = dump([templateEntry]);
992
+ const content = dump([newCustomAgentEntry]);
602
993
  if (! await this.fileService.exists(uri)) {
603
994
  await this.fileService.createFile(uri, BinaryBuffer.fromString(content));
604
995
  } else {