@theia/ai-core 1.66.0-next.44 → 1.66.0-next.73
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/frontend-prompt-customization-service.d.ts +12 -3
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +57 -13
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.spec.d.ts +2 -0
- package/lib/browser/frontend-prompt-customization-service.spec.d.ts.map +1 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js +127 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -0
- package/lib/browser/prompttemplate-parser.d.ts +36 -0
- package/lib/browser/prompttemplate-parser.d.ts.map +1 -0
- package/lib/browser/prompttemplate-parser.js +94 -0
- package/lib/browser/prompttemplate-parser.js.map +1 -0
- package/lib/common/prompt-service.d.ts +27 -1
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +29 -0
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +126 -0
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/prompt-text.d.ts +1 -0
- package/lib/common/prompt-text.d.ts.map +1 -1
- package/lib/common/prompt-text.js +1 -0
- package/lib/common/prompt-text.js.map +1 -1
- package/lib/common/prompt-variable-contribution.d.ts +2 -0
- package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
- package/lib/common/prompt-variable-contribution.js +89 -4
- package/lib/common/prompt-variable-contribution.js.map +1 -1
- package/lib/common/prompt-variable-contribution.spec.d.ts +2 -0
- package/lib/common/prompt-variable-contribution.spec.d.ts.map +1 -0
- package/lib/common/prompt-variable-contribution.spec.js +163 -0
- package/lib/common/prompt-variable-contribution.spec.js.map +1 -0
- package/package.json +9 -9
- package/src/browser/frontend-prompt-customization-service.spec.ts +145 -0
- package/src/browser/frontend-prompt-customization-service.ts +72 -20
- package/src/browser/prompttemplate-parser.ts +111 -0
- package/src/common/prompt-service.spec.ts +143 -0
- package/src/common/prompt-service.ts +73 -1
- package/src/common/prompt-text.ts +1 -0
- package/src/common/prompt-variable-contribution.spec.ts +236 -0
- package/src/common/prompt-variable-contribution.ts +109 -4
|
@@ -17,14 +17,15 @@
|
|
|
17
17
|
import { DisposableCollection, URI, Event, Emitter, nls } 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 { PromptFragmentCustomizationService, CustomAgentDescription, CustomizedPromptFragment } from '../common';
|
|
20
|
+
import { PromptFragmentCustomizationService, CustomAgentDescription, CustomizedPromptFragment, CommandPromptFragmentMetadata } 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';
|
|
24
24
|
import { AICorePreferences, PREFERENCE_NAME_PROMPT_TEMPLATES } from '../common/ai-core-preferences';
|
|
25
25
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
26
|
-
import {
|
|
26
|
+
import { dump, load } from 'js-yaml';
|
|
27
27
|
import { PROMPT_TEMPLATE_EXTENSION } from './prompttemplate-contribution';
|
|
28
|
+
import { parseTemplateWithMetadata, ParsedTemplate } from './prompttemplate-parser';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Default template entry for creating custom agents
|
|
@@ -80,8 +81,9 @@ export interface PromptFragmentCustomizationProperties {
|
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* Internal representation of a fragment entry in the customization service
|
|
84
|
+
* Extends TemplateMetadata to include command-related properties
|
|
83
85
|
*/
|
|
84
|
-
interface PromptFragmentCustomization {
|
|
86
|
+
interface PromptFragmentCustomization extends CommandPromptFragmentMetadata {
|
|
85
87
|
/** The template content */
|
|
86
88
|
template: string;
|
|
87
89
|
|
|
@@ -213,6 +215,7 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
213
215
|
* @param allCustomizationsCopy The map to track all loaded customizations
|
|
214
216
|
* @param priority The customization priority
|
|
215
217
|
* @param origin The source type of the customization
|
|
218
|
+
* @param metadata Optional command metadata
|
|
216
219
|
*/
|
|
217
220
|
protected addTemplate(
|
|
218
221
|
activeCustomizationsCopy: Map<string, PromptFragmentCustomization>,
|
|
@@ -221,14 +224,32 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
221
224
|
sourceUri: string,
|
|
222
225
|
allCustomizationsCopy: Map<string, PromptFragmentCustomization>,
|
|
223
226
|
priority: number,
|
|
224
|
-
origin: CustomizationSource
|
|
227
|
+
origin: CustomizationSource,
|
|
228
|
+
metadata?: CommandPromptFragmentMetadata
|
|
225
229
|
): void {
|
|
226
230
|
// Generate a unique customization ID based on source URI and priority
|
|
227
231
|
const customizationId = this.generateCustomizationId(id, sourceUri);
|
|
228
232
|
|
|
233
|
+
// Create customization object with metadata
|
|
234
|
+
const customization: PromptFragmentCustomization = {
|
|
235
|
+
id,
|
|
236
|
+
template,
|
|
237
|
+
sourceUri,
|
|
238
|
+
priority,
|
|
239
|
+
customizationId,
|
|
240
|
+
origin,
|
|
241
|
+
...(metadata && {
|
|
242
|
+
isCommand: metadata.isCommand,
|
|
243
|
+
commandName: metadata.commandName,
|
|
244
|
+
commandDescription: metadata.commandDescription,
|
|
245
|
+
commandArgumentHint: metadata.commandArgumentHint,
|
|
246
|
+
commandAgents: metadata.commandAgents,
|
|
247
|
+
})
|
|
248
|
+
};
|
|
249
|
+
|
|
229
250
|
// Always add to allCustomizationsCopy to keep track of all customizations including overridden ones
|
|
230
251
|
if (sourceUri) {
|
|
231
|
-
allCustomizationsCopy.set(sourceUri,
|
|
252
|
+
allCustomizationsCopy.set(sourceUri, customization);
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
const existingEntry = activeCustomizationsCopy.get(id);
|
|
@@ -237,13 +258,13 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
237
258
|
// If this is an update to the same file (same source URI)
|
|
238
259
|
if (sourceUri && existingEntry.sourceUri === sourceUri) {
|
|
239
260
|
// Update the content while keeping the same priority and source
|
|
240
|
-
activeCustomizationsCopy.set(id,
|
|
261
|
+
activeCustomizationsCopy.set(id, customization);
|
|
241
262
|
return;
|
|
242
263
|
}
|
|
243
264
|
|
|
244
265
|
// If the new customization has higher priority, replace the existing one
|
|
245
266
|
if (priority > existingEntry.priority) {
|
|
246
|
-
activeCustomizationsCopy.set(id,
|
|
267
|
+
activeCustomizationsCopy.set(id, customization);
|
|
247
268
|
return;
|
|
248
269
|
} else if (priority === existingEntry.priority) {
|
|
249
270
|
// There is a conflict with the same priority, we ignore the new customization
|
|
@@ -254,7 +275,7 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
// No conflict at all, add the customization
|
|
257
|
-
activeCustomizationsCopy.set(id,
|
|
278
|
+
activeCustomizationsCopy.set(id, customization);
|
|
258
279
|
}
|
|
259
280
|
|
|
260
281
|
/**
|
|
@@ -285,6 +306,15 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
285
306
|
return Math.abs(hash).toString(36).substring(0, 8);
|
|
286
307
|
}
|
|
287
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Parses a template file that may contain YAML front matter
|
|
311
|
+
* @param fileContent The raw file content
|
|
312
|
+
* @returns Parsed metadata and template content
|
|
313
|
+
*/
|
|
314
|
+
protected parseTemplateWithMetadata(fileContent: string): ParsedTemplate {
|
|
315
|
+
return parseTemplateWithMetadata(fileContent);
|
|
316
|
+
}
|
|
317
|
+
|
|
288
318
|
/**
|
|
289
319
|
* Removes a customization from customizations maps based on the source URI.
|
|
290
320
|
* Also checks for any lower-priority customizations with the same ID that might need to be loaded.
|
|
@@ -359,7 +389,8 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
359
389
|
if (await this.fileService.exists(fileURI)) {
|
|
360
390
|
trackedTemplateURIsCopy.add(uriString);
|
|
361
391
|
const fileContent = await this.fileService.read(fileURI);
|
|
362
|
-
this.
|
|
392
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
393
|
+
this.addTemplate(activeCustomizationsCopy, fragmentId, parsed.template, uriString, allCustomizationsCopy, priority, CustomizationSource.FILE, parsed.metadata);
|
|
363
394
|
parsedPromptFragments.add(fragmentId);
|
|
364
395
|
}
|
|
365
396
|
}
|
|
@@ -394,14 +425,16 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
394
425
|
|
|
395
426
|
if (fileInfo) {
|
|
396
427
|
const fileContent = await this.fileService.read(fileInfo.uri);
|
|
428
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
397
429
|
this.addTemplate(
|
|
398
430
|
this.activeCustomizations,
|
|
399
431
|
fileInfo.fragmentId,
|
|
400
|
-
|
|
432
|
+
parsed.template,
|
|
401
433
|
fileUriString,
|
|
402
434
|
this.allCustomizations,
|
|
403
435
|
priority,
|
|
404
|
-
CustomizationSource.FILE
|
|
436
|
+
CustomizationSource.FILE,
|
|
437
|
+
parsed.metadata
|
|
405
438
|
);
|
|
406
439
|
changedFragmentIds.add(fileInfo.fragmentId);
|
|
407
440
|
}
|
|
@@ -414,14 +447,16 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
414
447
|
|
|
415
448
|
if (fileInfo) {
|
|
416
449
|
const fileContent = await this.fileService.read(fileInfo.uri);
|
|
450
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
417
451
|
this.addTemplate(
|
|
418
452
|
this.activeCustomizations,
|
|
419
453
|
fileInfo.fragmentId,
|
|
420
|
-
|
|
454
|
+
parsed.template,
|
|
421
455
|
fileUriString,
|
|
422
456
|
this.allCustomizations,
|
|
423
457
|
priority,
|
|
424
|
-
CustomizationSource.FILE
|
|
458
|
+
CustomizationSource.FILE,
|
|
459
|
+
parsed.metadata
|
|
425
460
|
);
|
|
426
461
|
this.trackedTemplateURIs.add(fileUriString);
|
|
427
462
|
changedFragmentIds.add(fileInfo.fragmentId);
|
|
@@ -512,8 +547,9 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
512
547
|
if (this.isPromptTemplateExtension(fileURI.path.ext)) {
|
|
513
548
|
trackedTemplateURIsCopy.add(fileURI.toString());
|
|
514
549
|
const fileContent = await this.fileService.read(fileURI);
|
|
550
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
515
551
|
const fragmentId = this.removePromptTemplateSuffix(file.name);
|
|
516
|
-
this.addTemplate(activeCustomizationsCopy, fragmentId,
|
|
552
|
+
this.addTemplate(activeCustomizationsCopy, fragmentId, parsed.template, fileURI.toString(), allCustomizationsCopy, priority, customizationSource, parsed.metadata);
|
|
517
553
|
parsedPromptFragments.add(fragmentId);
|
|
518
554
|
}
|
|
519
555
|
}
|
|
@@ -575,15 +611,17 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
575
611
|
const uriString = updatedFile.resource.toString();
|
|
576
612
|
if (this.trackedTemplateURIs.has(uriString)) {
|
|
577
613
|
const fileContent = await this.fileService.read(updatedFile.resource);
|
|
614
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
578
615
|
const fragmentId = this.removePromptTemplateSuffix(updatedFile.resource.path.name);
|
|
579
616
|
this.addTemplate(
|
|
580
617
|
this.activeCustomizations,
|
|
581
618
|
fragmentId,
|
|
582
|
-
|
|
619
|
+
parsed.template,
|
|
583
620
|
uriString,
|
|
584
621
|
this.allCustomizations,
|
|
585
622
|
priority,
|
|
586
|
-
customizationSource
|
|
623
|
+
customizationSource,
|
|
624
|
+
parsed.metadata
|
|
587
625
|
);
|
|
588
626
|
changedFragmentIds.add(fragmentId);
|
|
589
627
|
}
|
|
@@ -596,15 +634,17 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
596
634
|
const uriString = addedFile.resource.toString();
|
|
597
635
|
this.trackedTemplateURIs.add(uriString);
|
|
598
636
|
const fileContent = await this.fileService.read(addedFile.resource);
|
|
637
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
599
638
|
const fragmentId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
|
|
600
639
|
this.addTemplate(
|
|
601
640
|
this.activeCustomizations,
|
|
602
641
|
fragmentId,
|
|
603
|
-
|
|
642
|
+
parsed.template,
|
|
604
643
|
uriString,
|
|
605
644
|
this.allCustomizations,
|
|
606
645
|
priority,
|
|
607
|
-
customizationSource
|
|
646
|
+
customizationSource,
|
|
647
|
+
parsed.metadata
|
|
608
648
|
);
|
|
609
649
|
changedFragmentIds.add(fragmentId);
|
|
610
650
|
}
|
|
@@ -735,7 +775,13 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
735
775
|
id: entry.id,
|
|
736
776
|
template: entry.template,
|
|
737
777
|
customizationId: entry.customizationId,
|
|
738
|
-
priority: entry.priority
|
|
778
|
+
priority: entry.priority,
|
|
779
|
+
// Pass through command metadata
|
|
780
|
+
isCommand: entry.isCommand,
|
|
781
|
+
commandName: entry.commandName,
|
|
782
|
+
commandDescription: entry.commandDescription,
|
|
783
|
+
commandArgumentHint: entry.commandArgumentHint,
|
|
784
|
+
commandAgents: entry.commandAgents,
|
|
739
785
|
};
|
|
740
786
|
}
|
|
741
787
|
|
|
@@ -749,7 +795,13 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
749
795
|
id: value.id,
|
|
750
796
|
template: value.template,
|
|
751
797
|
customizationId: value.customizationId,
|
|
752
|
-
priority: value.priority
|
|
798
|
+
priority: value.priority,
|
|
799
|
+
// Pass through command metadata
|
|
800
|
+
isCommand: value.isCommand,
|
|
801
|
+
commandName: value.commandName,
|
|
802
|
+
commandDescription: value.commandDescription,
|
|
803
|
+
commandArgumentHint: value.commandArgumentHint,
|
|
804
|
+
commandAgents: value.commandAgents,
|
|
753
805
|
});
|
|
754
806
|
}
|
|
755
807
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { load } from 'js-yaml';
|
|
18
|
+
import { CommandPromptFragmentMetadata } from '../common';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of parsing a template file that may contain YAML front matter
|
|
22
|
+
*/
|
|
23
|
+
export interface ParsedTemplate {
|
|
24
|
+
/** The template content (without front matter) */
|
|
25
|
+
template: string;
|
|
26
|
+
|
|
27
|
+
/** Parsed metadata from YAML front matter, if present */
|
|
28
|
+
metadata?: CommandPromptFragmentMetadata;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Type guard to check if an object is valid TemplateMetadata
|
|
33
|
+
*/
|
|
34
|
+
export function isTemplateMetadata(obj: unknown): obj is CommandPromptFragmentMetadata {
|
|
35
|
+
if (!obj || typeof obj !== 'object') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const metadata = obj as Record<string, unknown>;
|
|
39
|
+
return (
|
|
40
|
+
(metadata.isCommand === undefined || typeof metadata.isCommand === 'boolean') &&
|
|
41
|
+
(metadata.commandName === undefined || typeof metadata.commandName === 'string') &&
|
|
42
|
+
(metadata.commandDescription === undefined || typeof metadata.commandDescription === 'string') &&
|
|
43
|
+
(metadata.commandArgumentHint === undefined || typeof metadata.commandArgumentHint === 'string') &&
|
|
44
|
+
(metadata.commandAgents === undefined || (Array.isArray(metadata.commandAgents) &&
|
|
45
|
+
metadata.commandAgents.every(agent => typeof agent === 'string')))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parses a template file that may contain YAML front matter.
|
|
51
|
+
*
|
|
52
|
+
* Front matter format:
|
|
53
|
+
* ```
|
|
54
|
+
* ---
|
|
55
|
+
* isCommand: true
|
|
56
|
+
* commandName: mycommand
|
|
57
|
+
* commandDescription: My command description
|
|
58
|
+
* commandArgumentHint: <arg1> <arg2>
|
|
59
|
+
* commandAgents:
|
|
60
|
+
* - Agent1
|
|
61
|
+
* - Agent2
|
|
62
|
+
* ---
|
|
63
|
+
* Template content here
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @param fileContent The raw file content to parse
|
|
67
|
+
* @returns ParsedTemplate containing the template content and optional metadata
|
|
68
|
+
*/
|
|
69
|
+
export function parseTemplateWithMetadata(fileContent: string): ParsedTemplate {
|
|
70
|
+
const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
71
|
+
const match = fileContent.match(frontMatterRegex);
|
|
72
|
+
|
|
73
|
+
if (!match) {
|
|
74
|
+
// No front matter, return content as-is
|
|
75
|
+
return { template: fileContent };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const yamlContent = match[1];
|
|
80
|
+
const template = match[2];
|
|
81
|
+
const parsedYaml = load(yamlContent);
|
|
82
|
+
|
|
83
|
+
// Validate the parsed YAML is an object
|
|
84
|
+
if (!parsedYaml || typeof parsedYaml !== 'object') {
|
|
85
|
+
return { template: fileContent };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const metadata = parsedYaml as Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
// Extract and validate command metadata
|
|
91
|
+
const templateMetadata: CommandPromptFragmentMetadata = {
|
|
92
|
+
isCommand: typeof metadata.isCommand === 'boolean' ? metadata.isCommand : undefined,
|
|
93
|
+
commandName: typeof metadata.commandName === 'string' ? metadata.commandName : undefined,
|
|
94
|
+
commandDescription: typeof metadata.commandDescription === 'string' ? metadata.commandDescription : undefined,
|
|
95
|
+
commandArgumentHint: typeof metadata.commandArgumentHint === 'string' ? metadata.commandArgumentHint : undefined,
|
|
96
|
+
commandAgents: Array.isArray(metadata.commandAgents) ? metadata.commandAgents.filter(a => typeof a === 'string') : undefined,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Only include metadata if it's valid
|
|
100
|
+
if (isTemplateMetadata(templateMetadata)) {
|
|
101
|
+
return { template, metadata: templateMetadata };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Metadata validation failed, return just the template
|
|
105
|
+
return { template };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Failed to parse front matter:', error);
|
|
108
|
+
// Return entire content if YAML parsing fails
|
|
109
|
+
return { template: fileContent };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -362,4 +362,147 @@ describe('PromptService', () => {
|
|
|
362
362
|
// Verify that the tool invocation registry was called
|
|
363
363
|
expect(toolInvocationRegistry.getFunction.calledWith('testFunction')).to.be.true;
|
|
364
364
|
});
|
|
365
|
+
|
|
366
|
+
// ===== Command Tests =====
|
|
367
|
+
|
|
368
|
+
describe('Command Management', () => {
|
|
369
|
+
it('getCommands() returns only fragments with isCommand=true', () => {
|
|
370
|
+
promptService.addBuiltInPromptFragment({
|
|
371
|
+
id: 'cmd1',
|
|
372
|
+
template: 'Command 1',
|
|
373
|
+
isCommand: true,
|
|
374
|
+
commandName: 'cmd1'
|
|
375
|
+
});
|
|
376
|
+
promptService.addBuiltInPromptFragment({
|
|
377
|
+
id: 'normal',
|
|
378
|
+
template: 'Normal prompt'
|
|
379
|
+
});
|
|
380
|
+
promptService.addBuiltInPromptFragment({
|
|
381
|
+
id: 'cmd2',
|
|
382
|
+
template: 'Command 2',
|
|
383
|
+
isCommand: true,
|
|
384
|
+
commandName: 'cmd2'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const commands = promptService.getCommands();
|
|
388
|
+
expect(commands.length).to.equal(2);
|
|
389
|
+
expect(commands.map(c => c.id)).to.include('cmd1');
|
|
390
|
+
expect(commands.map(c => c.id)).to.include('cmd2');
|
|
391
|
+
expect(commands.map(c => c.id)).to.not.include('normal');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('getCommands(agentId) filters by commandAgents array', () => {
|
|
395
|
+
promptService.addBuiltInPromptFragment({
|
|
396
|
+
id: 'cmd-universal',
|
|
397
|
+
template: 'Universal command',
|
|
398
|
+
isCommand: true,
|
|
399
|
+
commandName: 'universal',
|
|
400
|
+
commandAgents: ['Universal']
|
|
401
|
+
});
|
|
402
|
+
promptService.addBuiltInPromptFragment({
|
|
403
|
+
id: 'cmd-specific',
|
|
404
|
+
template: 'Specific command',
|
|
405
|
+
isCommand: true,
|
|
406
|
+
commandName: 'specific',
|
|
407
|
+
commandAgents: ['SpecificAgent']
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const universalCommands = promptService.getCommands('Universal');
|
|
411
|
+
expect(universalCommands.length).to.equal(1);
|
|
412
|
+
expect(universalCommands[0].id).to.equal('cmd-universal');
|
|
413
|
+
|
|
414
|
+
const specificCommands = promptService.getCommands('SpecificAgent');
|
|
415
|
+
expect(specificCommands.length).to.equal(1);
|
|
416
|
+
expect(specificCommands[0].id).to.equal('cmd-specific');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('getCommands(agentId) includes commands without commandAgents', () => {
|
|
420
|
+
promptService.addBuiltInPromptFragment({
|
|
421
|
+
id: 'cmd-all',
|
|
422
|
+
template: 'Available for all',
|
|
423
|
+
isCommand: true,
|
|
424
|
+
commandName: 'all'
|
|
425
|
+
// No commandAgents means available for all
|
|
426
|
+
});
|
|
427
|
+
promptService.addBuiltInPromptFragment({
|
|
428
|
+
id: 'cmd-specific',
|
|
429
|
+
template: 'Specific command',
|
|
430
|
+
isCommand: true,
|
|
431
|
+
commandName: 'specific',
|
|
432
|
+
commandAgents: ['Universal']
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const commands = promptService.getCommands('SomeOtherAgent');
|
|
436
|
+
expect(commands.length).to.equal(1);
|
|
437
|
+
expect(commands[0].id).to.equal('cmd-all');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('getCommands() returns empty array when no commands registered', () => {
|
|
441
|
+
promptService.addBuiltInPromptFragment({
|
|
442
|
+
id: 'normal1',
|
|
443
|
+
template: 'Normal prompt 1'
|
|
444
|
+
});
|
|
445
|
+
promptService.addBuiltInPromptFragment({
|
|
446
|
+
id: 'normal2',
|
|
447
|
+
template: 'Normal prompt 2'
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const commands = promptService.getCommands();
|
|
451
|
+
expect(commands.length).to.equal(0);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('command metadata preserved through registration', () => {
|
|
455
|
+
promptService.addBuiltInPromptFragment({
|
|
456
|
+
id: 'test-cmd',
|
|
457
|
+
template: 'Test command',
|
|
458
|
+
isCommand: true,
|
|
459
|
+
commandName: 'test',
|
|
460
|
+
commandDescription: 'A test command',
|
|
461
|
+
commandArgumentHint: '<arg>',
|
|
462
|
+
commandAgents: ['Agent1', 'Agent2']
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const commands = promptService.getCommands();
|
|
466
|
+
expect(commands.length).to.equal(1);
|
|
467
|
+
const cmd = commands[0];
|
|
468
|
+
expect(cmd.isCommand).to.be.true;
|
|
469
|
+
expect(cmd.commandName).to.equal('test');
|
|
470
|
+
expect(cmd.commandDescription).to.equal('A test command');
|
|
471
|
+
expect(cmd.commandArgumentHint).to.equal('<arg>');
|
|
472
|
+
expect(cmd.commandAgents).to.deep.equal(['Agent1', 'Agent2']);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('getFragmentByCommandName finds fragment by command name', () => {
|
|
476
|
+
promptService.addBuiltInPromptFragment({
|
|
477
|
+
id: 'sample-debug',
|
|
478
|
+
template: 'Help debug: $ARGUMENTS',
|
|
479
|
+
isCommand: true,
|
|
480
|
+
commandName: 'debug',
|
|
481
|
+
commandDescription: 'Debug an issue',
|
|
482
|
+
commandArgumentHint: '<problem>'
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Should find by command name
|
|
486
|
+
const fragment = promptService.getPromptFragmentByCommandName('debug');
|
|
487
|
+
expect(fragment).to.not.be.undefined;
|
|
488
|
+
expect(fragment?.id).to.equal('sample-debug');
|
|
489
|
+
expect(fragment?.commandName).to.equal('debug');
|
|
490
|
+
expect(fragment?.template).to.equal('Help debug: $ARGUMENTS');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('getFragmentByCommandName returns undefined for non-command fragments', () => {
|
|
494
|
+
promptService.addBuiltInPromptFragment({
|
|
495
|
+
id: 'normal-fragment',
|
|
496
|
+
template: 'Not a command'
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const fragment = promptService.getPromptFragmentByCommandName('normal-fragment');
|
|
500
|
+
expect(fragment).to.be.undefined;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('getFragmentByCommandName returns undefined for non-existent command', () => {
|
|
504
|
+
const fragment = promptService.getPromptFragmentByCommandName('non-existent');
|
|
505
|
+
expect(fragment).to.be.undefined;
|
|
506
|
+
});
|
|
507
|
+
});
|
|
365
508
|
});
|
|
@@ -23,10 +23,27 @@ import { ToolRequest } from './language-model';
|
|
|
23
23
|
import { matchFunctionsRegEx, matchVariablesRegEx } from './prompt-service-util';
|
|
24
24
|
import { AISettingsService } from './settings-service';
|
|
25
25
|
|
|
26
|
+
export interface CommandPromptFragmentMetadata {
|
|
27
|
+
/** Mark this template as available as a slash command */
|
|
28
|
+
isCommand?: boolean;
|
|
29
|
+
|
|
30
|
+
/** Display name for the command (defaults to fragment id if not specified) */
|
|
31
|
+
commandName?: string;
|
|
32
|
+
|
|
33
|
+
/** Description shown in command autocomplete */
|
|
34
|
+
commandDescription?: string;
|
|
35
|
+
|
|
36
|
+
/** Hint for command arguments shown in autocomplete detail (e.g., "<topic>", "[options]") */
|
|
37
|
+
commandArgumentHint?: string;
|
|
38
|
+
|
|
39
|
+
/** List of agent IDs this command is available for (undefined means available for all agents) */
|
|
40
|
+
commandAgents?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
/**
|
|
27
44
|
* Represents a basic prompt fragment with an ID and template content.
|
|
28
45
|
*/
|
|
29
|
-
export interface BasePromptFragment {
|
|
46
|
+
export interface BasePromptFragment extends CommandPromptFragmentMetadata {
|
|
30
47
|
/** Unique identifier for this prompt fragment */
|
|
31
48
|
id: string;
|
|
32
49
|
|
|
@@ -306,6 +323,13 @@ export interface PromptService {
|
|
|
306
323
|
*/
|
|
307
324
|
getBuiltInRawPrompt(fragmentId: string): PromptFragment | undefined;
|
|
308
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Gets a prompt fragment by command name (for slash commands)
|
|
328
|
+
* @param commandName The command name to search for
|
|
329
|
+
* @returns The fragment with the matching command name or undefined if not found
|
|
330
|
+
*/
|
|
331
|
+
getPromptFragmentByCommandName(commandName: string): PromptFragment | undefined;
|
|
332
|
+
|
|
309
333
|
/**
|
|
310
334
|
* Resolves a prompt fragment by replacing variables and function references
|
|
311
335
|
* @param fragmentId The prompt fragment ID
|
|
@@ -399,6 +423,13 @@ export interface PromptService {
|
|
|
399
423
|
*/
|
|
400
424
|
getPromptVariantSets(): Map<string, string[]>;
|
|
401
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Gets all prompt fragments marked as commands, optionally filtered by agent
|
|
428
|
+
* @param agentId Optional agent ID to filter commands (undefined returns commands for all agents)
|
|
429
|
+
* @returns Array of command prompt fragments
|
|
430
|
+
*/
|
|
431
|
+
getCommands(agentId?: string): PromptFragment[];
|
|
432
|
+
|
|
402
433
|
/**
|
|
403
434
|
* The following methods delegate to the PromptFragmentCustomizationService
|
|
404
435
|
*/
|
|
@@ -559,6 +590,24 @@ export class PromptServiceImpl implements PromptService {
|
|
|
559
590
|
};
|
|
560
591
|
}
|
|
561
592
|
|
|
593
|
+
getPromptFragmentByCommandName(commandName: string): PromptFragment | undefined {
|
|
594
|
+
// First check customized fragments
|
|
595
|
+
if (this.customizationService) {
|
|
596
|
+
const customizedIds = this.customizationService.getCustomizedPromptFragmentIds();
|
|
597
|
+
for (const fragmentId of customizedIds) {
|
|
598
|
+
const fragment = this.customizationService.getActivePromptFragmentCustomization(fragmentId);
|
|
599
|
+
if (fragment?.isCommand && fragment.commandName === commandName) {
|
|
600
|
+
return fragment;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Then check built-in fragments
|
|
606
|
+
return this._builtInFragments.find(fragment =>
|
|
607
|
+
fragment.isCommand && fragment.commandName === commandName
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
562
611
|
/**
|
|
563
612
|
* Strips comments from a template string
|
|
564
613
|
* @param templateText The template text to process
|
|
@@ -888,6 +937,19 @@ export class PromptServiceImpl implements PromptService {
|
|
|
888
937
|
this._builtInFragments.push(promptFragment);
|
|
889
938
|
}
|
|
890
939
|
|
|
940
|
+
// Validate command name uniqueness if this is a command
|
|
941
|
+
if (promptFragment.isCommand && promptFragment.commandName) {
|
|
942
|
+
const commandName = promptFragment.commandName;
|
|
943
|
+
const duplicates = this._builtInFragments.filter(
|
|
944
|
+
f => f.isCommand && f.commandName === commandName && f.id !== promptFragment.id
|
|
945
|
+
);
|
|
946
|
+
if (duplicates.length > 0) {
|
|
947
|
+
this.logger.warn(
|
|
948
|
+
`Command name '${commandName}' is used by multiple fragments: ${promptFragment.id} and ${duplicates.map(d => d.id).join(', ')}`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
891
953
|
// If this is a variant of a prompt variant set, record it in the variants map
|
|
892
954
|
if (promptVariantSetId) {
|
|
893
955
|
this.addFragmentVariant(promptVariantSetId, promptFragment.id, isDefault);
|
|
@@ -1042,4 +1104,14 @@ export class PromptServiceImpl implements PromptService {
|
|
|
1042
1104
|
await this.customizationService.editBuiltInPromptFragmentCustomization(fragmentId, builtInTemplate?.template);
|
|
1043
1105
|
}
|
|
1044
1106
|
}
|
|
1107
|
+
|
|
1108
|
+
getCommands(agentId?: string): PromptFragment[] {
|
|
1109
|
+
const allCommands = this.getActivePromptFragments().filter(fragment => fragment.isCommand === true);
|
|
1110
|
+
|
|
1111
|
+
if (!agentId) {
|
|
1112
|
+
return allCommands;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return allCommands.filter(fragment => !fragment.commandAgents || fragment.commandAgents.includes(agentId));
|
|
1116
|
+
}
|
|
1045
1117
|
}
|