@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.
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +4 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-core-preferences.d.ts +22 -8
- package/lib/browser/ai-core-preferences.d.ts.map +1 -1
- package/lib/browser/ai-core-preferences.js +90 -10
- package/lib/browser/ai-core-preferences.js.map +1 -1
- package/lib/browser/frontend-language-model-registry.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-registry.js +1 -1
- package/lib/browser/frontend-language-model-registry.js.map +1 -1
- package/lib/browser/frontend-language-model-service.d.ts +10 -0
- package/lib/browser/frontend-language-model-service.d.ts.map +1 -0
- package/lib/browser/frontend-language-model-service.js +66 -0
- package/lib/browser/frontend-language-model-service.js.map +1 -0
- package/lib/browser/frontend-prompt-customization-service.d.ts +99 -2
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +245 -23
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/prompttemplate-contribution.d.ts +4 -0
- package/lib/browser/prompttemplate-contribution.d.ts.map +1 -1
- package/lib/browser/prompttemplate-contribution.js +61 -2
- package/lib/browser/prompttemplate-contribution.js.map +1 -1
- package/lib/browser/theia-variable-contribution.d.ts +9 -4
- package/lib/browser/theia-variable-contribution.d.ts.map +1 -1
- package/lib/browser/theia-variable-contribution.js +67 -38
- package/lib/browser/theia-variable-contribution.js.map +1 -1
- package/lib/common/communication-recording-service.d.ts +8 -7
- package/lib/common/communication-recording-service.d.ts.map +1 -1
- package/lib/common/communication-recording-service.js.map +1 -1
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-service.d.ts +15 -0
- package/lib/common/language-model-service.d.ts.map +1 -0
- package/lib/common/language-model-service.js +51 -0
- package/lib/common/language-model-service.js.map +1 -0
- package/lib/common/language-model-util.d.ts.map +1 -1
- package/lib/common/language-model-util.js +1 -2
- package/lib/common/language-model-util.js.map +1 -1
- package/lib/common/language-model.d.ts +56 -15
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +27 -2
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/tool-invocation-registry.d.ts +3 -0
- package/lib/common/tool-invocation-registry.d.ts.map +1 -1
- package/lib/common/tool-invocation-registry.js +7 -1
- package/lib/common/tool-invocation-registry.js.map +1 -1
- package/lib/common/variable-service.d.ts +1 -1
- package/lib/common/variable-service.d.ts.map +1 -1
- package/lib/common/variable-service.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/ai-core-frontend-module.ts +4 -0
- package/src/browser/ai-core-preferences.ts +106 -15
- package/src/browser/frontend-language-model-registry.ts +2 -1
- package/src/browser/frontend-language-model-service.ts +67 -0
- package/src/browser/frontend-prompt-customization-service.ts +348 -27
- package/src/browser/index.ts +1 -0
- package/src/browser/prompttemplate-contribution.ts +80 -3
- package/src/browser/theia-variable-contribution.ts +81 -40
- package/src/common/communication-recording-service.ts +9 -7
- package/src/common/index.ts +1 -0
- package/src/common/language-model-service.ts +59 -0
- package/src/common/language-model-util.ts +9 -2
- package/src/common/language-model.ts +72 -15
- package/src/common/tool-invocation-registry.ts +7 -2
- 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
|
-
|
|
53
|
-
protected
|
|
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
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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() ===
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
485
|
+
return this.effectiveTemplates.has(id);
|
|
166
486
|
}
|
|
167
487
|
|
|
168
488
|
getCustomizedPromptTemplate(id: string): string | undefined {
|
|
169
|
-
|
|
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.
|
|
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.
|
|
515
|
+
if (this.effectiveTemplates.has(id)) {
|
|
195
516
|
return id;
|
|
196
517
|
}
|
|
197
518
|
return undefined;
|
package/src/browser/index.ts
CHANGED
|
@@ -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);
|