@theia/ai-core 1.60.0-next.47 → 1.60.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 (71) hide show
  1. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-core-frontend-module.js +4 -0
  3. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  4. package/lib/browser/ai-core-preferences.d.ts +22 -8
  5. package/lib/browser/ai-core-preferences.d.ts.map +1 -1
  6. package/lib/browser/ai-core-preferences.js +90 -10
  7. package/lib/browser/ai-core-preferences.js.map +1 -1
  8. package/lib/browser/frontend-language-model-registry.d.ts.map +1 -1
  9. package/lib/browser/frontend-language-model-registry.js +1 -1
  10. package/lib/browser/frontend-language-model-registry.js.map +1 -1
  11. package/lib/browser/frontend-language-model-service.d.ts +10 -0
  12. package/lib/browser/frontend-language-model-service.d.ts.map +1 -0
  13. package/lib/browser/frontend-language-model-service.js +66 -0
  14. package/lib/browser/frontend-language-model-service.js.map +1 -0
  15. package/lib/browser/frontend-prompt-customization-service.d.ts +99 -2
  16. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  17. package/lib/browser/frontend-prompt-customization-service.js +245 -23
  18. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  19. package/lib/browser/index.d.ts +1 -0
  20. package/lib/browser/index.d.ts.map +1 -1
  21. package/lib/browser/index.js +1 -0
  22. package/lib/browser/index.js.map +1 -1
  23. package/lib/browser/prompttemplate-contribution.d.ts +4 -0
  24. package/lib/browser/prompttemplate-contribution.d.ts.map +1 -1
  25. package/lib/browser/prompttemplate-contribution.js +61 -2
  26. package/lib/browser/prompttemplate-contribution.js.map +1 -1
  27. package/lib/browser/theia-variable-contribution.d.ts +9 -4
  28. package/lib/browser/theia-variable-contribution.d.ts.map +1 -1
  29. package/lib/browser/theia-variable-contribution.js +67 -38
  30. package/lib/browser/theia-variable-contribution.js.map +1 -1
  31. package/lib/common/communication-recording-service.d.ts +8 -7
  32. package/lib/common/communication-recording-service.d.ts.map +1 -1
  33. package/lib/common/communication-recording-service.js.map +1 -1
  34. package/lib/common/index.d.ts +1 -0
  35. package/lib/common/index.d.ts.map +1 -1
  36. package/lib/common/index.js +1 -0
  37. package/lib/common/index.js.map +1 -1
  38. package/lib/common/language-model-service.d.ts +15 -0
  39. package/lib/common/language-model-service.d.ts.map +1 -0
  40. package/lib/common/language-model-service.js +51 -0
  41. package/lib/common/language-model-service.js.map +1 -0
  42. package/lib/common/language-model-util.d.ts.map +1 -1
  43. package/lib/common/language-model-util.js +1 -2
  44. package/lib/common/language-model-util.js.map +1 -1
  45. package/lib/common/language-model.d.ts +56 -15
  46. package/lib/common/language-model.d.ts.map +1 -1
  47. package/lib/common/language-model.js +27 -2
  48. package/lib/common/language-model.js.map +1 -1
  49. package/lib/common/tool-invocation-registry.d.ts +3 -0
  50. package/lib/common/tool-invocation-registry.d.ts.map +1 -1
  51. package/lib/common/tool-invocation-registry.js +7 -1
  52. package/lib/common/tool-invocation-registry.js.map +1 -1
  53. package/lib/common/variable-service.d.ts +1 -1
  54. package/lib/common/variable-service.d.ts.map +1 -1
  55. package/lib/common/variable-service.js.map +1 -1
  56. package/package.json +10 -10
  57. package/src/browser/ai-core-frontend-module.ts +4 -0
  58. package/src/browser/ai-core-preferences.ts +106 -15
  59. package/src/browser/frontend-language-model-registry.ts +2 -1
  60. package/src/browser/frontend-language-model-service.ts +67 -0
  61. package/src/browser/frontend-prompt-customization-service.ts +348 -27
  62. package/src/browser/index.ts +1 -0
  63. package/src/browser/prompttemplate-contribution.ts +80 -3
  64. package/src/browser/theia-variable-contribution.ts +81 -40
  65. package/src/common/communication-recording-service.ts +9 -7
  66. package/src/common/index.ts +1 -0
  67. package/src/common/language-model-service.ts +59 -0
  68. package/src/common/language-model-util.ts +9 -2
  69. package/src/common/language-model.ts +72 -15
  70. package/src/common/tool-invocation-registry.ts +7 -2
  71. package/src/common/variable-service.ts +2 -1
@@ -34,6 +34,38 @@ const templateEntry = {
34
34
  defaultLLM: 'openai/gpt-4o'
35
35
  };
36
36
 
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
43
+ }
44
+
45
+ /**
46
+ * Interface defining properties that can be updated in the service
47
+ */
48
+ export interface PromptCustomizationProperties {
49
+ /** Array of directory paths to load templates from */
50
+ directoryPaths?: string[];
51
+ /** Array of file paths to treat as templates */
52
+ filePaths?: string[];
53
+ /** Array of file extensions to consider as template files */
54
+ extensions?: string[];
55
+ }
56
+
57
+ interface TemplateEntry {
58
+ content: string;
59
+ priority: number;
60
+ sourceUri: string;
61
+ id: string;
62
+ }
63
+
64
+ interface WatchedFileInfo {
65
+ uri: URI;
66
+ templateId: string;
67
+ }
68
+
37
69
  @injectable()
38
70
  export class FrontendPromptCustomizationServiceImpl implements PromptCustomizationService {
39
71
 
@@ -49,9 +81,21 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
49
81
  @inject(OpenerService)
50
82
  protected readonly openerService: OpenerService;
51
83
 
52
- protected readonly trackedTemplateURIs = new Set<string>();
53
- protected templates = new Map<string, string>();
54
-
84
+ /** Stores URI strings of template files from directories currently being monitored for changes. */
85
+ 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>();
90
+ /** Stores additional directory paths for loading template files. */
91
+ protected additionalTemplateDirs = new Set<string>();
92
+ /** Contains file extensions that identify prompt template files. */
93
+ 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>();
96
+ /** Maps URI strings to WatchedFileInfo objects for individually watched template files. */
97
+ protected watchedFiles = new Map<string, WatchedFileInfo>();
98
+ /** Collection of disposable resources for cleanup when the service updates or is disposed. */
55
99
  protected toDispose = new DisposableCollection();
56
100
 
57
101
  private readonly onDidChangePromptEmitter = new Emitter<string>();
@@ -62,6 +106,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
62
106
 
63
107
  @postConstruct()
64
108
  protected init(): void {
109
+ // Ensure PROMPT_TEMPLATE_EXTENSION is always included in templateExtensions as a default
110
+ this.templateExtensions.add(PROMPT_TEMPLATE_EXTENSION);
111
+
65
112
  this.preferences.onPreferenceChanged(event => {
66
113
  if (event.preferenceName === PREFERENCE_NAME_PROMPT_TEMPLATES) {
67
114
  this.update();
@@ -72,55 +119,259 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
72
119
 
73
120
  protected async update(): Promise<void> {
74
121
  this.toDispose.dispose();
75
- // we need to assign a local variable, so that updates running in parallel don't interfere with each other
76
- const _templates = new Map<string, string>();
77
- this.templates = _templates;
78
- this.trackedTemplateURIs.clear();
122
+ // 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>();
79
127
 
128
+ // Process in order of priority (lowest to highest)
129
+ // First process the main template directory (lowest priority)
80
130
  const templateURI = await this.getTemplatesDirectoryURI();
131
+ await this.processTemplateDirectory(templatesMap, trackedURIs, loadedTemplates, templateURI, TemplatePriority.GLOBAL_TEMPLATE_DIR);
132
+
133
+ // Process additional template directories (medium priority)
134
+ for (const dirPath of this.additionalTemplateDirs) {
135
+ const dirURI = URI.fromFilePath(dirPath);
136
+ await this.processTemplateDirectory(templatesMap, trackedURIs, loadedTemplates, dirURI, TemplatePriority.ADDITIONAL_TEMPLATE_DIR);
137
+ }
138
+
139
+ // Process specific template files (highest priority)
140
+ await this.processTemplateFiles(templatesMap, trackedURIs, loadedTemplates, watchedFilesMap);
141
+
142
+ this.effectiveTemplates = templatesMap;
143
+ this.trackedTemplateURIs = trackedURIs;
144
+ this.allLoadedTemplates = loadedTemplates;
145
+ this.watchedFiles = watchedFilesMap;
146
+
147
+ this.onDidChangeCustomAgentsEmitter.fire();
148
+ }
149
+
150
+ /**
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
156
+ * @param sourceUri The URI of the source file (used to distinguish updates from conflicts)
157
+ * @param loadedTemplates The map to track all loaded templates
158
+ */
159
+ protected addTemplate(
160
+ templatesMap: Map<string, TemplateEntry>,
161
+ id: string,
162
+ content: string,
163
+ priority: number,
164
+ sourceUri: string,
165
+ loadedTemplates: Map<string, TemplateEntry>
166
+ ): void {
167
+ // Always add to loadedTemplates to keep track of all templates including overridden ones
168
+ if (sourceUri) {
169
+ loadedTemplates.set(sourceUri, { id, content, priority, sourceUri });
170
+ }
171
+
172
+ const existingEntry = templatesMap.get(id);
173
+
174
+ if (existingEntry) {
175
+ // If this is an update to the same file (same source URI)
176
+ if (sourceUri && existingEntry.sourceUri === sourceUri) {
177
+ // Update the content while keeping the same priority and source
178
+ templatesMap.set(id, { id, content, priority, sourceUri });
179
+ return;
180
+ }
181
+
182
+ // If the new template has higher priority, replace the existing one
183
+ if (priority > existingEntry.priority) {
184
+ templatesMap.set(id, { id, content, priority, sourceUri });
185
+ return;
186
+ } else if (priority === existingEntry.priority) {
187
+ // There is a conflict with the same priority, we ignore the new template
188
+ const conflictSourceUri = existingEntry.sourceUri ? ` (Existing source: ${existingEntry.sourceUri}, New source: ${sourceUri})` : '';
189
+ console.warn(`Template conflict detected for ID '${id}' with equal priority.${conflictSourceUri}`);
190
+ }
191
+ return;
192
+ }
193
+
194
+ // No conflict at all, add the template
195
+ templatesMap.set(id, { id, content, priority, sourceUri });
196
+ }
197
+
198
+ /**
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.
201
+ * @param sourceUri The URI of the source file being removed
202
+ * @param loadedTemplates The map of all loaded templates
203
+ */
204
+ protected removeTemplateBySourceUri(
205
+ 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;
212
+ }
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)
223
+ .sort((a, b) => b.priority - a.priority); // Sort by priority (highest first)
224
+
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);
229
+ }
230
+ this.onDidChangePromptEmitter.fire(templateId);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Process the template files specified by path, watching for changes
236
+ * and loading their content into the templates map
237
+ */
238
+ protected async processTemplateFiles(
239
+ templatesMap: Map<string, TemplateEntry>,
240
+ trackedURIs: Set<string>,
241
+ loadedTemplates: Map<string, TemplateEntry>,
242
+ watchedFilesMap: Map<string, WatchedFileInfo>
243
+ ): Promise<void> {
244
+
245
+ for (const filePath of this.templateFiles) {
246
+ const fileURI = URI.fromFilePath(filePath);
247
+ const templateId = this.getTemplateIdFromFilePath(filePath);
248
+ const uriString = fileURI.toString();
249
+
250
+ watchedFilesMap.set(uriString, { uri: fileURI, templateId });
251
+ this.toDispose.push(this.fileService.watch(fileURI, { recursive: false, excludes: [] }));
252
+
253
+ if (await this.fileService.exists(fileURI)) {
254
+ trackedURIs.add(uriString);
255
+ const fileContent = await this.fileService.read(fileURI);
256
+ this.addTemplate(templatesMap, templateId, fileContent.value, TemplatePriority.TEMPLATE_FILE, uriString, loadedTemplates);
257
+ }
258
+ }
81
259
 
82
- this.toDispose.push(this.fileService.watch(templateURI, { recursive: true, excludes: [] }));
260
+ this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => {
261
+
262
+ // Handle deleted files
263
+ for (const deletedFile of event.getDeleted()) {
264
+ const fileUriString = deletedFile.resource.toString();
265
+ const fileInfo = this.watchedFiles.get(fileUriString);
266
+
267
+ if (fileInfo) {
268
+ this.removeTemplateBySourceUri(fileUriString, loadedTemplates);
269
+ this.trackedTemplateURIs.delete(fileUriString);
270
+ this.onDidChangePromptEmitter.fire(fileInfo.templateId);
271
+ }
272
+ }
273
+
274
+ // Handle updated files
275
+ for (const updatedFile of event.getUpdated()) {
276
+ const fileUriString = updatedFile.resource.toString();
277
+ const fileInfo = this.watchedFiles.get(fileUriString);
278
+
279
+ if (fileInfo) {
280
+ const fileContent = await this.fileService.read(fileInfo.uri);
281
+ this.addTemplate(
282
+ this.effectiveTemplates,
283
+ fileInfo.templateId,
284
+ fileContent.value,
285
+ TemplatePriority.TEMPLATE_FILE,
286
+ fileUriString,
287
+ loadedTemplates
288
+ );
289
+ this.onDidChangePromptEmitter.fire(fileInfo.templateId);
290
+ }
291
+ }
292
+
293
+ // Handle newly created files
294
+ for (const addedFile of event.getAdded()) {
295
+ const fileUriString = addedFile.resource.toString();
296
+ const fileInfo = this.watchedFiles.get(fileUriString);
297
+
298
+ if (fileInfo) {
299
+ const fileContent = await this.fileService.read(fileInfo.uri);
300
+ this.addTemplate(
301
+ this.effectiveTemplates,
302
+ fileInfo.templateId,
303
+ fileContent.value,
304
+ TemplatePriority.TEMPLATE_FILE,
305
+ fileUriString,
306
+ loadedTemplates
307
+ );
308
+ this.trackedTemplateURIs.add(fileUriString);
309
+ this.onDidChangePromptEmitter.fire(fileInfo.templateId);
310
+ }
311
+ }
312
+ }));
313
+ }
314
+
315
+ /**
316
+ * Extract a template ID from a file path
317
+ * @param filePath The path to the template file
318
+ * @returns A template ID derived from the file name
319
+ */
320
+ protected getTemplateIdFromFilePath(filePath: string): string {
321
+ const uri = URI.fromFilePath(filePath);
322
+ return this.removePromptTemplateSuffix(uri.path.name);
323
+ }
324
+
325
+ protected async processTemplateDirectory(
326
+ templatesMap: Map<string, TemplateEntry>,
327
+ trackedURIs: Set<string>,
328
+ loadedTemplates: Map<string, TemplateEntry>,
329
+ dirURI: URI,
330
+ priority: TemplatePriority
331
+ ): Promise<void> {
332
+ this.toDispose.push(this.fileService.watch(dirURI, { recursive: true, excludes: [] }));
83
333
  this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => {
84
334
  if (event.changes.some(change => change.resource.toString().endsWith('customAgents.yml'))) {
85
335
  this.onDidChangeCustomAgentsEmitter.fire();
86
336
  }
337
+
87
338
  // check deleted templates
88
339
  for (const deletedFile of event.getDeleted()) {
89
340
  if (this.trackedTemplateURIs.has(deletedFile.resource.toString())) {
90
341
  this.trackedTemplateURIs.delete(deletedFile.resource.toString());
91
342
  const templateId = this.removePromptTemplateSuffix(deletedFile.resource.path.name);
92
- _templates.delete(templateId);
343
+ this.removeTemplateBySourceUri(deletedFile.resource.toString(), loadedTemplates);
93
344
  this.onDidChangePromptEmitter.fire(templateId);
94
345
  }
95
346
  }
347
+
96
348
  // check updated templates
97
349
  for (const updatedFile of event.getUpdated()) {
98
350
  if (this.trackedTemplateURIs.has(updatedFile.resource.toString())) {
99
351
  const fileContent = await this.fileService.read(updatedFile.resource);
100
352
  const templateId = this.removePromptTemplateSuffix(updatedFile.resource.path.name);
101
- _templates.set(templateId, fileContent.value);
353
+ this.addTemplate(this.effectiveTemplates, templateId, fileContent.value, priority, updatedFile.resource.toString(), loadedTemplates);
102
354
  this.onDidChangePromptEmitter.fire(templateId);
103
355
  }
104
356
  }
357
+
105
358
  // check new templates
106
359
  for (const addedFile of event.getAdded()) {
107
- if (addedFile.resource.parent.toString() === templateURI.toString() && addedFile.resource.path.ext === PROMPT_TEMPLATE_EXTENSION) {
360
+ if (addedFile.resource.parent.toString() === dirURI.toString() &&
361
+ this.isPromptTemplateExtension(addedFile.resource.path.ext)) {
108
362
  this.trackedTemplateURIs.add(addedFile.resource.toString());
109
363
  const fileContent = await this.fileService.read(addedFile.resource);
110
364
  const templateId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
111
- _templates.set(templateId, fileContent.value);
365
+ this.addTemplate(this.effectiveTemplates, templateId, fileContent.value, priority, addedFile.resource.toString(), loadedTemplates);
112
366
  this.onDidChangePromptEmitter.fire(templateId);
113
367
  }
114
368
  }
115
-
116
369
  }));
117
370
 
118
- this.onDidChangeCustomAgentsEmitter.fire();
119
-
120
- if (!(await this.fileService.exists(templateURI))) {
371
+ if (!(await this.fileService.exists(dirURI))) {
121
372
  return;
122
373
  }
123
- const stat = await this.fileService.resolve(templateURI);
374
+ const stat = await this.fileService.resolve(dirURI);
124
375
  if (stat.children === undefined) {
125
376
  return;
126
377
  }
@@ -130,14 +381,82 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
130
381
  continue;
131
382
  }
132
383
  const fileURI = file.resource;
133
- if (fileURI.path.ext === PROMPT_TEMPLATE_EXTENSION) {
134
- this.trackedTemplateURIs.add(fileURI.toString());
384
+ if (this.isPromptTemplateExtension(fileURI.path.ext)) {
385
+ trackedURIs.add(fileURI.toString());
135
386
  const fileContent = await this.fileService.read(fileURI);
136
387
  const templateId = this.removePromptTemplateSuffix(file.name);
137
- _templates.set(templateId, fileContent.value);
388
+ this.addTemplate(templatesMap, templateId, fileContent.value, priority, fileURI.toString(), loadedTemplates);
138
389
  this.onDidChangePromptEmitter.fire(templateId);
139
390
  }
140
391
  }
392
+ this.onDidChangeCustomAgentsEmitter.fire();
393
+ }
394
+
395
+ /**
396
+ * Checks if the given file extension is registered as a prompt template extension
397
+ * @param extension The file extension including the leading dot (e.g., '.prompttemplate')
398
+ * @returns True if the extension is registered as a prompt template extension
399
+ */
400
+ protected isPromptTemplateExtension(extension: string): boolean {
401
+ return this.templateExtensions.has(extension);
402
+ }
403
+
404
+ /**
405
+ * Gets the list of additional template directories that are being watched.
406
+ * @returns Array of directory paths
407
+ */
408
+ getAdditionalTemplateDirectories(): string[] {
409
+ return Array.from(this.additionalTemplateDirs);
410
+ }
411
+
412
+ /**
413
+ * Gets the list of file extensions that are considered prompt templates.
414
+ * @returns Array of file extensions including the leading dot (e.g., '.prompttemplate')
415
+ */
416
+ getTemplateFileExtensions(): string[] {
417
+ return Array.from(this.templateExtensions);
418
+ }
419
+
420
+ /**
421
+ * Gets the list of specific template files that are being watched.
422
+ * @returns Array of file paths
423
+ */
424
+ getTemplateFiles(): string[] {
425
+ return Array.from(this.templateFiles);
426
+ }
427
+
428
+ /**
429
+ * Updates multiple configuration properties at once, triggering only a single update process.
430
+ *
431
+ * @param properties An object containing the properties to update
432
+ * @returns Promise that resolves when the update is complete
433
+ */
434
+ async updateConfiguration(properties: PromptCustomizationProperties): Promise<void> {
435
+ if (properties.directoryPaths !== undefined) {
436
+ this.additionalTemplateDirs.clear();
437
+ for (const path of properties.directoryPaths) {
438
+ this.additionalTemplateDirs.add(path);
439
+ }
440
+ }
441
+
442
+ if (properties.extensions !== undefined) {
443
+ this.templateExtensions.clear();
444
+ for (const ext of properties.extensions) {
445
+ this.templateExtensions.add(ext);
446
+ }
447
+ // Always include the default PROMPT_TEMPLATE_EXTENSION
448
+ this.templateExtensions.add(PROMPT_TEMPLATE_EXTENSION);
449
+ }
450
+
451
+ if (properties.filePaths !== undefined) {
452
+ this.templateFiles.clear();
453
+ for (const path of properties.filePaths) {
454
+ this.templateFiles.add(path);
455
+ }
456
+ }
457
+
458
+ // Only run the update process once, no matter how many properties were changed
459
+ await this.update();
141
460
  }
142
461
 
143
462
  protected async getTemplatesDirectoryURI(): Promise<URI> {
@@ -154,23 +473,25 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
154
473
  }
155
474
 
156
475
  protected removePromptTemplateSuffix(filename: string): string {
157
- const suffix = PROMPT_TEMPLATE_EXTENSION;
158
- if (filename.endsWith(suffix)) {
159
- return filename.slice(0, -suffix.length);
476
+ for (const ext of this.templateExtensions) {
477
+ if (filename.endsWith(ext)) {
478
+ return filename.slice(0, -ext.length);
479
+ }
160
480
  }
161
481
  return filename;
162
482
  }
163
483
 
164
484
  isPromptTemplateCustomized(id: string): boolean {
165
- return this.templates.has(id);
485
+ return this.effectiveTemplates.has(id);
166
486
  }
167
487
 
168
488
  getCustomizedPromptTemplate(id: string): string | undefined {
169
- return this.templates.get(id);
489
+ const entry = this.effectiveTemplates.get(id);
490
+ return entry ? entry.content : undefined;
170
491
  }
171
492
 
172
493
  getCustomPromptTemplateIDs(): string[] {
173
- return Array.from(this.templates.keys());
494
+ return Array.from(this.effectiveTemplates.keys());
174
495
  }
175
496
 
176
497
  async editTemplate(id: string, defaultContent?: string): Promise<void> {
@@ -191,7 +512,7 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
191
512
 
192
513
  getTemplateIDFromURI(uri: URI): string | undefined {
193
514
  const id = this.removePromptTemplateSuffix(uri.path.name);
194
- if (this.templates.has(id)) {
515
+ if (this.effectiveTemplates.has(id)) {
195
516
  return id;
196
517
  }
197
518
  return undefined;
@@ -26,3 +26,4 @@ export * from './prompttemplate-contribution';
26
26
  export * from './theia-variable-contribution';
27
27
  export * from './frontend-variable-service';
28
28
  export * from './ai-core-command-contribution';
29
+ export * from '../common/language-model-service';
@@ -22,8 +22,9 @@ import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/li
22
22
 
23
23
  import { codicon, Widget } from '@theia/core/lib/browser';
24
24
  import { EditorWidget, ReplaceOperation } from '@theia/editor/lib/browser';
25
- import { PromptCustomizationService, PromptService, ToolInvocationRegistry } from '../common';
25
+ import { PromptCustomizationService, PromptService, PromptText, ToolInvocationRegistry } from '../common';
26
26
  import { ProviderResult } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
27
+ import { AIVariableService } from '../common/variable-service';
27
28
 
28
29
  const PROMPT_TEMPLATE_LANGUAGE_ID = 'theia-ai-prompt-template';
29
30
  const PROMPT_TEMPLATE_TEXTMATE_SCOPE = 'source.prompttemplate';
@@ -59,19 +60,28 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
59
60
  @inject(ToolInvocationRegistry)
60
61
  protected readonly toolInvocationRegistry: ToolInvocationRegistry;
61
62
 
63
+ @inject(AIVariableService)
64
+ protected readonly variableService: AIVariableService;
65
+
62
66
  readonly config: monaco.languages.LanguageConfiguration =
63
67
  {
64
68
  'brackets': [
65
69
  ['${', '}'],
66
- ['~{', '}']
70
+ ['~{', '}'],
71
+ ['{{', '}}'],
72
+ ['{{{', '}}}']
67
73
  ],
68
74
  'autoClosingPairs': [
69
75
  { 'open': '${', 'close': '}' },
70
76
  { 'open': '~{', 'close': '}' },
77
+ { 'open': '{{', 'close': '}}' },
78
+ { 'open': '{{{', 'close': '}}}' }
71
79
  ],
72
80
  'surroundingPairs': [
73
81
  { 'open': '${', 'close': '}' },
74
- { 'open': '~{', 'close': '}' }
82
+ { 'open': '~{', 'close': '}' },
83
+ { 'open': '{{', 'close': '}}' },
84
+ { 'open': '{{{', 'close': '}}}' }
75
85
  ]
76
86
  };
77
87
 
@@ -95,6 +105,17 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
95
105
  provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideFunctionCompletions(model, position),
96
106
  });
97
107
 
108
+ monaco.languages.registerCompletionItemProvider(PROMPT_TEMPLATE_LANGUAGE_ID, {
109
+ // Monaco only supports single character trigger characters
110
+ triggerCharacters: ['{'],
111
+ provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideVariableCompletions(model, position),
112
+ });
113
+ monaco.languages.registerCompletionItemProvider(PROMPT_TEMPLATE_LANGUAGE_ID, {
114
+ // Monaco only supports single character trigger characters
115
+ triggerCharacters: ['{', ':'],
116
+ provideCompletionItems: (model, position, _context, _token): ProviderResult<monaco.languages.CompletionList> => this.provideVariableWithArgCompletions(model, position),
117
+ });
118
+
98
119
  const textmateGrammar = require('../../data/prompttemplate.tmLanguage.json');
99
120
  const grammarDefinitionProvider: GrammarDefinitionProvider = {
100
121
  getGrammarDefinition: function (): Promise<GrammarDefinition> {
@@ -122,6 +143,62 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
122
143
  );
123
144
  }
124
145
 
146
+ provideVariableCompletions(model: monaco.editor.ITextModel, position: monaco.Position): ProviderResult<monaco.languages.CompletionList> {
147
+ return this.getSuggestions(
148
+ model,
149
+ position,
150
+ '{{',
151
+ this.variableService.getVariables(),
152
+ monaco.languages.CompletionItemKind.Variable,
153
+ variable => variable.args?.some(arg => !arg.isOptional) ? variable.name + PromptText.VARIABLE_SEPARATOR_CHAR : variable.name,
154
+ variable => variable.name,
155
+ variable => variable.description ?? ''
156
+ );
157
+ }
158
+
159
+ async provideVariableWithArgCompletions(model: monaco.editor.ITextModel, position: monaco.Position): Promise<monaco.languages.CompletionList> {
160
+ // Get the text of the current line up to the cursor position
161
+ const textUntilPosition = model.getValueInRange({
162
+ startLineNumber: position.lineNumber,
163
+ startColumn: 1,
164
+ endLineNumber: position.lineNumber,
165
+ endColumn: position.column,
166
+ });
167
+
168
+ // Regex that captures the variable name in contexts like {{, {{{, {{varname, {{{varname, {{varname:, or {{{varname:
169
+ const variableRegex = /(?:\{\{\{|\{\{)([\w-]+)?(?::)?/;
170
+ const match = textUntilPosition.match(variableRegex);
171
+
172
+ if (!match) {
173
+ return { suggestions: [] };
174
+ }
175
+
176
+ const currentVariableName = match[1];
177
+ const hasColonSeparator = textUntilPosition.includes(`${currentVariableName}:`);
178
+
179
+ const variables = this.variableService.getVariables();
180
+ const suggestions: monaco.languages.CompletionItem[] = [];
181
+
182
+ for (const variable of variables) {
183
+ // If we have a variable:arg pattern, only process the matching variable
184
+ if (hasColonSeparator && variable.name !== currentVariableName) {
185
+ continue;
186
+ }
187
+
188
+ const provider = await this.variableService.getArgumentCompletionProvider(variable.name);
189
+ if (provider) {
190
+ const items = await provider(model, position, '{');
191
+ if (items) {
192
+ suggestions.push(...items.map(item => ({
193
+ ...item
194
+ })));
195
+ }
196
+ }
197
+ }
198
+
199
+ return { suggestions };
200
+ }
201
+
125
202
  getCompletionRange(model: monaco.editor.ITextModel, position: monaco.Position, triggerCharacters: string): monaco.Range | undefined {
126
203
  // Check if the characters before the current position are the trigger characters
127
204
  const lineContent = model.getLineContent(position.lineNumber);