@theia/ai-core 1.66.0-next.44 → 1.66.0-next.67

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 (39) hide show
  1. package/lib/browser/frontend-prompt-customization-service.d.ts +12 -3
  2. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  3. package/lib/browser/frontend-prompt-customization-service.js +57 -13
  4. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  5. package/lib/browser/frontend-prompt-customization-service.spec.d.ts +2 -0
  6. package/lib/browser/frontend-prompt-customization-service.spec.d.ts.map +1 -0
  7. package/lib/browser/frontend-prompt-customization-service.spec.js +127 -0
  8. package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -0
  9. package/lib/browser/prompttemplate-parser.d.ts +36 -0
  10. package/lib/browser/prompttemplate-parser.d.ts.map +1 -0
  11. package/lib/browser/prompttemplate-parser.js +94 -0
  12. package/lib/browser/prompttemplate-parser.js.map +1 -0
  13. package/lib/common/prompt-service.d.ts +27 -1
  14. package/lib/common/prompt-service.d.ts.map +1 -1
  15. package/lib/common/prompt-service.js +29 -0
  16. package/lib/common/prompt-service.js.map +1 -1
  17. package/lib/common/prompt-service.spec.js +126 -0
  18. package/lib/common/prompt-service.spec.js.map +1 -1
  19. package/lib/common/prompt-text.d.ts +1 -0
  20. package/lib/common/prompt-text.d.ts.map +1 -1
  21. package/lib/common/prompt-text.js +1 -0
  22. package/lib/common/prompt-text.js.map +1 -1
  23. package/lib/common/prompt-variable-contribution.d.ts +2 -0
  24. package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
  25. package/lib/common/prompt-variable-contribution.js +89 -4
  26. package/lib/common/prompt-variable-contribution.js.map +1 -1
  27. package/lib/common/prompt-variable-contribution.spec.d.ts +2 -0
  28. package/lib/common/prompt-variable-contribution.spec.d.ts.map +1 -0
  29. package/lib/common/prompt-variable-contribution.spec.js +163 -0
  30. package/lib/common/prompt-variable-contribution.spec.js.map +1 -0
  31. package/package.json +9 -9
  32. package/src/browser/frontend-prompt-customization-service.spec.ts +145 -0
  33. package/src/browser/frontend-prompt-customization-service.ts +72 -20
  34. package/src/browser/prompttemplate-parser.ts +111 -0
  35. package/src/common/prompt-service.spec.ts +143 -0
  36. package/src/common/prompt-service.ts +73 -1
  37. package/src/common/prompt-text.ts +1 -0
  38. package/src/common/prompt-variable-contribution.spec.ts +236 -0
  39. 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 { load, dump } from 'js-yaml';
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, { id, template, sourceUri, priority, customizationId, origin });
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, { id, template, sourceUri, priority, customizationId, origin });
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, { id, template, sourceUri, priority, customizationId, origin });
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, { id, template, sourceUri, priority, customizationId, origin });
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.addTemplate(activeCustomizationsCopy, fragmentId, fileContent.value, uriString, allCustomizationsCopy, priority, CustomizationSource.FILE);
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
- fileContent.value,
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
- fileContent.value,
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, fileContent.value, fileURI.toString(), allCustomizationsCopy, priority, customizationSource);
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
- fileContent.value,
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
- fileContent.value,
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
  }
@@ -19,4 +19,5 @@ export namespace PromptText {
19
19
  export const VARIABLE_CHAR = '#';
20
20
  export const FUNCTION_CHAR = '~';
21
21
  export const VARIABLE_SEPARATOR_CHAR = ':';
22
+ export const COMMAND_CHAR = '/';
22
23
  }