@theia/ai-core 1.66.0-next.41 → 1.66.0-next.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/frontend-prompt-customization-service.d.ts +12 -3
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +57 -13
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.spec.d.ts +2 -0
- package/lib/browser/frontend-prompt-customization-service.spec.d.ts.map +1 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js +127 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -0
- package/lib/browser/prompttemplate-parser.d.ts +36 -0
- package/lib/browser/prompttemplate-parser.d.ts.map +1 -0
- package/lib/browser/prompttemplate-parser.js +94 -0
- package/lib/browser/prompttemplate-parser.js.map +1 -0
- package/lib/common/prompt-service.d.ts +27 -1
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +29 -0
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +126 -0
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/prompt-text.d.ts +1 -0
- package/lib/common/prompt-text.d.ts.map +1 -1
- package/lib/common/prompt-text.js +1 -0
- package/lib/common/prompt-text.js.map +1 -1
- package/lib/common/prompt-variable-contribution.d.ts +2 -0
- package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
- package/lib/common/prompt-variable-contribution.js +89 -4
- package/lib/common/prompt-variable-contribution.js.map +1 -1
- package/lib/common/prompt-variable-contribution.spec.d.ts +2 -0
- package/lib/common/prompt-variable-contribution.spec.d.ts.map +1 -0
- package/lib/common/prompt-variable-contribution.spec.js +163 -0
- package/lib/common/prompt-variable-contribution.spec.js.map +1 -0
- package/package.json +9 -9
- package/src/browser/frontend-prompt-customization-service.spec.ts +145 -0
- package/src/browser/frontend-prompt-customization-service.ts +72 -20
- package/src/browser/prompttemplate-parser.ts +111 -0
- package/src/common/prompt-service.spec.ts +143 -0
- package/src/common/prompt-service.ts +73 -1
- package/src/common/prompt-text.ts +1 -0
- package/src/common/prompt-variable-contribution.spec.ts +236 -0
- package/src/common/prompt-variable-contribution.ts +109 -4
|
@@ -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
|
|
73
|
-
if (
|
|
74
|
-
|
|
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:
|
|
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');
|