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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/lib/browser/frontend-prompt-customization-service.d.ts +12 -3
  2. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  3. package/lib/browser/frontend-prompt-customization-service.js +57 -13
  4. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  5. package/lib/browser/frontend-prompt-customization-service.spec.d.ts +2 -0
  6. package/lib/browser/frontend-prompt-customization-service.spec.d.ts.map +1 -0
  7. package/lib/browser/frontend-prompt-customization-service.spec.js +127 -0
  8. package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -0
  9. package/lib/browser/prompttemplate-parser.d.ts +36 -0
  10. package/lib/browser/prompttemplate-parser.d.ts.map +1 -0
  11. package/lib/browser/prompttemplate-parser.js +94 -0
  12. package/lib/browser/prompttemplate-parser.js.map +1 -0
  13. package/lib/common/prompt-service.d.ts +27 -1
  14. package/lib/common/prompt-service.d.ts.map +1 -1
  15. package/lib/common/prompt-service.js +29 -0
  16. package/lib/common/prompt-service.js.map +1 -1
  17. package/lib/common/prompt-service.spec.js +126 -0
  18. package/lib/common/prompt-service.spec.js.map +1 -1
  19. package/lib/common/prompt-text.d.ts +1 -0
  20. package/lib/common/prompt-text.d.ts.map +1 -1
  21. package/lib/common/prompt-text.js +1 -0
  22. package/lib/common/prompt-text.js.map +1 -1
  23. package/lib/common/prompt-variable-contribution.d.ts +2 -0
  24. package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
  25. package/lib/common/prompt-variable-contribution.js +89 -4
  26. package/lib/common/prompt-variable-contribution.js.map +1 -1
  27. package/lib/common/prompt-variable-contribution.spec.d.ts +2 -0
  28. package/lib/common/prompt-variable-contribution.spec.d.ts.map +1 -0
  29. package/lib/common/prompt-variable-contribution.spec.js +163 -0
  30. package/lib/common/prompt-variable-contribution.spec.js.map +1 -0
  31. package/package.json +9 -9
  32. package/src/browser/frontend-prompt-customization-service.spec.ts +145 -0
  33. package/src/browser/frontend-prompt-customization-service.ts +72 -20
  34. package/src/browser/prompttemplate-parser.ts +111 -0
  35. package/src/common/prompt-service.spec.ts +143 -0
  36. package/src/common/prompt-service.ts +73 -1
  37. package/src/common/prompt-text.ts +1 -0
  38. package/src/common/prompt-variable-contribution.spec.ts +236 -0
  39. package/src/common/prompt-variable-contribution.ts +109 -4
@@ -0,0 +1,236 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+
19
+ let disableJSDOM = enableJSDOM();
20
+
21
+ import 'reflect-metadata';
22
+
23
+ import { expect } from 'chai';
24
+ import * as sinon from 'sinon';
25
+ import { Container } from 'inversify';
26
+ import { CommandService, ILogger, Logger } from '@theia/core';
27
+ import { PromptVariableContribution, PROMPT_VARIABLE } from './prompt-variable-contribution';
28
+ import { PromptService, PromptServiceImpl } from './prompt-service';
29
+ import { DefaultAIVariableService, AIVariableService } from './variable-service';
30
+ import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
31
+
32
+ disableJSDOM();
33
+
34
+ describe('PromptVariableContribution', () => {
35
+ before(() => disableJSDOM = enableJSDOM());
36
+ after(() => disableJSDOM());
37
+ let contribution: PromptVariableContribution;
38
+ let promptService: PromptService;
39
+ let container: Container;
40
+
41
+ beforeEach(() => {
42
+ container = new Container();
43
+
44
+ // Set up PromptService
45
+ container.bind<PromptService>(PromptService).to(PromptServiceImpl).inSingletonScope();
46
+ const logger = sinon.createStubInstance(Logger);
47
+ const variableService = new DefaultAIVariableService({ getContributions: () => [] }, logger);
48
+ container.bind<AIVariableService>(AIVariableService).toConstantValue(variableService);
49
+ container.bind<ILogger>(ILogger).toConstantValue(new MockLogger);
50
+
51
+ // Set up CommandService stub (needed for PromptVariableContribution but not used in these tests)
52
+ const commandService = sinon.createStubInstance(Logger); // Using Logger as a simple mock
53
+ container.bind<CommandService>(CommandService).toConstantValue(commandService as unknown as CommandService);
54
+
55
+ // Bind PromptVariableContribution with proper DI
56
+ container.bind<PromptVariableContribution>(PromptVariableContribution).toSelf().inSingletonScope();
57
+
58
+ // Get instances
59
+ promptService = container.get<PromptService>(PromptService);
60
+ contribution = container.get<PromptVariableContribution>(PromptVariableContribution);
61
+ });
62
+
63
+ describe('Command Argument Substitution', () => {
64
+ it('substitutes $ARGUMENTS with full argument string', async () => {
65
+ promptService.addBuiltInPromptFragment({
66
+ id: 'test-cmd',
67
+ template: 'Process: $ARGUMENTS',
68
+ isCommand: true,
69
+ commandName: 'test'
70
+ });
71
+
72
+ const result = await contribution.resolve(
73
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|arg1 arg2 arg3' },
74
+ {}
75
+ );
76
+
77
+ expect(result?.value).to.equal('Process: arg1 arg2 arg3');
78
+ });
79
+
80
+ it('substitutes $0 with command name', async () => {
81
+ promptService.addBuiltInPromptFragment({
82
+ id: 'test-cmd',
83
+ template: 'Command $0 was called',
84
+ isCommand: true,
85
+ commandName: 'test'
86
+ });
87
+
88
+ const result = await contribution.resolve(
89
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|args' },
90
+ {}
91
+ );
92
+
93
+ expect(result?.value).to.equal('Command test-cmd was called');
94
+ });
95
+
96
+ it('substitutes $1, $2, ... with individual arguments', async () => {
97
+ promptService.addBuiltInPromptFragment({
98
+ id: 'compare-cmd',
99
+ template: 'Compare $1 with $2',
100
+ isCommand: true,
101
+ commandName: 'compare'
102
+ });
103
+
104
+ const result = await contribution.resolve(
105
+ { variable: PROMPT_VARIABLE, arg: 'compare-cmd|item1 item2' },
106
+ {}
107
+ );
108
+
109
+ expect(result?.value).to.equal('Compare item1 with item2');
110
+ });
111
+
112
+ it('handles quoted arguments in $1, $2', async () => {
113
+ promptService.addBuiltInPromptFragment({
114
+ id: 'test-cmd',
115
+ template: 'First: $1, Second: $2',
116
+ isCommand: true,
117
+ commandName: 'test'
118
+ });
119
+
120
+ const result = await contribution.resolve(
121
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|"arg with spaces" other' },
122
+ {}
123
+ );
124
+
125
+ expect(result?.value).to.equal('First: arg with spaces, Second: other');
126
+ });
127
+
128
+ it('handles escaped quotes in arguments', async () => {
129
+ promptService.addBuiltInPromptFragment({
130
+ id: 'test-cmd',
131
+ template: 'Arg: $1',
132
+ isCommand: true,
133
+ commandName: 'test'
134
+ });
135
+
136
+ const result = await contribution.resolve(
137
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|"value with \\"quote\\""' },
138
+ {}
139
+ );
140
+
141
+ expect(result?.value).to.equal('Arg: value with "quote"');
142
+ });
143
+
144
+ it('handles 10+ arguments correctly', async () => {
145
+ promptService.addBuiltInPromptFragment({
146
+ id: 'test-cmd',
147
+ template: 'Args: $1 $10 $11',
148
+ isCommand: true,
149
+ commandName: 'test'
150
+ });
151
+
152
+ const result = await contribution.resolve(
153
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|a b c d e f g h i j k' },
154
+ {}
155
+ );
156
+
157
+ expect(result?.value).to.equal('Args: a j k');
158
+ });
159
+
160
+ it('handles command without arguments', async () => {
161
+ promptService.addBuiltInPromptFragment({
162
+ id: 'hello-cmd',
163
+ template: 'Hello, world!',
164
+ isCommand: true,
165
+ commandName: 'hello'
166
+ });
167
+
168
+ const result = await contribution.resolve(
169
+ { variable: PROMPT_VARIABLE, arg: 'hello-cmd' },
170
+ {}
171
+ );
172
+
173
+ expect(result?.value).to.equal('Hello, world!');
174
+ });
175
+
176
+ it('handles non-command prompts without substitution', async () => {
177
+ promptService.addBuiltInPromptFragment({
178
+ id: 'normal-prompt',
179
+ template: 'This has $1 and $ARGUMENTS but is not a command'
180
+ });
181
+
182
+ const result = await contribution.resolve(
183
+ { variable: PROMPT_VARIABLE, arg: 'normal-prompt' },
184
+ {}
185
+ );
186
+
187
+ // No substitution should occur for non-commands
188
+ expect(result?.value).to.equal('This has $1 and $ARGUMENTS but is not a command');
189
+ });
190
+
191
+ it('handles missing argument placeholders gracefully', async () => {
192
+ promptService.addBuiltInPromptFragment({
193
+ id: 'test-cmd',
194
+ template: 'Args: $1 $2 $3',
195
+ isCommand: true,
196
+ commandName: 'test'
197
+ });
198
+
199
+ const result = await contribution.resolve(
200
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|only-one' },
201
+ {}
202
+ );
203
+
204
+ // Missing arguments should remain as placeholders
205
+ expect(result?.value).to.equal('Args: only-one $2 $3');
206
+ });
207
+ });
208
+
209
+ describe('Command Resolution', () => {
210
+ it('resolves command fragments correctly', async () => {
211
+ promptService.addBuiltInPromptFragment({
212
+ id: 'test-cmd',
213
+ template: 'Do something with $ARGUMENTS',
214
+ isCommand: true,
215
+ commandName: 'test'
216
+ });
217
+
218
+ const result = await contribution.resolve(
219
+ { variable: PROMPT_VARIABLE, arg: 'test-cmd|input' },
220
+ {}
221
+ );
222
+
223
+ expect(result?.value).to.equal('Do something with input');
224
+ expect(result?.variable).to.deep.equal(PROMPT_VARIABLE);
225
+ });
226
+
227
+ it('returns empty string for non-existent prompts', async () => {
228
+ const result = await contribution.resolve(
229
+ { variable: PROMPT_VARIABLE, arg: 'non-existent|args' },
230
+ {}
231
+ );
232
+
233
+ expect(result?.value).to.equal('');
234
+ });
235
+ });
236
+ });
@@ -69,13 +69,56 @@ export class PromptVariableContribution implements AIVariableContribution, AIVar
69
69
  resolveDependency?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>
70
70
  ): Promise<ResolvedAIVariable | undefined> {
71
71
  if (request.variable.name === PROMPT_VARIABLE.name) {
72
- const promptId = request.arg?.trim();
73
- if (promptId) {
74
- const resolvedPrompt = await this.promptService.getResolvedPromptFragmentWithoutFunctions(promptId, undefined, context, resolveDependency);
72
+ const arg = request.arg?.trim();
73
+ if (arg) {
74
+ // Check if this is a command-style reference (contains | separator)
75
+ const pipeIndex = arg.indexOf('|');
76
+ const promptIdOrCommandName = pipeIndex >= 0 ? arg.substring(0, pipeIndex) : arg;
77
+ const commandArgs = pipeIndex >= 0 ? arg.substring(pipeIndex + 1) : '';
78
+
79
+ // Determine the actual fragment ID
80
+ // If this is a command invocation (has args), try to find by command name first
81
+ let fragment = commandArgs
82
+ ? this.promptService.getPromptFragmentByCommandName(promptIdOrCommandName)
83
+ : undefined;
84
+
85
+ // Fall back to looking up by fragment ID if not found by command name
86
+ if (!fragment) {
87
+ fragment = this.promptService.getRawPromptFragment(promptIdOrCommandName);
88
+ }
89
+
90
+ // If we still don't have a fragment, we can't resolve
91
+ if (!fragment) {
92
+ this.logger.debug(`Could not find prompt fragment or command '${promptIdOrCommandName}'`);
93
+ return {
94
+ variable: request.variable,
95
+ value: '',
96
+ allResolvedDependencies: []
97
+ };
98
+ }
99
+
100
+ const fragmentId = fragment.id;
101
+
102
+ // Resolve the prompt fragment normally (this handles {{variables}} and ~{functions})
103
+ const resolvedPrompt = await this.promptService.getResolvedPromptFragmentWithoutFunctions(
104
+ fragmentId,
105
+ undefined,
106
+ context,
107
+ resolveDependency
108
+ );
109
+
75
110
  if (resolvedPrompt) {
111
+ // If command args were provided, substitute them in the resolved text
112
+ // This happens AFTER variable/function resolution, so $ARGUMENTS can be part of the template
113
+ // alongside {{variables}} which get resolved first
114
+ const isCommand = fragment?.isCommand === true;
115
+ const finalText = isCommand && commandArgs
116
+ ? this.substituteCommandArguments(resolvedPrompt.text, promptIdOrCommandName, commandArgs)
117
+ : resolvedPrompt.text;
118
+
76
119
  return {
77
120
  variable: request.variable,
78
- value: resolvedPrompt.text,
121
+ value: finalText,
79
122
  allResolvedDependencies: resolvedPrompt.variables
80
123
  };
81
124
  }
@@ -89,6 +132,68 @@ export class PromptVariableContribution implements AIVariableContribution, AIVar
89
132
  };
90
133
  }
91
134
 
135
+ private substituteCommandArguments(template: string, commandName: string, commandArgs: string): string {
136
+ // Parse arguments (respecting quotes)
137
+ const args = this.parseCommandArguments(commandArgs);
138
+
139
+ // Substitute $ARGUMENTS with full arg string
140
+ let result = template.replace(/\$ARGUMENTS/g, commandArgs);
141
+
142
+ // Substitute $0 with command name
143
+ result = result.replace(/\$0/g, commandName);
144
+
145
+ // Substitute numbered arguments in reverse order to avoid collision
146
+ // (e.g., $10 before $1 to prevent $1 from matching the "1" in "$10")
147
+ for (let i = args.length; i > 0; i--) {
148
+ const regex = new RegExp(`\\$${i}\\b`, 'g');
149
+ result = result.replace(regex, args[i - 1]);
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ private parseCommandArguments(commandArgs: string): string[] {
156
+ const args: string[] = [];
157
+ let current = '';
158
+ let inQuotes = false;
159
+ let quoteChar = '';
160
+
161
+ for (let i = 0; i < commandArgs.length; i++) {
162
+ const char = commandArgs[i];
163
+
164
+ // Handle escape sequences within quotes
165
+ if (char === '\\' && i + 1 < commandArgs.length && inQuotes) {
166
+ const nextChar = commandArgs[i + 1];
167
+ if (nextChar === '"' || nextChar === "'" || nextChar === '\\') {
168
+ current += nextChar;
169
+ i++; // Skip the next character
170
+ continue;
171
+ }
172
+ }
173
+
174
+ if ((char === '"' || char === "'") && !inQuotes) {
175
+ inQuotes = true;
176
+ quoteChar = char;
177
+ } else if (char === quoteChar && inQuotes) {
178
+ inQuotes = false;
179
+ quoteChar = '';
180
+ } else if (char === ' ' && !inQuotes) {
181
+ if (current.trim()) {
182
+ args.push(current.trim());
183
+ current = '';
184
+ }
185
+ } else {
186
+ current += char;
187
+ }
188
+ }
189
+
190
+ if (current.trim()) {
191
+ args.push(current.trim());
192
+ }
193
+
194
+ return args;
195
+ }
196
+
92
197
  protected async triggerArgumentPicker(): Promise<string | undefined> {
93
198
  // Trigger the suggestion command to show argument completions
94
199
  this.commandService.executeCommand('editor.action.triggerSuggest');