@theia/ai-ide 1.62.0-next.3 → 1.62.1

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 (92) hide show
  1. package/lib/browser/ai-configuration/agent-configuration-widget.d.ts +3 -3
  2. package/lib/browser/ai-configuration/agent-configuration-widget.d.ts.map +1 -1
  3. package/lib/browser/ai-configuration/agent-configuration-widget.js +24 -18
  4. package/lib/browser/ai-configuration/agent-configuration-widget.js.map +1 -1
  5. package/lib/browser/ai-configuration/ai-configuration-view-contribution.js +1 -1
  6. package/lib/browser/ai-configuration/ai-configuration-view-contribution.js.map +1 -1
  7. package/lib/browser/ai-configuration/ai-configuration-widget.d.ts +2 -0
  8. package/lib/browser/ai-configuration/ai-configuration-widget.d.ts.map +1 -1
  9. package/lib/browser/ai-configuration/ai-configuration-widget.js +6 -0
  10. package/lib/browser/ai-configuration/ai-configuration-widget.js.map +1 -1
  11. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.d.ts +138 -0
  12. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.d.ts.map +1 -0
  13. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js +492 -0
  14. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js.map +1 -0
  15. package/lib/browser/ai-configuration/template-settings-renderer.d.ts +4 -5
  16. package/lib/browser/ai-configuration/template-settings-renderer.d.ts.map +1 -1
  17. package/lib/browser/ai-configuration/template-settings-renderer.js +15 -31
  18. package/lib/browser/ai-configuration/template-settings-renderer.js.map +1 -1
  19. package/lib/browser/architect-agent.d.ts +1 -1
  20. package/lib/browser/architect-agent.d.ts.map +1 -1
  21. package/lib/browser/architect-agent.js +2 -2
  22. package/lib/browser/architect-agent.js.map +1 -1
  23. package/lib/browser/coder-agent.d.ts +2 -2
  24. package/lib/browser/coder-agent.d.ts.map +1 -1
  25. package/lib/browser/coder-agent.js +7 -3
  26. package/lib/browser/coder-agent.js.map +1 -1
  27. package/lib/browser/file-changeset-functions.d.ts +14 -1
  28. package/lib/browser/file-changeset-functions.d.ts.map +1 -1
  29. package/lib/browser/file-changeset-functions.js +153 -14
  30. package/lib/browser/file-changeset-functions.js.map +1 -1
  31. package/lib/browser/frontend-module.d.ts.map +1 -1
  32. package/lib/browser/frontend-module.js +11 -1
  33. package/lib/browser/frontend-module.js.map +1 -1
  34. package/lib/browser/template-preference-contribution.d.ts +2 -2
  35. package/lib/browser/template-preference-contribution.d.ts.map +1 -1
  36. package/lib/browser/template-preference-contribution.js +2 -2
  37. package/lib/browser/template-preference-contribution.js.map +1 -1
  38. package/lib/browser/workspace-functions.d.ts +1 -1
  39. package/lib/browser/workspace-functions.js +13 -13
  40. package/lib/common/architect-prompt-template.d.ts +2 -3
  41. package/lib/common/architect-prompt-template.d.ts.map +1 -1
  42. package/lib/common/architect-prompt-template.js +49 -11
  43. package/lib/common/architect-prompt-template.js.map +1 -1
  44. package/lib/common/coder-replace-prompt-template.d.ts +6 -3
  45. package/lib/common/coder-replace-prompt-template.d.ts.map +1 -1
  46. package/lib/common/coder-replace-prompt-template.js +148 -22
  47. package/lib/common/coder-replace-prompt-template.js.map +1 -1
  48. package/lib/common/command-chat-agents.d.ts +1 -1
  49. package/lib/common/command-chat-agents.d.ts.map +1 -1
  50. package/lib/common/command-chat-agents.js +4 -4
  51. package/lib/common/command-chat-agents.js.map +1 -1
  52. package/lib/common/command-prompt-template.d.ts +2 -2
  53. package/lib/common/command-prompt-template.d.ts.map +1 -1
  54. package/lib/common/command-prompt-template.js +18 -15
  55. package/lib/common/command-prompt-template.js.map +1 -1
  56. package/lib/common/orchestrator-chat-agent.d.ts +2 -3
  57. package/lib/common/orchestrator-chat-agent.d.ts.map +1 -1
  58. package/lib/common/orchestrator-chat-agent.js +11 -26
  59. package/lib/common/orchestrator-chat-agent.js.map +1 -1
  60. package/lib/common/orchestrator-prompt-template.d.ts +2 -2
  61. package/lib/common/orchestrator-prompt-template.d.ts.map +1 -1
  62. package/lib/common/orchestrator-prompt-template.js +4 -1
  63. package/lib/common/orchestrator-prompt-template.js.map +1 -1
  64. package/lib/common/universal-chat-agent.d.ts +5 -1
  65. package/lib/common/universal-chat-agent.d.ts.map +1 -1
  66. package/lib/common/universal-chat-agent.js +2 -2
  67. package/lib/common/universal-chat-agent.js.map +1 -1
  68. package/lib/common/universal-prompt-template.d.ts +3 -3
  69. package/lib/common/universal-prompt-template.d.ts.map +1 -1
  70. package/lib/common/universal-prompt-template.js +1 -2
  71. package/lib/common/universal-prompt-template.js.map +1 -1
  72. package/package.json +17 -17
  73. package/src/browser/ai-configuration/agent-configuration-widget.tsx +31 -24
  74. package/src/browser/ai-configuration/ai-configuration-view-contribution.ts +1 -1
  75. package/src/browser/ai-configuration/ai-configuration-widget.tsx +6 -0
  76. package/src/browser/ai-configuration/prompt-fragments-configuration-widget.tsx +710 -0
  77. package/src/browser/ai-configuration/template-settings-renderer.tsx +18 -38
  78. package/src/browser/architect-agent.ts +3 -3
  79. package/src/browser/coder-agent.ts +10 -5
  80. package/src/browser/file-changeset-functions.ts +152 -14
  81. package/src/browser/frontend-module.ts +14 -2
  82. package/src/browser/style/index.css +320 -0
  83. package/src/browser/template-preference-contribution.ts +4 -4
  84. package/src/browser/workspace-functions.ts +3 -3
  85. package/src/common/architect-prompt-template.ts +54 -14
  86. package/src/common/coder-replace-prompt-template.ts +150 -24
  87. package/src/common/command-chat-agents.ts +4 -4
  88. package/src/common/command-prompt-template.ts +21 -18
  89. package/src/common/orchestrator-chat-agent.ts +12 -28
  90. package/src/common/orchestrator-prompt-template.ts +7 -4
  91. package/src/common/universal-chat-agent.ts +2 -2
  92. package/src/common/universal-prompt-template.ts +4 -5
@@ -0,0 +1,710 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
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 { nls } from '@theia/core';
18
+ import { ConfirmDialog, ReactWidget, codicon } from '@theia/core/lib/browser';
19
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
20
+ import {
21
+ CustomizedPromptFragment,
22
+ PromptFragment,
23
+ isCustomizedPromptFragment,
24
+ isBasePromptFragment,
25
+ PromptService,
26
+ BasePromptFragment
27
+ } from '@theia/ai-core/lib/common/prompt-service';
28
+ import * as React from '@theia/core/shared/react';
29
+ import '../../../src/browser/style/index.css';
30
+ import { AgentService } from '@theia/ai-core/lib/common/agent-service';
31
+ import { Agent } from '@theia/ai-core/lib/common/agent';
32
+ import { CustomizationSource } from '@theia/ai-core/lib/browser/frontend-prompt-customization-service';
33
+
34
+ /**
35
+ * Widget for configuring AI prompt fragments and prompt variant sets.
36
+ * Allows users to view, create, edit, and manage various types of prompt
37
+ * fragments including their customizations and variants.
38
+ */
39
+ @injectable()
40
+ export class AIPromptFragmentsConfigurationWidget extends ReactWidget {
41
+
42
+ static readonly ID = 'ai-prompt-fragments-configuration';
43
+ static readonly LABEL = nls.localize('theia/ai/core/promptFragmentsConfiguration/label', 'Prompt Fragments');
44
+
45
+ /**
46
+ * Stores all available prompt fragments by ID
47
+ */
48
+ protected promptFragmentMap: Map<string, PromptFragment[]> = new Map<string, PromptFragment[]>();
49
+
50
+ /**
51
+ * Stores prompt variant sets and their variant IDs
52
+ */
53
+ protected promptVariantsMap: Map<string, string[]> = new Map<string, string[]>();
54
+
55
+ /**
56
+ * Currently active prompt fragments
57
+ */
58
+ protected activePromptFragments: PromptFragment[] = [];
59
+
60
+ /**
61
+ * Tracks expanded state of prompt fragment sections in the UI
62
+ */
63
+ protected expandedPromptFragmentIds: Set<string> = new Set();
64
+
65
+ /**
66
+ * Tracks expanded state of prompt content display
67
+ */
68
+ protected expandedPromptFragmentTemplates: Set<string> = new Set();
69
+
70
+ /**
71
+ * Tracks expanded state of prompt variant set sections
72
+ */
73
+ protected expandedPromptVariantSetIds: Set<string> = new Set();
74
+
75
+ /**
76
+ * All available agents that may use prompts
77
+ */
78
+ protected availableAgents: Agent[] = [];
79
+
80
+ /**
81
+ * Maps prompt variant set IDs to their currently selected variant IDs
82
+ */
83
+ protected selectedVariantIds: Map<string, string | undefined> = new Map();
84
+
85
+ /**
86
+ * Maps prompt variant set IDs to their default variant IDs
87
+ */
88
+ protected defaultVariantIds: Map<string, string | undefined> = new Map();
89
+
90
+ @inject(PromptService) protected promptService: PromptService;
91
+ @inject(AgentService) protected agentService: AgentService;
92
+
93
+ @postConstruct()
94
+ protected init(): void {
95
+ this.id = AIPromptFragmentsConfigurationWidget.ID;
96
+ this.title.label = AIPromptFragmentsConfigurationWidget.LABEL;
97
+ this.title.caption = AIPromptFragmentsConfigurationWidget.LABEL;
98
+ this.title.closable = true;
99
+ this.addClass('ai-configuration-tab-content');
100
+ this.loadPromptFragments();
101
+ this.loadAgents();
102
+
103
+ this.toDispose.pushAll([
104
+ this.promptService.onPromptsChange(() => {
105
+ this.loadPromptFragments();
106
+ }),
107
+ this.promptService.onSelectedVariantChange(notification => {
108
+ this.selectedVariantIds.set(notification.promptVariantSetId, notification.variantId);
109
+ this.update();
110
+ }),
111
+ this.agentService.onDidChangeAgents(() => {
112
+ this.loadAgents();
113
+ })
114
+ ]);
115
+ }
116
+
117
+ /**
118
+ * Loads all prompt fragments and prompt variant sets from the prompt service.
119
+ * Preserves UI expansion states and updates variant information.
120
+ */
121
+ protected async loadPromptFragments(): Promise<void> {
122
+ this.promptFragmentMap = this.promptService.getAllPromptFragments();
123
+ this.promptVariantsMap = this.promptService.getPromptVariantSets();
124
+ this.activePromptFragments = this.promptService.getActivePromptFragments();
125
+
126
+ // Preserve expansion state when reloading
127
+ const existingExpandedFragmentIds = new Set(this.expandedPromptFragmentIds);
128
+ const existingExpandedPromptVariantIds = new Set(this.expandedPromptVariantSetIds);
129
+ const existingExpandedTemplates = new Set(this.expandedPromptFragmentTemplates);
130
+
131
+ // If no sections were previously expanded, expand all by default
132
+ if (existingExpandedFragmentIds.size === 0) {
133
+ this.expandedPromptFragmentIds = new Set(Array.from(this.promptFragmentMap.keys()));
134
+ } else {
135
+ // Keep existing expansion state but remove entries for fragments that no longer exist
136
+ this.expandedPromptFragmentIds = new Set(
137
+ Array.from(existingExpandedFragmentIds).filter(id => this.promptFragmentMap.has(id))
138
+ );
139
+ }
140
+
141
+ if (existingExpandedPromptVariantIds.size === 0) {
142
+ this.expandedPromptVariantSetIds = new Set(Array.from(this.promptVariantsMap.keys()));
143
+ } else {
144
+ // Keep existing expansion state but remove entries for prompt variant sets that no longer exist
145
+ this.expandedPromptVariantSetIds = new Set(
146
+ Array.from(existingExpandedPromptVariantIds).filter(id => this.promptVariantsMap.has(id))
147
+ );
148
+ }
149
+
150
+ // For templates, preserve existing expanded states - don't expand by default
151
+ this.expandedPromptFragmentTemplates = new Set(
152
+ Array.from(existingExpandedTemplates).filter(id => {
153
+ const [fragmentId] = id.split('_');
154
+ return this.promptFragmentMap.has(fragmentId);
155
+ })
156
+ );
157
+
158
+ // Update variant information (selected/default) for prompt variant sets
159
+ for (const promptVariantSetId of this.promptVariantsMap.keys()) {
160
+ const selectedId = await this.promptService.getSelectedVariantId(promptVariantSetId);
161
+ const defaultId = await this.promptService.getDefaultVariantId(promptVariantSetId);
162
+ this.selectedVariantIds.set(promptVariantSetId, selectedId);
163
+ this.defaultVariantIds.set(promptVariantSetId, defaultId);
164
+ }
165
+
166
+ this.update();
167
+ }
168
+
169
+ /**
170
+ * Loads all available agents from the agent service
171
+ */
172
+ protected loadAgents(): void {
173
+ this.availableAgents = this.agentService.getAllAgents();
174
+ this.update();
175
+ }
176
+
177
+ /**
178
+ * Finds agents that use a specific prompt variant set
179
+ * @param promptVariantSetId ID of the prompt variant set to match
180
+ * @returns Array of agents that use the prompt variant set
181
+ */
182
+ protected getAgentsUsingPromptVariantId(promptVariantSetId: string): Agent[] {
183
+ return this.availableAgents.filter((agent: Agent) =>
184
+ agent.prompts.find(promptVariantSet => promptVariantSet.id === promptVariantSetId)
185
+ );
186
+ }
187
+
188
+ protected togglePromptVariantSetExpansion = (promptVariantSetId: string): void => {
189
+ if (this.expandedPromptVariantSetIds.has(promptVariantSetId)) {
190
+ this.expandedPromptVariantSetIds.delete(promptVariantSetId);
191
+ } else {
192
+ this.expandedPromptVariantSetIds.add(promptVariantSetId);
193
+ }
194
+ this.update();
195
+ };
196
+
197
+ protected togglePromptFragmentExpansion = (promptFragmentId: string): void => {
198
+ if (this.expandedPromptFragmentIds.has(promptFragmentId)) {
199
+ this.expandedPromptFragmentIds.delete(promptFragmentId);
200
+ } else {
201
+ this.expandedPromptFragmentIds.add(promptFragmentId);
202
+ }
203
+ this.update();
204
+ };
205
+
206
+ protected toggleTemplateExpansion = (fragmentKey: string, event: React.MouseEvent): void => {
207
+ event.stopPropagation();
208
+ if (this.expandedPromptFragmentTemplates.has(fragmentKey)) {
209
+ this.expandedPromptFragmentTemplates.delete(fragmentKey);
210
+ } else {
211
+ this.expandedPromptFragmentTemplates.add(fragmentKey);
212
+ }
213
+ this.update();
214
+ };
215
+
216
+ /**
217
+ * Call the edit action for the provided customized prompt fragment
218
+ * @param promptFragment Fragment to edit
219
+ * @param event Mouse event
220
+ */
221
+ protected editPromptCustomization = (promptFragment: CustomizedPromptFragment, event: React.MouseEvent): void => {
222
+ event.stopPropagation();
223
+ this.promptService.editCustomization(promptFragment.id, promptFragment.customizationId);
224
+ };
225
+
226
+ /**
227
+ * Determines if a prompt fragment is currently the active one for its ID
228
+ * @param promptFragment The prompt fragment to check
229
+ * @returns True if this prompt fragment is the active customization
230
+ */
231
+ protected isActiveCustomization(promptFragment: PromptFragment): boolean {
232
+ const activePromptFragment = this.activePromptFragments.find(activePrompt => activePrompt.id === promptFragment.id);
233
+ if (!activePromptFragment) {
234
+ return false;
235
+ }
236
+
237
+ if (isCustomizedPromptFragment(activePromptFragment) && isCustomizedPromptFragment(promptFragment)) {
238
+ return (
239
+ activePromptFragment.id === promptFragment.id &&
240
+ activePromptFragment.template === promptFragment.template &&
241
+ activePromptFragment.customizationId === promptFragment.customizationId &&
242
+ activePromptFragment.priority === promptFragment.priority
243
+ );
244
+ }
245
+
246
+ if (isBasePromptFragment(activePromptFragment) && isBasePromptFragment(promptFragment)) {
247
+ return (
248
+ activePromptFragment.id === promptFragment.id &&
249
+ activePromptFragment.template === promptFragment.template
250
+ );
251
+ }
252
+
253
+ return false;
254
+ }
255
+
256
+ /**
257
+ * Resets a prompt fragment to use a specific customization (with confirmation dialog)
258
+ * @param customization customization to reset to
259
+ * @param event Mouse event
260
+ */
261
+ protected resetToPromptFragment = async (customization: PromptFragment, event: React.MouseEvent): Promise<void> => {
262
+ event.stopPropagation();
263
+
264
+ if (isCustomizedPromptFragment(customization)) {
265
+ // Get the customization type to show in the confirmation dialog
266
+ const type = await this.promptService.getCustomizationType(customization.id, customization.customizationId);
267
+
268
+ const dialog = new ConfirmDialog({
269
+ title: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetToCustomizationDialogTitle', 'Reset to Customization'),
270
+ msg: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetToCustomizationDialogMsg',
271
+ 'Are you sure you want to reset the prompt fragment "{0}" to use the {1} customization? This will remove all higher-priority customizations.',
272
+ customization.id, type),
273
+ ok: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetButton', 'Reset'),
274
+ cancel: nls.localize('theia/ai/core/promptFragmentsConfiguration/cancelButton', 'Cancel')
275
+ });
276
+
277
+ const shouldReset = await dialog.open();
278
+ if (shouldReset) {
279
+ await this.promptService.resetToCustomization(customization.id, customization.customizationId);
280
+ }
281
+ } else {
282
+ const dialog = new ConfirmDialog({
283
+ title: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetToBuiltInDialogTitle', 'Reset to Built-in'),
284
+ msg: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetToBuiltInDialogMsg',
285
+ 'Are you sure you want to reset the prompt fragment "{0}" to its built-in version? This will remove all customizations.', customization.id),
286
+ ok: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetButton', 'Reset'),
287
+ cancel: nls.localize('theia/ai/core/promptFragmentsConfiguration/cancelButton', 'Cancel')
288
+ });
289
+
290
+ const shouldReset = await dialog.open();
291
+ if (shouldReset) {
292
+ await this.promptService.resetToBuiltIn(customization.id);
293
+ }
294
+ }
295
+ };
296
+
297
+ /**
298
+ * Creates a new customization for a built-in prompt fragment
299
+ * @param promptFragment Built-in prompt fragment to customize
300
+ * @param event Mouse event
301
+ */
302
+ protected createPromptFragmentCustomization = (promptFragment: BasePromptFragment, event: React.MouseEvent): void => {
303
+ event.stopPropagation();
304
+ this.promptService.createBuiltInCustomization(promptFragment.id);
305
+ };
306
+
307
+ /**
308
+ * Deletes a customization with confirmation dialog
309
+ * @param customization Customized prompt fragment to delete
310
+ * @param event Mouse event
311
+ */
312
+ protected deletePromptFragmentCustomization = async (customization: CustomizedPromptFragment, event: React.MouseEvent): Promise<void> => {
313
+ event.stopPropagation();
314
+
315
+ // First get the customization type and description to show in the confirmation dialog
316
+ const type = await this.promptService.getCustomizationType(customization.id, customization.customizationId) || '';
317
+ const description = await this.promptService.getCustomizationDescription(customization.id, customization.customizationId) || '';
318
+
319
+ const dialog = new ConfirmDialog({
320
+ title: nls.localize('theia/ai/core/promptFragmentsConfiguration/removeCustomizationDialogTitle', 'Remove Customization'),
321
+ msg: description ?
322
+ nls.localize('theia/ai/core/promptFragmentsConfiguration/removeCustomizationWithDescDialogMsg',
323
+ 'Are you sure you want to remove the {0} customization for prompt fragment "{1}" ({2})?', type, customization.id, description) :
324
+ nls.localize('theia/ai/core/promptFragmentsConfiguration/removeCustomizationDialogMsg',
325
+ 'Are you sure you want to remove the {0} customization for prompt fragment "{1}"?', type, customization.id),
326
+ ok: nls.localize('theia/ai/core/promptFragmentsConfiguration/removeButton', 'Remove'),
327
+ cancel: nls.localize('theia/ai/core/promptFragmentsConfiguration/cancelButton', 'Cancel')
328
+ });
329
+
330
+ const shouldDelete = await dialog.open();
331
+ if (shouldDelete) {
332
+ await this.promptService.removeCustomization(customization.id, customization.customizationId);
333
+ }
334
+ };
335
+
336
+ /**
337
+ * Removes all prompt customizations (resets to built-in versions) with confirmation
338
+ */
339
+ protected removeAllCustomizations = async (): Promise<void> => {
340
+ const dialog = new ConfirmDialog({
341
+ title: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetAllCustomizationsDialogTitle', 'Reset All Customizations'),
342
+ msg: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetAllCustomizationsDialogMsg',
343
+ 'Are you sure you want to reset all prompt fragments to their built-in versions? This will remove all customizations.'),
344
+ ok: nls.localize('theia/ai/core/promptFragmentsConfiguration/resetAllButton', 'Reset All'),
345
+ cancel: nls.localize('theia/ai/core/promptFragmentsConfiguration/cancelButton', 'Cancel')
346
+ });
347
+
348
+ const shouldReset = await dialog.open();
349
+ if (shouldReset) {
350
+ this.promptFragmentMap.forEach(fragments => {
351
+ this.promptService.resetToBuiltIn(fragments[0].id);
352
+ });
353
+ }
354
+ };
355
+
356
+ /**
357
+ * Main render method for the widget
358
+ * @returns Complete UI for the configuration widget
359
+ */
360
+ protected render(): React.ReactNode {
361
+ const nonSystemPromptFragments = this.getNonPromptVariantSetFragments();
362
+
363
+ return (
364
+ <div className='ai-prompt-fragments-configuration'>
365
+ <div className="prompt-fragments-header">
366
+ <h2>{nls.localize('theia/ai/core/promptFragmentsConfiguration/headerTitle', 'Prompt Fragments')}</h2>
367
+ <div className="global-actions">
368
+ <button
369
+ className="global-action-button"
370
+ onClick={this.removeAllCustomizations}
371
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/resetAllCustomizationsTitle', 'Reset all customizations')}
372
+ >
373
+ {nls.localize('theia/ai/core/promptFragmentsConfiguration/resetAllPromptFragments',
374
+ 'Reset all prompt fragments')} <span className={codicon('clear-all')}></span>
375
+ </button>
376
+ </div>
377
+ </div>
378
+
379
+ <div className="prompt-variants-container">
380
+ <h3 className="section-header">{nls.localize('theia/ai/core/promptFragmentsConfiguration/promptVariantsHeader', 'Prompt Variant Sets')}</h3>
381
+ {Array.from(this.promptVariantsMap.entries()).map(([promptVariantSetId, variantIds]) =>
382
+ this.renderPromptVariantSet(promptVariantSetId, variantIds)
383
+ )}
384
+ </div>
385
+
386
+ {nonSystemPromptFragments.size > 0 && <div className="prompt-fragments-container">
387
+ <h3 className="section-header">{nls.localize('theia/ai/core/promptFragmentsConfiguration/otherPromptFragmentsHeader', 'Other Prompt Fragments')}</h3>
388
+ {Array.from(nonSystemPromptFragments.entries()).map(([promptFragmentId, fragments]) =>
389
+ this.renderPromptFragment(promptFragmentId, fragments)
390
+ )}
391
+ </div>}
392
+
393
+ {this.promptFragmentMap.size === 0 && (
394
+ <div className="no-fragments">
395
+ <p>{nls.localize('theia/ai/core/promptFragmentsConfiguration/noFragmentsAvailable', 'No prompt fragments available.')}</p>
396
+ </div>
397
+ )}
398
+ </div>
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Renders a prompt variant set with its variants
404
+ * @param promptVariantSetId ID of the prompt variant set
405
+ * @param variantIds Array of variant IDs
406
+ * @returns React node for the prompt variant set group
407
+ */
408
+ protected renderPromptVariantSet(promptVariantSetId: string, variantIds: string[]): React.ReactNode {
409
+ const isSectionExpanded = this.expandedPromptVariantSetIds.has(promptVariantSetId);
410
+
411
+ // Get selected and default variant IDs from our class properties
412
+ const selectedVariantId = this.selectedVariantIds.get(promptVariantSetId);
413
+ const defaultVariantId = this.defaultVariantIds.get(promptVariantSetId);
414
+
415
+ // Get variant fragments grouped by ID
416
+ const variantGroups = new Map<string, PromptFragment[]>();
417
+
418
+ // First, collect all actual fragments for each variant ID
419
+ for (const variantId of variantIds) {
420
+ if (this.promptFragmentMap.has(variantId)) {
421
+ variantGroups.set(variantId, this.promptFragmentMap.get(variantId)!);
422
+ }
423
+ }
424
+
425
+ const relatedAgents = this.getAgentsUsingPromptVariantId(promptVariantSetId);
426
+
427
+ return (
428
+ <div className="prompt-fragment-section" key={`variant-${promptVariantSetId}`}>
429
+ <div
430
+ className={`prompt-fragment-header ${isSectionExpanded ? 'expanded' : ''}`}
431
+ onClick={() => this.togglePromptVariantSetExpansion(promptVariantSetId)}
432
+ >
433
+ <div className="prompt-fragment-title">
434
+ <span className="expansion-icon">{isSectionExpanded ? '▼' : '▶'}</span>
435
+ <h2>{promptVariantSetId}</h2>
436
+ </div>
437
+ {relatedAgents.length > 0 && (
438
+ <div className="agent-chips-container">
439
+ {relatedAgents.map(agent => (
440
+ <span key={agent.id} className="agent-chip"
441
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/usedByAgentTitle', 'Used by agent: {0}', agent.name)}
442
+ onClick={e => e.stopPropagation()}>
443
+ <span className={codicon('copilot')}></span>
444
+ {agent.name}
445
+ </span>
446
+ ))}
447
+ </div>
448
+ )}
449
+ </div>
450
+ {isSectionExpanded && (
451
+ <div className="prompt-fragment-body">
452
+ <div className="prompt-fragment-description">
453
+ <p>{nls.localize('theia/ai/core/promptFragmentsConfiguration/variantsOfSystemPrompt', 'Variants of this prompt variant set:')}</p>
454
+ </div>
455
+ {Array.from(variantGroups.entries()).map(([variantId, fragments]) => {
456
+ const isVariantExpanded = this.expandedPromptFragmentIds.has(variantId);
457
+
458
+ return (
459
+ <div key={variantId} className={`prompt-fragment-section ${selectedVariantId === variantId ? 'selected-variant' : ''}`}>
460
+ <div
461
+ className={`prompt-fragment-header ${isVariantExpanded ? 'expanded' : ''}`}
462
+ onClick={() => this.togglePromptFragmentExpansion(variantId)}
463
+ >
464
+ <div className="prompt-fragment-title">
465
+ <span className="expansion-icon">{isVariantExpanded ? '▼' : '▶'}</span>
466
+ <h4>{variantId}</h4>
467
+ {defaultVariantId === variantId && (
468
+ <span className="badge default-variant"
469
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/defaultVariantTitle', 'Default variant')}>
470
+ {nls.localize('theia/ai/core/promptFragmentsConfiguration/defaultVariantLabel', 'Default')}
471
+ </span>
472
+ )}
473
+ {selectedVariantId === variantId && (
474
+ <span className="selected-indicator"
475
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/selectedVariantTitle', 'Selected variant')}>
476
+ {nls.localize('theia/ai/core/promptFragmentsConfiguration/selectedVariantLabel', 'Selected')}
477
+ </span>
478
+ )}
479
+ </div>
480
+ </div>
481
+ {isVariantExpanded && (
482
+ <div className='prompt-fragment-body'>
483
+ {fragments.map(fragment => this.renderPromptFragmentCustomization(fragment))}
484
+ </div>
485
+ )}
486
+ </div>
487
+ );
488
+ })}
489
+ </div>
490
+ )}
491
+ </div>
492
+ );
493
+ }
494
+
495
+ /**
496
+ * Gets fragments that aren't part of any prompt variant set
497
+ * @returns Map of fragment IDs to their customizations
498
+ */
499
+ protected getNonPromptVariantSetFragments(): Map<string, PromptFragment[]> {
500
+ const nonSystemPromptFragments = new Map<string, PromptFragment[]>();
501
+ const allVariantIds = new Set<string>();
502
+
503
+ // Collect all variant IDs from prompt variant sets
504
+ this.promptVariantsMap.forEach((variants, _) => {
505
+ variants.forEach(variantId => allVariantIds.add(variantId));
506
+ });
507
+
508
+ // Add prompt variant set main IDs
509
+ this.promptVariantsMap.forEach((_, promptVariantSetId) => {
510
+ allVariantIds.add(promptVariantSetId);
511
+ });
512
+
513
+ // Filter the fragment map to only include non-prompt variant set fragments
514
+ this.promptFragmentMap.forEach((fragments, promptFragmentId) => {
515
+ if (!allVariantIds.has(promptFragmentId)) {
516
+ nonSystemPromptFragments.set(promptFragmentId, fragments);
517
+ }
518
+ });
519
+
520
+ return nonSystemPromptFragments;
521
+ }
522
+
523
+ /**
524
+ * Renders a prompt fragment with all of its customizations
525
+ * @param promptFragmentId ID of the prompt fragment
526
+ * @param customizations Array of the customizations
527
+ * @returns React node for the prompt fragment
528
+ */
529
+ protected renderPromptFragment(promptFragmentId: string, customizations: PromptFragment[]): React.ReactNode {
530
+ const isSectionExpanded = this.expandedPromptFragmentIds.has(promptFragmentId);
531
+
532
+ return (
533
+ <div className={'prompt-fragment-group'} key={promptFragmentId}>
534
+ <div
535
+ className={`prompt-fragment-header ${isSectionExpanded ? 'expanded' : ''}`}
536
+ onClick={() => this.togglePromptFragmentExpansion(promptFragmentId)}
537
+ >
538
+ <div className="prompt-fragment-title">
539
+ <span className="expansion-icon">{isSectionExpanded ? '▼' : '▶'}</span>
540
+ {promptFragmentId}
541
+ </div>
542
+ </div>
543
+ {isSectionExpanded && (
544
+ <div className="prompt-fragment-body">
545
+ {customizations.map(fragment => this.renderPromptFragmentCustomization(fragment))}
546
+ </div>
547
+ )}
548
+ </div>
549
+ );
550
+ }
551
+
552
+ /**
553
+ * Renders a single prompt fragment customization with its controls and content
554
+ * @param promptFragment The prompt fragment to render
555
+ * @returns React node for the prompt fragment
556
+ */
557
+ protected renderPromptFragmentCustomization(promptFragment: PromptFragment): React.ReactNode {
558
+ const isCustomized = isCustomizedPromptFragment(promptFragment);
559
+ const isActive = this.isActiveCustomization(promptFragment);
560
+ // Create a unique key for this fragment to track expansion state
561
+ const fragmentKey = `${promptFragment.id}_${isCustomized ? promptFragment.customizationId : 'built-in'}`;
562
+ const isTemplateExpanded = this.expandedPromptFragmentTemplates.has(fragmentKey);
563
+ const hasCustomizedBuiltIn =
564
+ this.promptFragmentMap.get(promptFragment.id)?.some(fragment => isCustomizedPromptFragment(fragment) && fragment.priority === CustomizationSource.CUSTOMIZED);
565
+
566
+ return (
567
+ <div
568
+ className={`prompt-customization ${isActive ? 'active-customization' : ''}`}
569
+ key={fragmentKey}
570
+ >
571
+ <div className="prompt-customization-header">
572
+ <div className="prompt-customization-title">
573
+ <React.Suspense fallback={<div>Loading...</div>}>
574
+ <CustomizationTypeBadge promptFragment={promptFragment} promptService={this.promptService} />
575
+ </React.Suspense>
576
+ {isActive && (
577
+ <span className="active-indicator"
578
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/activeCustomizationTitle', 'Active customization')}>
579
+ {nls.localize('theia/ai/core/promptFragmentsConfiguration/activeCustomizationLabel', 'Active')}
580
+ </span>
581
+ )}
582
+ </div>
583
+ <div className="prompt-customization-actions">
584
+ {!isCustomized && !hasCustomizedBuiltIn && (
585
+ <button
586
+ className="template-action-button config-button"
587
+ onClick={e => this.createPromptFragmentCustomization(promptFragment, e)}
588
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/createCustomizationTitle', 'Create Customization')}
589
+ >
590
+ <span className={codicon('add')}></span>
591
+ </button>
592
+ )}
593
+ {isCustomized && (
594
+ <button
595
+ className="source-uri-button"
596
+ onClick={e => this.editPromptCustomization(promptFragment, e)}
597
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/editTemplateTitle', 'Edit template')}
598
+ >
599
+ <span className={codicon('edit')}></span>
600
+ </button>
601
+ )}
602
+ {!isActive && (
603
+ <button
604
+ className="template-action-button reset-button"
605
+ onClick={e => this.resetToPromptFragment(promptFragment, e)}
606
+ title={!isCustomized ?
607
+ nls.localize('theia/ai/core/promptFragmentsConfiguration/resetToBuiltInTitle', 'Reset to this built-in') :
608
+ nls.localize('theia/ai/core/promptFragmentsConfiguration/resetToCustomizationTitle', 'Reset to this customization')}
609
+ >
610
+ <span className={codicon('discard')}></span>
611
+ </button>
612
+ )}
613
+ {isCustomized && (
614
+ <button
615
+ className="template-action-button delete-button"
616
+ onClick={e => this.deletePromptFragmentCustomization(promptFragment, e)}
617
+ title={nls.localize('theia/ai/core/promptFragmentsConfiguration/deleteCustomizationTitle', 'Delete Customization')}
618
+ >
619
+ <span className={codicon('trash')}></span>
620
+ </button>
621
+ )}
622
+ </div>
623
+ </div>
624
+
625
+ {isCustomized && (
626
+ <React.Suspense fallback={<div>Loading...</div>}>
627
+ <DescriptionBadge promptFragment={promptFragment} promptService={this.promptService} />
628
+ </React.Suspense>
629
+ )}
630
+
631
+ <div className="template-content-container">
632
+ <div
633
+ className="template-toggle-button"
634
+ onClick={e => this.toggleTemplateExpansion(fragmentKey, e)}
635
+ >
636
+ <span className="template-expansion-icon">{isTemplateExpanded ? '▼' : '▶'}</span>
637
+ <span>{nls.localize('theia/ai/core/promptFragmentsConfiguration/promptTemplateText', 'Prompt Template Text')}</span>
638
+ </div>
639
+
640
+ {isTemplateExpanded && (
641
+ <div className="template-content">
642
+ <pre>{promptFragment.template}</pre>
643
+ </div>
644
+ )}
645
+ </div>
646
+ </div>
647
+ );
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Props for the CustomizationTypeBadge component
653
+ */
654
+ interface CustomizationTypeBadgeProps {
655
+ promptFragment: PromptFragment;
656
+ promptService: PromptService;
657
+ }
658
+
659
+ /**
660
+ * Displays a badge indicating the type of a prompt fragment customization (built-in, user, workspace)
661
+ */
662
+ const CustomizationTypeBadge: React.FC<CustomizationTypeBadgeProps> = ({ promptFragment, promptService }) => {
663
+ const [typeLabel, setTypeLabel] = React.useState<string>('unknown');
664
+
665
+ React.useEffect(() => {
666
+ const fetchCustomizationType = async () => {
667
+ if (isCustomizedPromptFragment(promptFragment)) {
668
+ const customizationType = await promptService.getCustomizationType(promptFragment.id, promptFragment.customizationId);
669
+ setTypeLabel(`${customizationType ?
670
+ customizationType + ' ' + nls.localize('theia/ai/core/promptFragmentsConfiguration/customization', 'customization')
671
+ : nls.localize('theia/ai/core/promptFragmentsConfiguration/customizationLabel', 'Customization')}`);
672
+ } else {
673
+ setTypeLabel(nls.localize('theia/ai/core/promptFragmentsConfiguration/builtInLabel', 'Built-in'));
674
+ }
675
+ };
676
+
677
+ fetchCustomizationType();
678
+ }, [promptFragment, promptService]);
679
+
680
+ return <span>{typeLabel}</span>;
681
+ };
682
+
683
+ /**
684
+ * Props for the DescriptionBadge component
685
+ */
686
+ interface CustomizationDescriptionBadgeProps {
687
+ promptFragment: CustomizedPromptFragment;
688
+ promptService: PromptService;
689
+ }
690
+
691
+ /**
692
+ * Displays the description of a customized prompt fragment if available
693
+ */
694
+ const DescriptionBadge: React.FC<CustomizationDescriptionBadgeProps> = ({ promptFragment, promptService }) => {
695
+ const [description, setDescription] = React.useState<string>('');
696
+
697
+ React.useEffect(() => {
698
+ const fetchDescription = async () => {
699
+ const customizationDescription = await promptService.getCustomizationDescription(
700
+ promptFragment.id,
701
+ promptFragment.customizationId
702
+ );
703
+ setDescription(customizationDescription || '');
704
+ };
705
+
706
+ fetchDescription();
707
+ }, [promptFragment.id, promptFragment.customizationId, promptService]);
708
+
709
+ return <span className="prompt-customization-description">{description}</span>;
710
+ };