@theia/ai-core 1.73.0-next.7 → 1.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +2 -2
  2. package/lib/browser/{agent-completion-notification-service.d.ts → agent-notification-service.d.ts} +12 -10
  3. package/lib/browser/agent-notification-service.d.ts.map +1 -0
  4. package/lib/browser/{agent-completion-notification-service.js → agent-notification-service.js} +37 -27
  5. package/lib/browser/agent-notification-service.js.map +1 -0
  6. package/lib/browser/ai-core-frontend-module.js +2 -2
  7. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  8. package/lib/browser/frontend-language-model-alias-registry.js +3 -3
  9. package/lib/browser/frontend-prompt-customization-service.d.ts +193 -16
  10. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  11. package/lib/browser/frontend-prompt-customization-service.js +686 -46
  12. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  13. package/lib/browser/frontend-prompt-customization-service.spec.js +259 -0
  14. package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
  15. package/lib/browser/frontend-variable-service.d.ts +2 -1
  16. package/lib/browser/frontend-variable-service.d.ts.map +1 -1
  17. package/lib/browser/frontend-variable-service.js +6 -1
  18. package/lib/browser/frontend-variable-service.js.map +1 -1
  19. package/lib/browser/index.d.ts +1 -1
  20. package/lib/browser/index.d.ts.map +1 -1
  21. package/lib/browser/index.js +1 -1
  22. package/lib/browser/index.js.map +1 -1
  23. package/lib/browser/os-notification-service.d.ts +10 -6
  24. package/lib/browser/os-notification-service.d.ts.map +1 -1
  25. package/lib/browser/os-notification-service.js +34 -20
  26. package/lib/browser/os-notification-service.js.map +1 -1
  27. package/lib/browser/prompt-variable-contribution.spec.js +2 -1
  28. package/lib/browser/prompt-variable-contribution.spec.js.map +1 -1
  29. package/lib/browser/skill-service.d.ts +6 -1
  30. package/lib/browser/skill-service.d.ts.map +1 -1
  31. package/lib/browser/skill-service.js +47 -18
  32. package/lib/browser/skill-service.js.map +1 -1
  33. package/lib/browser/skill-service.spec.js +153 -31
  34. package/lib/browser/skill-service.spec.js.map +1 -1
  35. package/lib/browser/token-usage-frontend-service-impl.d.ts +2 -0
  36. package/lib/browser/token-usage-frontend-service-impl.d.ts.map +1 -1
  37. package/lib/browser/token-usage-frontend-service-impl.js +6 -1
  38. package/lib/browser/token-usage-frontend-service-impl.js.map +1 -1
  39. package/lib/browser/window-blink-service.d.ts +2 -0
  40. package/lib/browser/window-blink-service.d.ts.map +1 -1
  41. package/lib/browser/window-blink-service.js +8 -3
  42. package/lib/browser/window-blink-service.js.map +1 -1
  43. package/lib/common/agent-preferences.d.ts.map +1 -1
  44. package/lib/common/agent-preferences.js +12 -2
  45. package/lib/common/agent-preferences.js.map +1 -1
  46. package/lib/common/ai-core-preferences.d.ts.map +1 -1
  47. package/lib/common/ai-core-preferences.js +4 -2
  48. package/lib/common/ai-core-preferences.js.map +1 -1
  49. package/lib/common/capability-variable-contribution.spec.js +2 -1
  50. package/lib/common/capability-variable-contribution.spec.js.map +1 -1
  51. package/lib/common/frontmatter.d.ts +33 -0
  52. package/lib/common/frontmatter.d.ts.map +1 -0
  53. package/lib/common/frontmatter.js +80 -0
  54. package/lib/common/frontmatter.js.map +1 -0
  55. package/lib/common/frontmatter.spec.d.ts +2 -0
  56. package/lib/common/frontmatter.spec.d.ts.map +1 -0
  57. package/lib/common/frontmatter.spec.js +73 -0
  58. package/lib/common/frontmatter.spec.js.map +1 -0
  59. package/lib/common/index.d.ts +1 -0
  60. package/lib/common/index.d.ts.map +1 -1
  61. package/lib/common/index.js +1 -0
  62. package/lib/common/index.js.map +1 -1
  63. package/lib/common/language-model-service.js +1 -1
  64. package/lib/common/language-model-service.js.map +1 -1
  65. package/lib/common/language-model-service.spec.d.ts +2 -0
  66. package/lib/common/language-model-service.spec.d.ts.map +1 -0
  67. package/lib/common/language-model-service.spec.js +62 -0
  68. package/lib/common/language-model-service.spec.js.map +1 -0
  69. package/lib/common/language-model.d.ts +64 -2
  70. package/lib/common/language-model.d.ts.map +1 -1
  71. package/lib/common/language-model.js +8 -2
  72. package/lib/common/language-model.js.map +1 -1
  73. package/lib/common/notification-types.d.ts +7 -0
  74. package/lib/common/notification-types.d.ts.map +1 -1
  75. package/lib/common/notification-types.js +7 -1
  76. package/lib/common/notification-types.js.map +1 -1
  77. package/lib/common/prompt-service-util.d.ts +22 -1
  78. package/lib/common/prompt-service-util.d.ts.map +1 -1
  79. package/lib/common/prompt-service-util.js +24 -2
  80. package/lib/common/prompt-service-util.js.map +1 -1
  81. package/lib/common/prompt-service-util.spec.d.ts +2 -0
  82. package/lib/common/prompt-service-util.spec.d.ts.map +1 -0
  83. package/lib/common/prompt-service-util.spec.js +54 -0
  84. package/lib/common/prompt-service-util.spec.js.map +1 -0
  85. package/lib/common/prompt-service.d.ts +72 -8
  86. package/lib/common/prompt-service.d.ts.map +1 -1
  87. package/lib/common/prompt-service.js +36 -4
  88. package/lib/common/prompt-service.js.map +1 -1
  89. package/lib/common/prompt-service.spec.js +49 -2
  90. package/lib/common/prompt-service.spec.js.map +1 -1
  91. package/lib/common/settings-service.d.ts +5 -0
  92. package/lib/common/settings-service.d.ts.map +1 -1
  93. package/lib/common/skill.d.ts +17 -4
  94. package/lib/common/skill.d.ts.map +1 -1
  95. package/lib/common/skill.js +22 -35
  96. package/lib/common/skill.js.map +1 -1
  97. package/lib/common/tool-invocation-registry.d.ts +7 -0
  98. package/lib/common/tool-invocation-registry.d.ts.map +1 -1
  99. package/lib/common/tool-invocation-registry.js +5 -0
  100. package/lib/common/tool-invocation-registry.js.map +1 -1
  101. package/lib/common/variable-service.d.ts +2 -2
  102. package/lib/common/variable-service.d.ts.map +1 -1
  103. package/lib/common/variable-service.js +7 -4
  104. package/lib/common/variable-service.js.map +1 -1
  105. package/lib/common/variable-service.spec.js +2 -1
  106. package/lib/common/variable-service.spec.js.map +1 -1
  107. package/lib/node/backend-language-model-registry.d.ts.map +1 -1
  108. package/lib/node/backend-language-model-registry.js +1 -0
  109. package/lib/node/backend-language-model-registry.js.map +1 -1
  110. package/lib/node/backend-language-model-registry.spec.d.ts +2 -0
  111. package/lib/node/backend-language-model-registry.spec.d.ts.map +1 -0
  112. package/lib/node/backend-language-model-registry.spec.js +48 -0
  113. package/lib/node/backend-language-model-registry.spec.js.map +1 -0
  114. package/lib/node/token-usage-service-impl.d.ts +2 -0
  115. package/lib/node/token-usage-service-impl.d.ts.map +1 -1
  116. package/lib/node/token-usage-service-impl.js +7 -1
  117. package/lib/node/token-usage-service-impl.js.map +1 -1
  118. package/package.json +12 -12
  119. package/src/browser/{agent-completion-notification-service.ts → agent-notification-service.ts} +52 -27
  120. package/src/browser/ai-core-frontend-module.ts +2 -2
  121. package/src/browser/frontend-language-model-alias-registry.ts +3 -3
  122. package/src/browser/frontend-prompt-customization-service.spec.ts +319 -1
  123. package/src/browser/frontend-prompt-customization-service.ts +810 -59
  124. package/src/browser/frontend-variable-service.ts +5 -3
  125. package/src/browser/index.ts +1 -1
  126. package/src/browser/os-notification-service.ts +39 -24
  127. package/src/browser/prompt-variable-contribution.spec.ts +2 -1
  128. package/src/browser/skill-service.spec.ts +191 -38
  129. package/src/browser/skill-service.ts +61 -33
  130. package/src/browser/token-usage-frontend-service-impl.ts +6 -3
  131. package/src/browser/window-blink-service.ts +7 -4
  132. package/src/common/agent-preferences.ts +13 -2
  133. package/src/common/ai-core-preferences.ts +4 -2
  134. package/src/common/capability-variable-contribution.spec.ts +2 -1
  135. package/src/common/frontmatter.spec.ts +82 -0
  136. package/src/common/frontmatter.ts +99 -0
  137. package/src/common/index.ts +1 -0
  138. package/src/common/language-model-service.spec.ts +70 -0
  139. package/src/common/language-model-service.ts +1 -1
  140. package/src/common/language-model.ts +72 -3
  141. package/src/common/notification-types.ts +10 -0
  142. package/src/common/prompt-service-util.spec.ts +60 -0
  143. package/src/common/prompt-service-util.ts +31 -1
  144. package/src/common/prompt-service.spec.ts +59 -2
  145. package/src/common/prompt-service.ts +104 -8
  146. package/src/common/settings-service.ts +5 -0
  147. package/src/common/skill.ts +37 -39
  148. package/src/common/tool-invocation-registry.ts +13 -0
  149. package/src/common/variable-service.spec.ts +2 -2
  150. package/src/common/variable-service.ts +3 -1
  151. package/src/node/backend-language-model-registry.spec.ts +52 -0
  152. package/src/node/backend-language-model-registry.ts +1 -0
  153. package/src/node/token-usage-service-impl.ts +7 -2
  154. package/lib/browser/agent-completion-notification-service.d.ts.map +0 -1
  155. package/lib/browser/agent-completion-notification-service.js.map +0 -1
@@ -15,7 +15,7 @@
15
15
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
16
  // *****************************************************************************
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.DefaultPromptFragmentCustomizationService = exports.CustomizationSource = void 0;
18
+ exports.DefaultPromptFragmentCustomizationService = exports.CustomizationSource = exports.CUSTOM_AGENT_WORKSPACE_DIRECTORIES = exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM = exports.CUSTOM_AGENT_FILE_NAME = exports.CUSTOM_AGENTS_DIRECTORY = void 0;
19
19
  exports.getCustomizationSourceString = getCustomizationSourceString;
20
20
  const tslib_1 = require("tslib");
21
21
  const core_1 = require("@theia/core");
@@ -23,6 +23,7 @@ const browser_1 = require("@theia/core/lib/browser");
23
23
  const inversify_1 = require("@theia/core/shared/inversify");
24
24
  const common_1 = require("../common");
25
25
  const configurable_in_memory_resources_1 = require("../common/configurable-in-memory-resources");
26
+ const frontmatter_1 = require("../common/frontmatter");
26
27
  const buffer_1 = require("@theia/core/lib/common/buffer");
27
28
  const file_service_1 = require("@theia/filesystem/lib/browser/file-service");
28
29
  const ai_core_preferences_1 = require("../common/ai-core-preferences");
@@ -31,6 +32,51 @@ const js_yaml_1 = require("js-yaml");
31
32
  const prompttemplate_contribution_1 = require("./prompttemplate-contribution");
32
33
  const prompttemplate_parser_1 = require("./prompttemplate-parser");
33
34
  const browser_2 = require("@theia/workspace/lib/browser");
35
+ /**
36
+ * Subdirectory (relative to a prompt-templates scope) holding one folder per custom agent.
37
+ */
38
+ exports.CUSTOM_AGENTS_DIRECTORY = 'agents';
39
+ /**
40
+ * Filename of the per-agent definition file (frontmatter + prompt body) inside `agents/<id>/`.
41
+ */
42
+ exports.CUSTOM_AGENT_FILE_NAME = 'agent.md';
43
+ /**
44
+ * Filename stem (without extension) reserved for the customization of a custom agent's
45
+ * default prompt. Lives at `<scope>/agents/<id>/prompt.prompttemplate` and maps to
46
+ * fragment id `<agent-name>_prompt`. Excluded from the variant loader so it doesn't double
47
+ * as an extra variant.
48
+ */
49
+ exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM = 'prompt';
50
+ /**
51
+ * Workspace-relative parent folders scanned for custom agents, independent of the configurable
52
+ * prompt-templates directories. Scanning both folders mirrors the skills convention introduced
53
+ * in #17553, but the duplicate-id precedence is intentionally inverted: here `.agents` is listed
54
+ * first, so it becomes the default location for newly created agents and wins over `.prompts`,
55
+ * whereas for skills `.prompts` wins over `.agents` (see `combineSkillDirectories` in
56
+ * `skill-service.ts`). `.prompts` is retained for backward compatibility with agents authored
57
+ * before the move to `.agents`.
58
+ */
59
+ exports.CUSTOM_AGENT_WORKSPACE_DIRECTORIES = ['.agents', '.prompts'];
60
+ var CustomAgentFrontmatter;
61
+ (function (CustomAgentFrontmatter) {
62
+ function is(value) {
63
+ if (!value || typeof value !== 'object') {
64
+ return false;
65
+ }
66
+ const entry = value;
67
+ if (typeof entry.name !== 'string' || typeof entry.description !== 'string' || typeof entry.defaultLLM !== 'string') {
68
+ return false;
69
+ }
70
+ if ('showInChat' in entry && typeof entry.showInChat !== 'boolean') {
71
+ return false;
72
+ }
73
+ if ('id' in entry && typeof entry.id !== 'string') {
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ CustomAgentFrontmatter.is = is;
79
+ })(CustomAgentFrontmatter || (CustomAgentFrontmatter = {}));
34
80
  /**
35
81
  * Default template entry for creating custom agents
36
82
  */
@@ -78,18 +124,31 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
78
124
  this.allCustomizations = new Map();
79
125
  /** Stores additional directory paths for loading template files. */
80
126
  this.additionalTemplateDirs = new Set();
127
+ /**
128
+ * Built-in parent directories scanned for custom agents (`.agents`, `.prompts`), independent of
129
+ * {@link additionalTemplateDirs}. Insertion order is preserved so the first entry (`.agents`)
130
+ * acts as the default location for newly created agents.
131
+ */
132
+ this.customAgentDirs = new Set();
81
133
  /** Contains file extensions that identify prompt template files. */
82
134
  this.templateExtensions = new Set([prompttemplate_contribution_1.PROMPT_TEMPLATE_EXTENSION]);
83
135
  /** Stores specific file paths, provided by the settings, that should be treated as templates. */
84
136
  this.workspaceTemplateFiles = new Set();
85
137
  /** Maps URI strings to WatchedFileInfo objects for individually watched template files. */
86
138
  this.watchedFiles = new Map();
139
+ /**
140
+ * For each known custom-agent prompt fragment id (i.e. `<name>_prompt`), the URI of the
141
+ * agent's folder (`<scope>/agents/<id>/`). Populated as agents are loaded and used to
142
+ * route customization writes/reads into the agent folder.
143
+ */
144
+ this.customAgentFolderByFragmentId = new Map();
87
145
  /** Collection of disposable resources for cleanup when the service updates or is disposed. */
88
146
  this.toDispose = new core_1.DisposableCollection();
89
147
  this.onDidChangePromptFragmentCustomizationEmitter = new core_1.Emitter();
90
148
  this.onDidChangePromptFragmentCustomization = this.onDidChangePromptFragmentCustomizationEmitter.event;
91
149
  this.onDidChangeCustomAgentsEmitter = new core_1.Emitter();
92
150
  this.onDidChangeCustomAgents = this.onDidChangeCustomAgentsEmitter.event;
151
+ this.warnedLegacyYamlUris = new Set();
93
152
  }
94
153
  init() {
95
154
  this.preferences.onPreferenceChanged(event => {
@@ -113,11 +172,28 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
113
172
  // First process the main templates directory (lowest priority)
114
173
  const templatesURI = await this.getTemplatesDirectoryURI();
115
174
  await this.processTemplateDirectory(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, templatesURI, 1, CustomizationSource.CUSTOMIZED); // Priority 1 for customized fragments
116
- // Process additional template directories (medium priority)
117
- for (const dirPath of this.additionalTemplateDirs) {
118
- const dirURI = core_1.URI.fromFilePath(dirPath);
175
+ // Process additional template directories (medium priority). Skip any directory that
176
+ // resolves to the main templates directory — a user can set both prefs to the same
177
+ // path; re-processing the same dir overwrites the priority-1 entries with priority-2,
178
+ // which then hides them from `editBuiltIn`'s priority-1-only lookup.
179
+ const processedScopeKeys = new Set([templatesURI.toString()]);
180
+ for (const dirURI of this.getDedupedAdditionalScopes(templatesURI)) {
181
+ processedScopeKeys.add(dirURI.toString());
119
182
  await this.processTemplateDirectory(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, 2, CustomizationSource.FOLDER); // Priority 2 for folder fragments
120
183
  }
184
+ // Process built-in custom-agent directories (`.agents`/`.prompts`) not already covered as
185
+ // template directories above: register the prompt fragments inside their `agents/<id>/`
186
+ // folders and watch them, so agents under these scopes get the same live refresh and
187
+ // prompt-fragment editing as those under template directories — without loading loose
188
+ // prompt templates from these built-in scopes themselves.
189
+ for (const dirPath of this.customAgentDirs) {
190
+ const scopeURI = core_1.URI.fromFilePath(dirPath);
191
+ if (processedScopeKeys.has(scopeURI.toString())) {
192
+ continue;
193
+ }
194
+ processedScopeKeys.add(scopeURI.toString());
195
+ await this.processCustomAgentScope(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, scopeURI, 2, CustomizationSource.FOLDER);
196
+ }
121
197
  // Process specific template files (highest priority)
122
198
  await this.processTemplateFiles(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, watchedFilesCopy);
123
199
  this.activeCustomizations = activeCustomizationsCopy;
@@ -388,10 +464,115 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
388
464
  // Process existing files if directory exists
389
465
  if (dirExists) {
390
466
  await this.processExistingTemplateDirectory(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource);
467
+ await this.processCustomAgentFolders(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource);
391
468
  }
392
469
  // Set up file watching for the directory (works for both existing and non-existing directories)
393
470
  this.setupDirectoryWatcher(dirURI, priority, customizationSource);
394
471
  }
472
+ /**
473
+ * Processes a built-in custom-agent scope (`.agents`/`.prompts`): registers the prompt fragments
474
+ * inside its `agents/<id>/` folders and watches the scope for changes. Unlike
475
+ * {@link processTemplateDirectory} it does not scan loose `*.prompttemplate` files in the scope
476
+ * root, so `.agents` does not become a general prompt-template directory.
477
+ * @param dirURI URI of the agent scope directory
478
+ * @param priority Priority level for customizations in this scope
479
+ * @param customizationSource Source type of the customization
480
+ */
481
+ async processCustomAgentScope(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource) {
482
+ await this.processCustomAgentFolders(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource);
483
+ // Watch for changes (works for both existing and non-existing directories), restricted to
484
+ // agent folders so newly created/edited agents under `.agents` are picked up live.
485
+ this.setupDirectoryWatcher(dirURI, priority, customizationSource, true);
486
+ }
487
+ /**
488
+ * Scan `<dirURI>/agents/<id>/*.prompttemplate` files and register each as a customized
489
+ * prompt fragment so they appear in the Prompt Fragments configuration view and so
490
+ * `editPromptFragmentCustomization` / `removePromptFragmentCustomization` can find their
491
+ * source URIs.
492
+ *
493
+ * The reserved filename `prompt.prompttemplate` maps to the agent's default-variant
494
+ * fragment id (`<agent-name>_prompt` read from the sibling `agent.md`'s frontmatter).
495
+ * Any other `.prompttemplate` file uses its filename stem as the fragment id (matching
496
+ * how variants are registered via {@link readCustomAgentPromptVariants}).
497
+ */
498
+ async processCustomAgentFolders(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, scopeDir, priority, customizationSource) {
499
+ const agentsDirURI = scopeDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
500
+ let agentsStat;
501
+ try {
502
+ agentsStat = await this.fileService.resolve(agentsDirURI);
503
+ }
504
+ catch {
505
+ return;
506
+ }
507
+ if (!agentsStat.isDirectory || !agentsStat.children?.length) {
508
+ return;
509
+ }
510
+ for (const agentChild of agentsStat.children) {
511
+ if (!agentChild.isDirectory) {
512
+ continue;
513
+ }
514
+ const defaultFragmentId = await this.readCustomAgentDefaultFragmentId(agentChild.resource);
515
+ if (defaultFragmentId) {
516
+ this.customAgentFolderByFragmentId.set(defaultFragmentId, agentChild.resource);
517
+ }
518
+ const folderStat = await this.fileService.resolve(agentChild.resource).catch(() => undefined);
519
+ if (!folderStat?.children?.length) {
520
+ continue;
521
+ }
522
+ for (const file of folderStat.children) {
523
+ if (!file.isFile || !this.isPromptTemplateExtension(file.resource.path.ext)) {
524
+ continue;
525
+ }
526
+ const stem = this.removePromptTemplateSuffix(file.resource.path.name);
527
+ const fragmentId = stem === exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM ? defaultFragmentId : stem;
528
+ if (!fragmentId) {
529
+ continue;
530
+ }
531
+ trackedTemplateURIsCopy.add(file.resource.toString());
532
+ try {
533
+ const fileContent = await this.fileService.read(file.resource);
534
+ const parsed = this.parseTemplateWithMetadata(fileContent.value);
535
+ this.addTemplate(activeCustomizationsCopy, fragmentId, parsed.template, file.resource.toString(), allCustomizationsCopy, priority, customizationSource, parsed.metadata);
536
+ }
537
+ catch (e) {
538
+ this.logger.debug(`Failed to load custom-agent customization ${file.resource.toString()}: ${e?.message ?? e}`);
539
+ }
540
+ }
541
+ }
542
+ }
543
+ /**
544
+ * Return the parent URI when `resource` lives in `<scopeDirURI>/agents/<id>/`; otherwise
545
+ * undefined. Used to decide whether a newly-added file should be processed as a
546
+ * custom-agent prompt customization.
547
+ */
548
+ matchesCustomAgentFolder(resource, scopeDirURI) {
549
+ const parent = resource.parent;
550
+ const grandParent = parent.parent;
551
+ if (!grandParent) {
552
+ return undefined;
553
+ }
554
+ if (grandParent.path.base !== exports.CUSTOM_AGENTS_DIRECTORY) {
555
+ return undefined;
556
+ }
557
+ if (!grandParent.parent.isEqual(scopeDirURI)) {
558
+ return undefined;
559
+ }
560
+ return parent;
561
+ }
562
+ /**
563
+ * Read `<agentFolderURI>/agent.md`'s frontmatter and return the implied default
564
+ * fragment id (`<name>_prompt`). Returns undefined if the file is missing or invalid.
565
+ */
566
+ async readCustomAgentDefaultFragmentId(agentFolderURI) {
567
+ try {
568
+ const content = (await this.fileService.read(agentFolderURI.resolve(exports.CUSTOM_AGENT_FILE_NAME), { encoding: 'utf-8' })).value;
569
+ const { metadata } = (0, frontmatter_1.parseFrontmatter)(content, { isValid: CustomAgentFrontmatter.is });
570
+ return metadata ? `${metadata.name}_prompt` : undefined;
571
+ }
572
+ catch {
573
+ return undefined;
574
+ }
575
+ }
395
576
  /**
396
577
  * Processes an existing directory for template files
397
578
  * @param activeCustomizationsCopy Map to store active customizations
@@ -424,13 +605,27 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
424
605
  this.onDidChangePromptFragmentCustomizationEmitter.fire(Array.from(parsedPromptFragments));
425
606
  this.onDidChangeCustomAgentsEmitter.fire();
426
607
  }
608
+ /**
609
+ * Whether a changed resource path affects custom agents, i.e. it is (or is inside) an `agents/`
610
+ * directory, or a legacy `customAgents.yml`. Matches the `agents` directory itself (e.g. when the
611
+ * whole folder is deleted), not only files within it, so removing `agents/` triggers a reload.
612
+ * @param path The string form of the changed resource URI
613
+ */
614
+ isCustomAgentChange(path) {
615
+ return path.endsWith('customAgents.yml')
616
+ || path.includes(`/${exports.CUSTOM_AGENTS_DIRECTORY}/`)
617
+ || path.endsWith(`/${exports.CUSTOM_AGENTS_DIRECTORY}`);
618
+ }
427
619
  /**
428
620
  * Sets up file watching for a template directory (works for both existing and non-existing directories)
429
621
  * @param dirURI URI of the directory to watch
430
622
  * @param priority Priority level for customizations in this directory
431
623
  * @param customizationSource Source type of the customization
624
+ * @param agentsOnly When true, only `agents/<id>/` prompt files are registered on add; loose
625
+ * `*.prompttemplate` files in the scope root are ignored. Used for built-in agent scopes
626
+ * (`.agents`) that must not become general prompt-template directories.
432
627
  */
433
- setupDirectoryWatcher(dirURI, priority, customizationSource) {
628
+ setupDirectoryWatcher(dirURI, priority, customizationSource, agentsOnly = false) {
434
629
  this.toDispose.push(this.fileService.watch(dirURI, { recursive: true, excludes: [] }));
435
630
  this.toDispose.push(this.fileService.onDidFilesChange(async (event) => {
436
631
  // Filter for changes within the watched directory
@@ -444,7 +639,7 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
444
639
  await this.update();
445
640
  return;
446
641
  }
447
- if (event.changes.some(change => change.resource.toString().endsWith('customAgents.yml'))) {
642
+ if (event.changes.some(change => this.isCustomAgentChange(change.resource.toString()))) {
448
643
  this.onDidChangeCustomAgentsEmitter.fire();
449
644
  }
450
645
  // Track changes for batched notification
@@ -465,23 +660,44 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
465
660
  if (this.trackedTemplateURIs.has(uriString)) {
466
661
  const fileContent = await this.fileService.read(updatedFile.resource);
467
662
  const parsed = this.parseTemplateWithMetadata(fileContent.value);
468
- const fragmentId = this.removePromptTemplateSuffix(updatedFile.resource.path.name);
663
+ // Prefer the already-tracked fragment id over the filename stem so that
664
+ // `<scope>/agents/<id>/prompt.prompttemplate` keeps mapping to `<name>_prompt`
665
+ // instead of becoming a fragment called `prompt`.
666
+ const fragmentId = this.allCustomizations.get(uriString)?.id
667
+ ?? this.removePromptTemplateSuffix(updatedFile.resource.path.name);
469
668
  this.addTemplate(this.activeCustomizations, fragmentId, parsed.template, uriString, this.allCustomizations, priority, customizationSource, parsed.metadata);
470
669
  changedFragmentIds.add(fragmentId);
471
670
  }
472
671
  }
473
672
  // Handle new templates
474
673
  for (const addedFile of event.getAdded()) {
475
- if (addedFile.resource.parent.toString() === dirURI.toString() &&
476
- this.isPromptTemplateExtension(addedFile.resource.path.ext)) {
477
- const uriString = addedFile.resource.toString();
478
- this.trackedTemplateURIs.add(uriString);
479
- const fileContent = await this.fileService.read(addedFile.resource);
480
- const parsed = this.parseTemplateWithMetadata(fileContent.value);
481
- const fragmentId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
482
- this.addTemplate(this.activeCustomizations, fragmentId, parsed.template, uriString, this.allCustomizations, priority, customizationSource, parsed.metadata);
483
- changedFragmentIds.add(fragmentId);
674
+ if (!this.isPromptTemplateExtension(addedFile.resource.path.ext)) {
675
+ continue;
676
+ }
677
+ const isScopeRoot = !agentsOnly && addedFile.resource.parent.toString() === dirURI.toString();
678
+ const agentFolderURI = this.matchesCustomAgentFolder(addedFile.resource, dirURI);
679
+ if (!isScopeRoot && !agentFolderURI) {
680
+ continue;
681
+ }
682
+ const uriString = addedFile.resource.toString();
683
+ this.trackedTemplateURIs.add(uriString);
684
+ const fileContent = await this.fileService.read(addedFile.resource);
685
+ const parsed = this.parseTemplateWithMetadata(fileContent.value);
686
+ let fragmentId;
687
+ if (agentFolderURI) {
688
+ const stem = this.removePromptTemplateSuffix(addedFile.resource.path.name);
689
+ fragmentId = stem === exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM
690
+ ? await this.readCustomAgentDefaultFragmentId(agentFolderURI)
691
+ : stem;
692
+ }
693
+ else {
694
+ fragmentId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
695
+ }
696
+ if (!fragmentId) {
697
+ continue;
484
698
  }
699
+ this.addTemplate(this.activeCustomizations, fragmentId, parsed.template, uriString, this.allCustomizations, priority, customizationSource, parsed.metadata);
700
+ changedFragmentIds.add(fragmentId);
485
701
  }
486
702
  const changedFragmentIdsArray = Array.from(changedFragmentIds);
487
703
  if (changedFragmentIdsArray.length > 0) {
@@ -530,6 +746,12 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
530
746
  this.additionalTemplateDirs.add(path);
531
747
  }
532
748
  }
749
+ if (properties.agentDirectoryPaths !== undefined) {
750
+ this.customAgentDirs.clear();
751
+ for (const path of properties.agentDirectoryPaths) {
752
+ this.customAgentDirs.add(path);
753
+ }
754
+ }
533
755
  if (properties.extensions !== undefined) {
534
756
  this.templateExtensions.clear();
535
757
  for (const ext of properties.extensions) {
@@ -559,12 +781,62 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
559
781
  const theiaConfigDir = await this.envVariablesServer.getConfigDirUri();
560
782
  return new core_1.URI(theiaConfigDir).resolve('prompt-templates');
561
783
  }
784
+ /**
785
+ * The additional (workspace) template directories as URIs, excluding any that resolves to the
786
+ * same location as `excluding` (typically the global templates directory). A user can point
787
+ * both the `promptTemplates` preference and a workspace template directory at the same path;
788
+ * processing that scope twice would hide priority-1 customizations behind priority-2 copies
789
+ * (see {@link update}) or list the same scope twice in the agent-location picker.
790
+ */
791
+ getDedupedAdditionalScopes(excluding) {
792
+ const result = [];
793
+ for (const dirPath of this.additionalTemplateDirs) {
794
+ const dirURI = core_1.URI.fromFilePath(dirPath);
795
+ if (!dirURI.isEqual(excluding)) {
796
+ result.push(dirURI);
797
+ }
798
+ }
799
+ return result;
800
+ }
801
+ /**
802
+ * The deduplicated parent directories scanned for custom agents, in precedence order:
803
+ * the built-in {@link customAgentDirs} (`.agents` then `.prompts`) first, so `.agents` is both
804
+ * the discovery winner and the default creation target; then any configured
805
+ * {@link additionalTemplateDirs} that may also hold agents; then the global templates directory.
806
+ * Independent of the prompt-templates preference, mirroring how skills resolve their folders.
807
+ */
808
+ async getCustomAgentScopes() {
809
+ const seen = new Set();
810
+ const scopes = [];
811
+ const add = (uri) => {
812
+ const key = uri.toString();
813
+ if (!seen.has(key)) {
814
+ seen.add(key);
815
+ scopes.push(uri);
816
+ }
817
+ };
818
+ for (const dirPath of this.customAgentDirs) {
819
+ add(core_1.URI.fromFilePath(dirPath));
820
+ }
821
+ for (const dirPath of this.additionalTemplateDirs) {
822
+ add(core_1.URI.fromFilePath(dirPath));
823
+ }
824
+ add(await this.getTemplatesDirectoryURI());
825
+ return scopes;
826
+ }
562
827
  /**
563
828
  * Gets the URI for a specific template file
564
829
  * @param fragmentId The fragment ID
565
830
  * @returns URI for the template file
566
831
  */
567
832
  async getTemplateURI(fragmentId) {
833
+ // Custom-agent default prompts live inside their own folder under the reserved filename
834
+ // `prompt.prompttemplate`. Variants and other fragments fall back to scope-root behavior
835
+ // (where filename = fragment id), matching how Theia tracks all other customizations.
836
+ const agentFolder = this.customAgentFolderByFragmentId.get(fragmentId);
837
+ if (agentFolder) {
838
+ return agentFolder.resolve(`${exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM}${prompttemplate_contribution_1.PROMPT_TEMPLATE_EXTENSION}`);
839
+ }
568
840
  return (await this.getTemplatesDirectoryURI()).resolve(`${fragmentId}${prompttemplate_contribution_1.PROMPT_TEMPLATE_EXTENSION}`);
569
841
  }
570
842
  /**
@@ -825,21 +1097,197 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
825
1097
  }
826
1098
  async getCustomAgents() {
827
1099
  const agentsById = new Map();
828
- // First, process additional (workspace) template directories to give them precedence
829
- for (const dirPath of this.additionalTemplateDirs) {
830
- const dirURI = core_1.URI.fromFilePath(dirPath);
831
- await this.loadCustomAgentsFromDirectory(dirURI, agentsById);
1100
+ // Process scopes in precedence order (`.agents`/`.prompts` first, global last). The map only
1101
+ // keeps the first agent seen per id, so earlier scopes win on conflicts.
1102
+ for (const scope of await this.getCustomAgentScopes()) {
1103
+ await this.loadCustomAgentsFromAgentsDirectory(scope, agentsById);
1104
+ await this.loadCustomAgentsFromDirectory(scope, agentsById);
832
1105
  }
833
- // Then process global templates directory (only adding agents that don't conflict)
834
- const globalTemplatesDir = await this.getTemplatesDirectoryURI();
835
- await this.loadCustomAgentsFromDirectory(globalTemplatesDir, agentsById);
836
- // Return the merged list of agents
1106
+ // Note: customAgentFolderByFragmentId is intentionally not cleared here. It grows
1107
+ // monotonically stale entries (renamed/removed agents) at worst point at a folder
1108
+ // that no longer exists, which the file-create path mkdirps if needed. Clearing it
1109
+ // up front would create a race where a concurrent `getTemplateURI` call sees an empty
1110
+ // map and falls back to the scope root.
837
1111
  return Array.from(agentsById.values());
838
1112
  }
839
1113
  /**
840
- * Load custom agents from a specific directory
841
- * @param directoryURI The URI of the directory to load from
842
- * @param agentsById Map to store the loaded agents by ID
1114
+ * Load custom agents from `<parentDirectory>/agents/<id>/agent.md`. Each immediate
1115
+ * subdirectory under `agents/` defines one custom agent: the folder name is the
1116
+ * agent id (single source of truth), the file's YAML frontmatter holds the metadata,
1117
+ * and the body is the prompt text. Folders without a readable `agent.md` are skipped.
1118
+ */
1119
+ async loadCustomAgentsFromAgentsDirectory(parentDirectory, agentsById) {
1120
+ const agentsDirURI = parentDirectory.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
1121
+ let directoryStat;
1122
+ try {
1123
+ directoryStat = await this.fileService.resolve(agentsDirURI);
1124
+ }
1125
+ catch {
1126
+ return;
1127
+ }
1128
+ if (!directoryStat.isDirectory || !directoryStat.children?.length) {
1129
+ return;
1130
+ }
1131
+ for (const child of directoryStat.children) {
1132
+ if (!child.isDirectory) {
1133
+ continue;
1134
+ }
1135
+ const agent = await this.readCustomAgentFile(child.resource);
1136
+ if (!agent) {
1137
+ continue;
1138
+ }
1139
+ if (!agentsById.has(agent.id)) {
1140
+ agentsById.set(agent.id, agent);
1141
+ }
1142
+ }
1143
+ }
1144
+ async readCustomAgentFile(agentFolderURI) {
1145
+ const id = agentFolderURI.path.base;
1146
+ const fileURI = agentFolderURI.resolve(exports.CUSTOM_AGENT_FILE_NAME);
1147
+ let content;
1148
+ try {
1149
+ content = (await this.fileService.read(fileURI, { encoding: 'utf-8' })).value;
1150
+ }
1151
+ catch {
1152
+ return undefined;
1153
+ }
1154
+ const { metadata, body } = (0, frontmatter_1.parseFrontmatter)(content, { isValid: CustomAgentFrontmatter.is });
1155
+ if (!metadata) {
1156
+ this.logger.debug(`Invalid or missing frontmatter in ${fileURI.toString()}`);
1157
+ return undefined;
1158
+ }
1159
+ if (metadata.id !== undefined && metadata.id !== id) {
1160
+ this.logger.debug(`Frontmatter id '${metadata.id}' in ${fileURI.toString()} does not match folder name '${id}'. Skipping.`);
1161
+ return undefined;
1162
+ }
1163
+ const promptVariants = await this.readCustomAgentPromptVariants(agentFolderURI);
1164
+ const fragmentId = `${metadata.name}_prompt`;
1165
+ this.customAgentFolderByFragmentId.set(fragmentId, agentFolderURI);
1166
+ return {
1167
+ id,
1168
+ name: metadata.name,
1169
+ description: metadata.description,
1170
+ prompt: body,
1171
+ defaultLLM: metadata.defaultLLM,
1172
+ showInChat: metadata.showInChat,
1173
+ ...(promptVariants.length > 0 ? { promptVariants } : {})
1174
+ };
1175
+ }
1176
+ /**
1177
+ * Remove `<scopeDir>/agents/<id>/` directories that contain no `agent.md`. They are usually
1178
+ * the leftover side effect of a previous migration whose `agent.md` write failed after the
1179
+ * parent directory had already been created. Removing them lets the next run re-attempt.
1180
+ */
1181
+ async cleanupEmptyAgentFolders(scopeDir) {
1182
+ const agentsDirURI = scopeDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
1183
+ let stat;
1184
+ try {
1185
+ stat = await this.fileService.resolve(agentsDirURI);
1186
+ }
1187
+ catch {
1188
+ return;
1189
+ }
1190
+ if (!stat.children?.length) {
1191
+ return;
1192
+ }
1193
+ for (const child of stat.children) {
1194
+ if (!child.isDirectory) {
1195
+ continue;
1196
+ }
1197
+ const agentMdURI = child.resource.resolve(exports.CUSTOM_AGENT_FILE_NAME);
1198
+ if (await this.fileService.exists(agentMdURI)) {
1199
+ continue;
1200
+ }
1201
+ const childStat = await this.fileService.resolve(child.resource).catch(() => undefined);
1202
+ if (childStat?.children?.length) {
1203
+ continue;
1204
+ }
1205
+ try {
1206
+ await this.fileService.delete(child.resource, { recursive: true });
1207
+ this.logger.info(`Removed empty agent folder ${child.resource.toString()} left behind by a previous run.`);
1208
+ }
1209
+ catch (e) {
1210
+ this.logger.warn(`Failed to remove empty agent folder ${child.resource.toString()}: ${e?.message ?? e}`);
1211
+ }
1212
+ }
1213
+ }
1214
+ /**
1215
+ * List `.prompttemplate` files directly inside the given scope directory (one level only).
1216
+ * Returns each file's URI along with its filename stem to enable cheap prefix matching
1217
+ * during migration.
1218
+ */
1219
+ async listScopeRootPromptTemplates(scopeDir) {
1220
+ let stat;
1221
+ try {
1222
+ stat = await this.fileService.resolve(scopeDir);
1223
+ }
1224
+ catch {
1225
+ return [];
1226
+ }
1227
+ if (!stat.children?.length) {
1228
+ return [];
1229
+ }
1230
+ const result = [];
1231
+ for (const child of stat.children) {
1232
+ if (!child.isFile) {
1233
+ continue;
1234
+ }
1235
+ if (!this.isPromptTemplateExtension(child.resource.path.ext)) {
1236
+ continue;
1237
+ }
1238
+ result.push({ uri: child.resource, stem: this.removePromptTemplateSuffix(child.resource.path.name) });
1239
+ }
1240
+ return result;
1241
+ }
1242
+ /**
1243
+ * Scan an agent folder for sibling `.prompttemplate` files; each becomes a variant
1244
+ * of the agent's prompt. Variant id = filename stem (e.g. `concise.prompttemplate`
1245
+ * yields a variant with id `concise`). Files inside subdirectories are ignored.
1246
+ *
1247
+ * The reserved filename `prompt.prompttemplate` is excluded: it represents the
1248
+ * default-variant customization (overrides `agent.md`'s body), not an extra variant,
1249
+ * and is registered by the prompt-fragment customization scan instead.
1250
+ */
1251
+ async readCustomAgentPromptVariants(agentFolderURI) {
1252
+ let folderStat;
1253
+ try {
1254
+ folderStat = await this.fileService.resolve(agentFolderURI);
1255
+ }
1256
+ catch {
1257
+ return [];
1258
+ }
1259
+ if (!folderStat.children?.length) {
1260
+ return [];
1261
+ }
1262
+ const variants = [];
1263
+ for (const child of folderStat.children) {
1264
+ if (!child.isFile) {
1265
+ continue;
1266
+ }
1267
+ if (!this.isPromptTemplateExtension(child.resource.path.ext)) {
1268
+ continue;
1269
+ }
1270
+ const variantId = this.removePromptTemplateSuffix(child.resource.path.name);
1271
+ if (variantId === exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM) {
1272
+ continue;
1273
+ }
1274
+ try {
1275
+ const variantContent = (await this.fileService.read(child.resource, { encoding: 'utf-8' })).value;
1276
+ const parsed = this.parseTemplateWithMetadata(variantContent);
1277
+ variants.push({ id: variantId, template: parsed.template });
1278
+ }
1279
+ catch (e) {
1280
+ this.logger.debug(`Failed to read prompt variant ${child.resource.toString()}: ${e?.message ?? e}`);
1281
+ }
1282
+ }
1283
+ // Stable order to keep equality checks deterministic between loads.
1284
+ variants.sort((a, b) => a.id.localeCompare(b.id));
1285
+ return variants;
1286
+ }
1287
+ /**
1288
+ * @deprecated Reads legacy `customAgents.yml` files. New agents should live under
1289
+ * `<scope>/agents/<id>/agent.md`. Kept as a fallback until existing files have been
1290
+ * auto-migrated; loader logs a one-time warning per scope when it finds one.
843
1291
  */
844
1292
  async loadCustomAgentsFromDirectory(directoryURI, agentsById) {
845
1293
  const customAgentYamlUri = directoryURI.resolve('customAgents.yml');
@@ -847,11 +1295,12 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
847
1295
  if (!yamlExists) {
848
1296
  return;
849
1297
  }
1298
+ this.warnOnceLegacyYaml(customAgentYamlUri);
850
1299
  try {
851
1300
  const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
852
1301
  const doc = (0, js_yaml_1.load)(fileContent.value);
853
1302
  if (!Array.isArray(doc) || !doc.every(entry => common_1.CustomAgentDescription.is(entry))) {
854
- console.debug(`Invalid customAgents.yml file content in ${directoryURI.toString()}`);
1303
+ this.logger.debug(`Invalid customAgents.yml file content in ${directoryURI.toString()}`);
855
1304
  return;
856
1305
  }
857
1306
  const readAgents = doc;
@@ -863,35 +1312,195 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
863
1312
  }
864
1313
  }
865
1314
  catch (e) {
866
- console.debug(`Error loading customAgents.yml from ${directoryURI.toString()}: ${e.message}`, e);
1315
+ this.logger.debug(`Error loading customAgents.yml from ${directoryURI.toString()}: ${e.message}`, e);
867
1316
  }
868
1317
  }
1318
+ warnOnceLegacyYaml(uri) {
1319
+ const key = uri.toString();
1320
+ if (this.warnedLegacyYamlUris.has(key)) {
1321
+ return;
1322
+ }
1323
+ this.warnedLegacyYamlUris.add(key);
1324
+ this.logger.warn(`Loading custom agents from legacy '${key}'. ` +
1325
+ `Files in this format are auto-migrated to the '${exports.CUSTOM_AGENTS_DIRECTORY}/' folder on startup. ` +
1326
+ 'If migration did not run, invoke the command \'AI: Re-run custom-agent migration\'.');
1327
+ }
869
1328
  /**
870
- * Returns all locations of existing customAgents.yml files and potential locations where
871
- * new customAgents.yml files could be created.
872
- *
873
- * @returns An array of objects containing the URI and whether the file exists
1329
+ * Returns all locations of existing customAgents.yml files and `agents/` directories,
1330
+ * plus the canonical locations where new agents would be created (one per scope). Scopes are
1331
+ * returned in precedence order, so the first `agents-dir` entry is the default creation target.
874
1332
  */
875
1333
  async getCustomAgentsLocations() {
876
1334
  const locations = [];
877
- // Check global templates directory
878
- const globalTemplatesDir = await this.getTemplatesDirectoryURI();
879
- const globalAgentsUri = globalTemplatesDir.resolve('customAgents.yml');
880
- const globalExists = await this.fileService.exists(globalAgentsUri);
881
- locations.push({ uri: globalAgentsUri, exists: globalExists });
882
- // Check additional (workspace) template directories
883
- for (const dirPath of this.additionalTemplateDirs) {
884
- const dirURI = core_1.URI.fromFilePath(dirPath);
885
- const agentsUri = dirURI.resolve('customAgents.yml');
886
- const exists = await this.fileService.exists(agentsUri);
887
- locations.push({ uri: agentsUri, exists: exists });
1335
+ for (const parentDir of await this.getCustomAgentScopes()) {
1336
+ const agentsDirURI = parentDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
1337
+ locations.push({ uri: agentsDirURI, exists: await this.fileService.exists(agentsDirURI), kind: 'agents-dir' });
1338
+ const yamlURI = parentDir.resolve('customAgents.yml');
1339
+ locations.push({ uri: yamlURI, exists: await this.fileService.exists(yamlURI), kind: 'legacy-yaml' });
888
1340
  }
889
1341
  return locations;
890
1342
  }
891
1343
  /**
892
- * Opens an existing customAgents.yml file at the given URI, or creates a new one if it doesn't exist.
1344
+ * Creates `<parentDirectory>/agents/<id>/agent.md` from a `CustomAgentDescription` and opens it.
1345
+ * The agent id determines the folder name; the prompt body is written verbatim under the YAML frontmatter.
1346
+ */
1347
+ async createCustomAgentFile(parentDirectory, agent) {
1348
+ const fileURI = parentDirectory.resolve(exports.CUSTOM_AGENTS_DIRECTORY).resolve(agent.id).resolve(exports.CUSTOM_AGENT_FILE_NAME);
1349
+ const content = serializeCustomAgentFile(agent);
1350
+ if (!(await this.fileService.exists(fileURI))) {
1351
+ await this.fileService.createFile(fileURI, buffer_1.BinaryBuffer.fromString(content));
1352
+ }
1353
+ else {
1354
+ await this.fileService.writeFile(fileURI, buffer_1.BinaryBuffer.fromString(content));
1355
+ }
1356
+ const openHandler = await this.openerService.getOpener(fileURI);
1357
+ openHandler.open(fileURI);
1358
+ return fileURI;
1359
+ }
1360
+ /**
1361
+ * Auto-migrate every legacy `customAgents.yml` reachable from the configured scopes to the new
1362
+ * `agents/<id>/agent.md` layout. The original content is never deleted:
1363
+ * - on full success the YAML is renamed to `customAgents.yml.bak`, replacing any previous backup;
1364
+ * - on partial failure the YAML is renamed to `customAgents.yml.bak` only if no backup exists yet;
1365
+ * if one already exists the YAML is left in place, so the loader keeps serving it and the next
1366
+ * startup retries the migration.
893
1367
  *
894
- * @param uri The URI of the customAgents.yml file to open or create
1368
+ * Idempotent: rerunning never overwrites an already-migrated agent file.
1369
+ */
1370
+ async migrateCustomAgentsYaml() {
1371
+ // Coalesce concurrent calls. refreshCustomAgents() can re-fire during startup when
1372
+ // additionalTemplateDirs is populated asynchronously; without this guard each call
1373
+ // launches a parallel migration that races on the same `customAgents.yml` and on
1374
+ // sibling `*_prompt*.prompttemplate` files.
1375
+ if (this.migrationInFlight) {
1376
+ return this.migrationInFlight;
1377
+ }
1378
+ this.migrationInFlight = this.doMigrateCustomAgentsYaml().finally(() => {
1379
+ this.migrationInFlight = undefined;
1380
+ });
1381
+ return this.migrationInFlight;
1382
+ }
1383
+ async doMigrateCustomAgentsYaml() {
1384
+ const scopes = await this.getCustomAgentScopes();
1385
+ const reports = [];
1386
+ for (const scope of scopes) {
1387
+ const report = await this.migrateSingleScope(scope);
1388
+ if (report) {
1389
+ reports.push(report);
1390
+ }
1391
+ }
1392
+ if (reports.some(r => r.migrated > 0)) {
1393
+ this.onDidChangeCustomAgentsEmitter.fire();
1394
+ }
1395
+ return reports;
1396
+ }
1397
+ async migrateSingleScope(scopeDir) {
1398
+ const yamlURI = scopeDir.resolve('customAgents.yml');
1399
+ if (!(await this.fileService.exists(yamlURI))) {
1400
+ return undefined;
1401
+ }
1402
+ this.logger.info(`Migrating custom agents from ${yamlURI.toString()}`);
1403
+ let entries;
1404
+ try {
1405
+ const fileContent = await this.fileService.read(yamlURI, { encoding: 'utf-8' });
1406
+ const doc = (0, js_yaml_1.load)(fileContent.value);
1407
+ if (!Array.isArray(doc) || !doc.every(common_1.CustomAgentDescription.is)) {
1408
+ this.logger.warn(`Skipping migration of ${yamlURI.toString()}: file content is not a valid CustomAgentDescription[]`);
1409
+ return { scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0, yamlBackedUp: false, promptOverridesMigrated: 0 };
1410
+ }
1411
+ entries = doc;
1412
+ }
1413
+ catch (e) {
1414
+ this.logger.warn(`Skipping migration of ${yamlURI.toString()}: ${e.message}`);
1415
+ return { scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0, yamlBackedUp: false, promptOverridesMigrated: 0 };
1416
+ }
1417
+ let migrated = 0;
1418
+ let alreadyPresent = 0;
1419
+ let failed = 0;
1420
+ let promptOverridesMigrated = 0;
1421
+ // A previous failed migration may have created `agents/<id>/` directories without an
1422
+ // `agent.md` inside. Remove those empty placeholders so this run can recreate them cleanly.
1423
+ await this.cleanupEmptyAgentFolders(scopeDir);
1424
+ // Snapshot scope-root template files once so we can match per agent without re-listing.
1425
+ const scopeRootTemplateFiles = await this.listScopeRootPromptTemplates(scopeDir);
1426
+ // Prompt-fragment files are matched by agent *name* (the fragment id is `<name>_prompt`)
1427
+ // while folders are keyed by the unique *id*. When two entries share a name, only the first
1428
+ // can own the name-based files, so track seen names and skip the move for the duplicates.
1429
+ const seenNames = new Set();
1430
+ for (const entry of entries) {
1431
+ const agentFolderURI = scopeDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY).resolve(entry.id);
1432
+ const targetURI = agentFolderURI.resolve(exports.CUSTOM_AGENT_FILE_NAME);
1433
+ if (await this.fileService.exists(targetURI)) {
1434
+ alreadyPresent++;
1435
+ }
1436
+ else {
1437
+ try {
1438
+ await this.fileService.createFile(targetURI, buffer_1.BinaryBuffer.fromString(serializeCustomAgentFile(entry)), { overwrite: true });
1439
+ migrated++;
1440
+ }
1441
+ catch (e) {
1442
+ this.logger.warn(`Failed to migrate agent '${entry.id}' from ${yamlURI.toString()}: ${e?.message ?? e}`, e);
1443
+ failed++;
1444
+ continue;
1445
+ }
1446
+ }
1447
+ if (seenNames.has(entry.name)) {
1448
+ this.logger.warn(`Multiple custom agents in ${yamlURI.toString()} share the name '${entry.name}'; ` +
1449
+ 'prompt fragment files were migrated to the first matching agent only.');
1450
+ continue;
1451
+ }
1452
+ seenNames.add(entry.name);
1453
+ // Move any scope-root .prompttemplate files that look like they belong to this agent
1454
+ // (filename stem starts with `<name>_prompt`, followed by EOF or a separator).
1455
+ // They become variants under `<scope>/agents/<id>/`.
1456
+ for (const file of scopeRootTemplateFiles) {
1457
+ if (!matchesAgentPromptPrefix(file.stem, entry.name)) {
1458
+ continue;
1459
+ }
1460
+ const destURI = agentFolderURI.resolve(file.uri.path.base);
1461
+ if (await this.fileService.exists(destURI)) {
1462
+ continue;
1463
+ }
1464
+ try {
1465
+ await this.fileService.move(file.uri, destURI);
1466
+ promptOverridesMigrated++;
1467
+ }
1468
+ catch (e) {
1469
+ this.logger.warn(`Failed to move prompt fragment ${file.uri.toString()} into ${destURI.toString()}: ${e?.message ?? e}`);
1470
+ }
1471
+ }
1472
+ }
1473
+ let yamlBackedUp = false;
1474
+ const backupURI = scopeDir.resolve('customAgents.yml.bak');
1475
+ if (failed === 0) {
1476
+ try {
1477
+ await this.fileService.move(yamlURI, backupURI, { overwrite: true });
1478
+ yamlBackedUp = true;
1479
+ }
1480
+ catch (e) {
1481
+ this.logger.warn(`Migrated ${migrated} agents but failed to back up ${yamlURI.toString()}: ${e.message}`);
1482
+ }
1483
+ }
1484
+ else {
1485
+ try {
1486
+ if (!(await this.fileService.exists(backupURI))) {
1487
+ await this.fileService.move(yamlURI, backupURI);
1488
+ yamlBackedUp = true;
1489
+ }
1490
+ }
1491
+ catch (e) {
1492
+ this.logger.warn(`Failed to back up ${yamlURI.toString()} to ${backupURI.toString()}: ${e.message}`);
1493
+ }
1494
+ }
1495
+ this.logger.info(`Migration done for ${yamlURI.toString()}: ` +
1496
+ `migrated=${migrated}, alreadyPresent=${alreadyPresent}, failed=${failed}, ` +
1497
+ `yamlBackedUp=${yamlBackedUp}, promptOverridesMigrated=${promptOverridesMigrated}`);
1498
+ return { scope: scopeDir, yamlURI, migrated, alreadyPresent, failed, yamlBackedUp, promptOverridesMigrated };
1499
+ }
1500
+ /**
1501
+ * @deprecated Use {@link createCustomAgentFile} to author agents in the new
1502
+ * `<scope>/agents/<id>/agent.md` layout. Retained so existing UI affordances keep
1503
+ * working until they are migrated.
895
1504
  */
896
1505
  async openCustomAgentYaml(uri) {
897
1506
  const content = (0, js_yaml_1.dump)([newCustomAgentEntry]);
@@ -931,6 +1540,11 @@ tslib_1.__decorate([
931
1540
  (0, inversify_1.inject)(browser_2.WorkspaceService),
932
1541
  tslib_1.__metadata("design:type", browser_2.WorkspaceService)
933
1542
  ], DefaultPromptFragmentCustomizationService.prototype, "workspaceService", void 0);
1543
+ tslib_1.__decorate([
1544
+ (0, inversify_1.inject)(core_1.ILogger),
1545
+ (0, inversify_1.named)('ai-core:DefaultPromptFragmentCustomizationService'),
1546
+ tslib_1.__metadata("design:type", Object)
1547
+ ], DefaultPromptFragmentCustomizationService.prototype, "logger", void 0);
934
1548
  tslib_1.__decorate([
935
1549
  (0, inversify_1.postConstruct)(),
936
1550
  tslib_1.__metadata("design:type", Function),
@@ -940,4 +1554,30 @@ tslib_1.__decorate([
940
1554
  exports.DefaultPromptFragmentCustomizationService = DefaultPromptFragmentCustomizationService = tslib_1.__decorate([
941
1555
  (0, inversify_1.injectable)()
942
1556
  ], DefaultPromptFragmentCustomizationService);
1557
+ /**
1558
+ * Returns true when a filename stem looks like a prompt fragment owned by the named agent —
1559
+ * i.e. it begins with `<name>_prompt` followed by end-of-string or a separator
1560
+ * (`-`, `_`, ` `, `.`). Used to scoop variant files like `Foo_prompt_old.prompttemplate` into
1561
+ * the agent's folder during migration without grabbing unrelated fragments such as
1562
+ * `FooBar_prompt.prompttemplate` for a different agent.
1563
+ */
1564
+ function matchesAgentPromptPrefix(stem, agentName) {
1565
+ const prefix = `${agentName}_prompt`;
1566
+ if (!stem.startsWith(prefix)) {
1567
+ return false;
1568
+ }
1569
+ const next = stem.charAt(prefix.length);
1570
+ return next === '' || next === '-' || next === '_' || next === ' ' || next === '.';
1571
+ }
1572
+ function serializeCustomAgentFile(agent) {
1573
+ const metadata = {
1574
+ name: agent.name,
1575
+ description: agent.description,
1576
+ defaultLLM: agent.defaultLLM,
1577
+ };
1578
+ if (agent.showInChat !== undefined) {
1579
+ metadata.showInChat = agent.showInChat;
1580
+ }
1581
+ return (0, frontmatter_1.serializeFrontmatter)(metadata, agent.prompt);
1582
+ }
943
1583
  //# sourceMappingURL=frontend-prompt-customization-service.js.map