@theia/ai-core 1.63.0-next.24 → 1.63.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 (95) hide show
  1. package/lib/browser/agent-completion-notification-service.d.ts +69 -0
  2. package/lib/browser/agent-completion-notification-service.d.ts.map +1 -0
  3. package/lib/browser/agent-completion-notification-service.js +187 -0
  4. package/lib/browser/agent-completion-notification-service.js.map +1 -0
  5. package/lib/browser/agent-preferences.d.ts.map +1 -1
  6. package/lib/browser/agent-preferences.js +11 -0
  7. package/lib/browser/agent-preferences.js.map +1 -1
  8. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  9. package/lib/browser/ai-core-frontend-module.js +9 -1
  10. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  11. package/lib/browser/ai-core-preferences.d.ts +5 -0
  12. package/lib/browser/ai-core-preferences.d.ts.map +1 -1
  13. package/lib/browser/ai-core-preferences.js +22 -1
  14. package/lib/browser/ai-core-preferences.js.map +1 -1
  15. package/lib/browser/frontend-language-model-registry.d.ts +5 -5
  16. package/lib/browser/frontend-language-model-registry.d.ts.map +1 -1
  17. package/lib/browser/frontend-language-model-registry.js +12 -5
  18. package/lib/browser/frontend-language-model-registry.js.map +1 -1
  19. package/lib/browser/frontend-language-model-registry.spec.d.ts +2 -0
  20. package/lib/browser/frontend-language-model-registry.spec.d.ts.map +1 -0
  21. package/lib/browser/frontend-language-model-registry.spec.js +248 -0
  22. package/lib/browser/frontend-language-model-registry.spec.js.map +1 -0
  23. package/lib/browser/frontend-variable-service.d.ts +12 -0
  24. package/lib/browser/frontend-variable-service.d.ts.map +1 -1
  25. package/lib/browser/frontend-variable-service.js +23 -0
  26. package/lib/browser/frontend-variable-service.js.map +1 -1
  27. package/lib/browser/index.d.ts +5 -0
  28. package/lib/browser/index.d.ts.map +1 -1
  29. package/lib/browser/index.js +5 -0
  30. package/lib/browser/index.js.map +1 -1
  31. package/lib/browser/open-editors-variable-contribution.d.ts +17 -0
  32. package/lib/browser/open-editors-variable-contribution.d.ts.map +1 -0
  33. package/lib/browser/open-editors-variable-contribution.js +84 -0
  34. package/lib/browser/open-editors-variable-contribution.js.map +1 -0
  35. package/lib/browser/os-notification-service.d.ts +96 -0
  36. package/lib/browser/os-notification-service.d.ts.map +1 -0
  37. package/lib/browser/os-notification-service.js +222 -0
  38. package/lib/browser/os-notification-service.js.map +1 -0
  39. package/lib/browser/window-blink-service.d.ts +41 -0
  40. package/lib/browser/window-blink-service.d.ts.map +1 -0
  41. package/lib/browser/window-blink-service.js +174 -0
  42. package/lib/browser/window-blink-service.js.map +1 -0
  43. package/lib/common/configurable-in-memory-resources.d.ts +2 -0
  44. package/lib/common/configurable-in-memory-resources.d.ts.map +1 -1
  45. package/lib/common/configurable-in-memory-resources.js +7 -0
  46. package/lib/common/configurable-in-memory-resources.js.map +1 -1
  47. package/lib/common/index.d.ts +1 -0
  48. package/lib/common/index.d.ts.map +1 -1
  49. package/lib/common/index.js +1 -0
  50. package/lib/common/index.js.map +1 -1
  51. package/lib/common/language-model-delegate.d.ts +2 -2
  52. package/lib/common/language-model-delegate.d.ts.map +1 -1
  53. package/lib/common/language-model-delegate.js.map +1 -1
  54. package/lib/common/language-model.d.ts +44 -4
  55. package/lib/common/language-model.d.ts.map +1 -1
  56. package/lib/common/language-model.js +16 -1
  57. package/lib/common/language-model.js.map +1 -1
  58. package/lib/common/notification-types.d.ts +7 -0
  59. package/lib/common/notification-types.d.ts.map +1 -0
  60. package/lib/common/notification-types.js +29 -0
  61. package/lib/common/notification-types.js.map +1 -0
  62. package/lib/common/prompt-service.d.ts +14 -4
  63. package/lib/common/prompt-service.d.ts.map +1 -1
  64. package/lib/common/prompt-service.js +78 -9
  65. package/lib/common/prompt-service.js.map +1 -1
  66. package/lib/common/prompt-service.spec.js +3 -0
  67. package/lib/common/prompt-service.spec.js.map +1 -1
  68. package/lib/common/settings-service.d.ts +6 -0
  69. package/lib/common/settings-service.d.ts.map +1 -1
  70. package/lib/common/settings-service.js.map +1 -1
  71. package/lib/common/variable-service.d.ts +2 -0
  72. package/lib/common/variable-service.d.ts.map +1 -1
  73. package/lib/common/variable-service.js +10 -2
  74. package/lib/common/variable-service.js.map +1 -1
  75. package/package.json +10 -10
  76. package/src/browser/agent-completion-notification-service.ts +242 -0
  77. package/src/browser/agent-preferences.ts +14 -0
  78. package/src/browser/ai-core-frontend-module.ts +10 -0
  79. package/src/browser/ai-core-preferences.ts +30 -0
  80. package/src/browser/frontend-language-model-registry.spec.ts +298 -0
  81. package/src/browser/frontend-language-model-registry.ts +14 -8
  82. package/src/browser/frontend-variable-service.ts +36 -0
  83. package/src/browser/index.ts +5 -0
  84. package/src/browser/open-editors-variable-contribution.ts +88 -0
  85. package/src/browser/os-notification-service.ts +271 -0
  86. package/src/browser/window-blink-service.ts +195 -0
  87. package/src/common/configurable-in-memory-resources.ts +8 -0
  88. package/src/common/index.ts +1 -0
  89. package/src/common/language-model-delegate.ts +5 -2
  90. package/src/common/language-model.ts +32 -4
  91. package/src/common/notification-types.ts +31 -0
  92. package/src/common/prompt-service.spec.ts +4 -1
  93. package/src/common/prompt-service.ts +101 -12
  94. package/src/common/settings-service.ts +6 -0
  95. package/src/common/variable-service.ts +11 -1
@@ -18,11 +18,18 @@ import { nls } from '@theia/core';
18
18
  import { PreferenceContribution, PreferenceProxy, PreferenceSchema } from '@theia/core/lib/browser';
19
19
  import { PreferenceProxyFactory } from '@theia/core/lib/browser/preferences/injectable-preference-proxy';
20
20
  import { interfaces } from '@theia/core/shared/inversify';
21
+ import {
22
+ NOTIFICATION_TYPES,
23
+ NOTIFICATION_TYPE_OFF,
24
+ NotificationType
25
+ } from '../common/notification-types';
21
26
 
22
27
  export const AI_CORE_PREFERENCES_TITLE = nls.localize('theia/ai/core/prefs/title', '✨ AI Features [Alpha]');
23
28
  export const PREFERENCE_NAME_ENABLE_AI = 'ai-features.AiEnable.enableAI';
24
29
  export const PREFERENCE_NAME_PROMPT_TEMPLATES = 'ai-features.promptTemplates.promptTemplatesFolder';
25
30
  export const PREFERENCE_NAME_REQUEST_SETTINGS = 'ai-features.modelSettings.requestSettings';
31
+ export const PREFERENCE_NAME_MAX_RETRIES = 'ai-features.modelSettings.maxRetries';
32
+ export const PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE = 'ai-features.notifications.default';
26
33
 
27
34
  export const aiCorePreferenceSchema: PreferenceSchema = {
28
35
  type: 'object',
@@ -121,13 +128,36 @@ export const aiCorePreferenceSchema: PreferenceSchema = {
121
128
  additionalProperties: false
122
129
  },
123
130
  default: [],
131
+ },
132
+ [PREFERENCE_NAME_MAX_RETRIES]: {
133
+ title: nls.localize('theia/ai/core/maxRetries/title', 'Maximum Retries'),
134
+ markdownDescription: nls.localize('theia/ai/core/maxRetries/mdDescription',
135
+ 'The maximum number of retry attempts when a request to an AI provider fails. A value of 0 means no retries.'),
136
+ type: 'number',
137
+ minimum: 0,
138
+ default: 3
139
+ },
140
+ [PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE]: {
141
+ title: nls.localize('theia/ai/core/defaultNotification/title', 'Default Notification Type'),
142
+ markdownDescription: nls.localize('theia/ai/core/defaultNotification/mdDescription',
143
+ 'The default notification method used when an AI agent completes a task. Individual agents can override this setting.\n\
144
+ - `os-notification`: Show OS/system notifications\n\
145
+ - `message`: Show notifications in the status bar/message area\n\
146
+ - `blink`: Blink or highlight the UI\n\
147
+ - `off`: Disable all notifications'),
148
+ type: 'string',
149
+ enum: [...NOTIFICATION_TYPES],
150
+ default: NOTIFICATION_TYPE_OFF
124
151
  }
125
152
  }
126
153
  };
154
+
127
155
  export interface AICoreConfiguration {
128
156
  [PREFERENCE_NAME_ENABLE_AI]: boolean | undefined;
129
157
  [PREFERENCE_NAME_PROMPT_TEMPLATES]: string | undefined;
130
158
  [PREFERENCE_NAME_REQUEST_SETTINGS]: Array<RequestSetting> | undefined;
159
+ [PREFERENCE_NAME_MAX_RETRIES]: number | undefined;
160
+ [PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE]: NotificationType | undefined;
131
161
  }
132
162
 
133
163
  export interface RequestSetting {
@@ -0,0 +1,298 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource.
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
+ let disableJSDOM = enableJSDOM();
19
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
20
+ FrontendApplicationConfigProvider.set({});
21
+
22
+ import { expect } from 'chai';
23
+ import {
24
+ LanguageModelRequest,
25
+ ToolRequest
26
+ } from '../common';
27
+
28
+ disableJSDOM();
29
+
30
+ interface ErrorObject {
31
+ error: boolean;
32
+ message: string;
33
+ }
34
+
35
+ function isErrorObject(obj: unknown): obj is ErrorObject {
36
+ return !!obj && typeof obj === 'object' && 'error' in obj && 'message' in obj;
37
+ }
38
+
39
+ // This class provides a minimal implementation focused solely on testing the toolCall method.
40
+ // We cannot extend FrontendLanguageModelRegistryImpl directly due to issues in the test environment:
41
+ // - FrontendLanguageModelRegistryImpl imports dependencies that transitively depend on 'p-queue'
42
+ // - p-queue is an ESM-only module that cannot be loaded in the current test environment
43
+ class TestableLanguageModelRegistry {
44
+ private requests = new Map<string, LanguageModelRequest>();
45
+
46
+ async toolCall(id: string, toolId: string, arg_string: string): Promise<unknown> {
47
+ if (!this.requests.has(id)) {
48
+ return { error: true, message: `No request found for ID '${id}'. The request may have been cancelled or completed.` };
49
+ }
50
+ const request = this.requests.get(id)!;
51
+ const tool = request.tools?.find(t => t.id === toolId);
52
+ if (tool) {
53
+ try {
54
+ return await tool.handler(arg_string);
55
+ } catch (error) {
56
+ const errorMessage = error instanceof Error ? error.message : String(error);
57
+ return { error: true, message: `Error executing tool '${toolId}': ${errorMessage}` };
58
+ };
59
+ }
60
+ return { error: true, message: `Tool '${toolId}' not found in the available tools for this request.` };
61
+ }
62
+
63
+ // Test helper method
64
+ setRequest(id: string, request: LanguageModelRequest): void {
65
+ this.requests.set(id, request);
66
+ }
67
+ }
68
+
69
+ describe('FrontendLanguageModelRegistryImpl toolCall functionality', () => {
70
+ let registry: TestableLanguageModelRegistry;
71
+
72
+ before(() => {
73
+ disableJSDOM = enableJSDOM();
74
+ });
75
+
76
+ after(() => {
77
+ disableJSDOM();
78
+ });
79
+
80
+ beforeEach(() => {
81
+ registry = new TestableLanguageModelRegistry();
82
+ });
83
+
84
+ describe('toolCall', () => {
85
+ it('should return error object when request ID does not exist', async () => {
86
+ const result = await registry.toolCall('nonexistent-id', 'test-tool', '{}');
87
+
88
+ expect(result).to.be.an('object');
89
+ expect(isErrorObject(result)).to.be.true;
90
+ if (isErrorObject(result)) {
91
+ expect(result.error).to.be.true;
92
+ expect(result.message).to.include('No request found for ID \'nonexistent-id\'');
93
+ expect(result.message).to.include('The request may have been cancelled or completed');
94
+ }
95
+ });
96
+
97
+ it('should return error object when tool is not found', async () => {
98
+ // Set up a request without the requested tool
99
+ const requestId = 'test-request-id';
100
+ const mockRequest: LanguageModelRequest = {
101
+ messages: [],
102
+ tools: [
103
+ {
104
+ id: 'different-tool',
105
+ name: 'Different Tool',
106
+ description: 'A different tool',
107
+ parameters: {
108
+ type: 'object',
109
+ properties: {}
110
+ },
111
+ handler: () => Promise.resolve('success')
112
+ }
113
+ ]
114
+ };
115
+
116
+ registry.setRequest(requestId, mockRequest);
117
+
118
+ const result = await registry.toolCall(requestId, 'nonexistent-tool', '{}');
119
+
120
+ expect(result).to.be.an('object');
121
+ expect(isErrorObject(result)).to.be.true;
122
+ if (isErrorObject(result)) {
123
+ expect(result.error).to.be.true;
124
+ expect(result.message).to.include('Tool \'nonexistent-tool\' not found in the available tools for this request');
125
+ }
126
+ });
127
+
128
+ it('should call tool handler successfully when tool exists', async () => {
129
+ const requestId = 'test-request-id';
130
+ const toolId = 'test-tool';
131
+ const expectedResult = 'tool execution result';
132
+
133
+ const mockTool: ToolRequest = {
134
+ id: toolId,
135
+ name: 'Test Tool',
136
+ description: 'A test tool',
137
+ parameters: {
138
+ type: 'object',
139
+ properties: {}
140
+ },
141
+ handler: (args: string) => Promise.resolve(expectedResult)
142
+ };
143
+
144
+ const mockRequest: LanguageModelRequest = {
145
+ messages: [],
146
+ tools: [mockTool]
147
+ };
148
+
149
+ registry.setRequest(requestId, mockRequest);
150
+
151
+ const result = await registry.toolCall(requestId, toolId, '{}');
152
+
153
+ expect(result).to.equal(expectedResult);
154
+ });
155
+
156
+ it('should handle synchronous tool handler errors gracefully', async () => {
157
+ const requestId = 'test-request-id';
158
+ const toolId = 'error-tool';
159
+ const errorMessage = 'Tool execution failed';
160
+
161
+ const mockTool: ToolRequest = {
162
+ id: toolId,
163
+ name: 'Error Tool',
164
+ description: 'A tool that throws an error',
165
+ parameters: {
166
+ type: 'object',
167
+ properties: {}
168
+ },
169
+ handler: () => {
170
+ throw new Error(errorMessage);
171
+ }
172
+ };
173
+
174
+ const mockRequest: LanguageModelRequest = {
175
+ messages: [],
176
+ tools: [mockTool]
177
+ };
178
+
179
+ registry.setRequest(requestId, mockRequest);
180
+
181
+ const result = await registry.toolCall(requestId, toolId, '{}');
182
+
183
+ expect(result).to.be.an('object');
184
+ expect(isErrorObject(result)).to.be.true;
185
+ if (isErrorObject(result)) {
186
+ expect(result.error).to.be.true;
187
+ expect(result.message).to.include(`Error executing tool '${toolId}': ${errorMessage}`);
188
+ }
189
+ });
190
+
191
+ it('should handle non-Error exceptions gracefully', async () => {
192
+ const requestId = 'test-request-id';
193
+ const toolId = 'string-error-tool';
194
+ const errorMessage = 'String error';
195
+
196
+ const mockTool: ToolRequest = {
197
+ id: toolId,
198
+ name: 'String Error Tool',
199
+ description: 'A tool that throws a string',
200
+ parameters: {
201
+ type: 'object',
202
+ properties: {}
203
+ },
204
+ handler: () => {
205
+ // eslint-disable-next-line no-throw-literal
206
+ throw errorMessage;
207
+ }
208
+ };
209
+
210
+ const mockRequest: LanguageModelRequest = {
211
+ messages: [],
212
+ tools: [mockTool]
213
+ };
214
+
215
+ registry.setRequest(requestId, mockRequest);
216
+
217
+ const result = await registry.toolCall(requestId, toolId, '{}');
218
+
219
+ expect(result).to.be.an('object');
220
+ expect(isErrorObject(result)).to.be.true;
221
+ if (isErrorObject(result)) {
222
+ expect(result.error).to.be.true;
223
+ expect(result.message).to.include(`Error executing tool '${toolId}': ${errorMessage}`);
224
+ }
225
+ });
226
+
227
+ it('should handle asynchronous tool handler errors gracefully', async () => {
228
+ const requestId = 'test-request-id';
229
+ const toolId = 'async-error-tool';
230
+ const errorMessage = 'Async tool execution failed';
231
+
232
+ const mockTool: ToolRequest = {
233
+ id: toolId,
234
+ name: 'Async Error Tool',
235
+ description: 'A tool that returns a rejected promise',
236
+ parameters: {
237
+ type: 'object',
238
+ properties: {}
239
+ },
240
+ handler: () => Promise.reject(new Error(errorMessage))
241
+ };
242
+
243
+ const mockRequest: LanguageModelRequest = {
244
+ messages: [],
245
+ tools: [mockTool]
246
+ };
247
+
248
+ registry.setRequest(requestId, mockRequest);
249
+
250
+ const result = await registry.toolCall(requestId, toolId, '{}');
251
+
252
+ expect(result).to.be.an('object');
253
+ expect(isErrorObject(result)).to.be.true;
254
+ if (isErrorObject(result)) {
255
+ expect(result.error).to.be.true;
256
+ expect(result.message).to.include(`Error executing tool '${toolId}': ${errorMessage}`);
257
+ }
258
+ });
259
+
260
+ it('should handle tool handler with no tools array', async () => {
261
+ const requestId = 'test-request-id';
262
+ const mockRequest: LanguageModelRequest = {
263
+ messages: []
264
+ // No tools property
265
+ };
266
+
267
+ registry.setRequest(requestId, mockRequest);
268
+
269
+ const result = await registry.toolCall(requestId, 'any-tool', '{}');
270
+
271
+ expect(result).to.be.an('object');
272
+ expect(isErrorObject(result)).to.be.true;
273
+ if (isErrorObject(result)) {
274
+ expect(result.error).to.be.true;
275
+ expect(result.message).to.include('Tool \'any-tool\' not found in the available tools for this request');
276
+ }
277
+ });
278
+
279
+ it('should handle tool handler with empty tools array', async () => {
280
+ const requestId = 'test-request-id';
281
+ const mockRequest: LanguageModelRequest = {
282
+ messages: [],
283
+ tools: []
284
+ };
285
+
286
+ registry.setRequest(requestId, mockRequest);
287
+
288
+ const result = await registry.toolCall(requestId, 'any-tool', '{}');
289
+
290
+ expect(result).to.be.an('object');
291
+ expect(isErrorObject(result)).to.be.true;
292
+ if (isErrorObject(result)) {
293
+ expect(result.error).to.be.true;
294
+ expect(result.message).to.include('Tool \'any-tool\' not found in the available tools for this request');
295
+ }
296
+ });
297
+ });
298
+ });
@@ -1,5 +1,5 @@
1
1
  // *****************************************************************************
2
- // Copyright (C) 2024 EclipseSource GmbH.
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
3
  //
4
4
  // This program and the accompanying materials are made available under the
5
5
  // terms of the Eclipse Public License v. 2.0 which is available at
@@ -43,6 +43,7 @@ import {
43
43
  LanguageModelResponse,
44
44
  LanguageModelSelector,
45
45
  LanguageModelStreamResponsePart,
46
+ ToolCallResult,
46
47
  } from '../common';
47
48
 
48
49
  @injectable()
@@ -58,7 +59,7 @@ export class LanguageModelDelegateClientImpl
58
59
  this.receiver.send(id, token);
59
60
  }
60
61
 
61
- toolCall(requestId: string, toolId: string, args_string: string): Promise<unknown> {
62
+ toolCall(requestId: string, toolId: string, args_string: string): Promise<ToolCallResult> {
62
63
  return this.receiver.toolCall(requestId, toolId, args_string);
63
64
  }
64
65
 
@@ -236,8 +237,8 @@ export class FrontendLanguageModelRegistryImpl
236
237
  };
237
238
  }
238
239
 
239
- private streams = new Map<string, StreamState>();
240
- private requests = new Map<string, LanguageModelRequest>();
240
+ protected streams = new Map<string, StreamState>();
241
+ protected requests = new Map<string, LanguageModelRequest>();
241
242
 
242
243
  async *getIterable(
243
244
  state: StreamState
@@ -281,16 +282,21 @@ export class FrontendLanguageModelRegistryImpl
281
282
  }
282
283
 
283
284
  // called by backend once tool is invoked
284
- toolCall(id: string, toolId: string, arg_string: string): Promise<unknown> {
285
+ async toolCall(id: string, toolId: string, arg_string: string): Promise<ToolCallResult> {
285
286
  if (!this.requests.has(id)) {
286
- throw new Error('Somehow we got a callback for a non existing request!');
287
+ return { error: true, message: `No request found for ID '${id}'. The request may have been cancelled or completed.` };
287
288
  }
288
289
  const request = this.requests.get(id)!;
289
290
  const tool = request.tools?.find(t => t.id === toolId);
290
291
  if (tool) {
291
- return tool.handler(arg_string);
292
+ try {
293
+ return await tool.handler(arg_string);
294
+ } catch (error) {
295
+ const errorMessage = error instanceof Error ? error.message : String(error);
296
+ return { error: true, message: `Error executing tool '${toolId}': ${errorMessage}` };
297
+ };
292
298
  }
293
- throw new Error(`Could not find a tool for ${toolId}!`);
299
+ return { error: true, message: `Tool '${toolId}' not found in the available tools for this request.` };
294
300
  }
295
301
 
296
302
  // called by backend via the "delegate client" with the error to use for rejection
@@ -37,6 +37,13 @@ export interface AIVariableDropResult {
37
37
  text?: string
38
38
  };
39
39
 
40
+ export type AIVariablePasteHandler = (event: ClipboardEvent, context: AIVariableContext) => Promise<AIVariablePasteResult | undefined>;
41
+
42
+ export interface AIVariablePasteResult {
43
+ variables: AIVariableResolutionRequest[],
44
+ text?: string
45
+ };
46
+
40
47
  export interface AIVariableCompletionContext {
41
48
  /** Portion of user input to be used for filtering completion candidates. */
42
49
  userInput: string;
@@ -79,6 +86,10 @@ export interface FrontendVariableService extends AIVariableService {
79
86
  unregisterDropHandler(handler: AIVariableDropHandler): void;
80
87
  getDropResult(event: DragEvent, context: AIVariableContext): Promise<AIVariableDropResult>;
81
88
 
89
+ registerPasteHandler(handler: AIVariablePasteHandler): Disposable;
90
+ unregisterPasteHandler(handler: AIVariablePasteHandler): void;
91
+ getPasteResult(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult>;
92
+
82
93
  registerOpener(variable: AIVariable, opener: AIVariableOpener): Disposable;
83
94
  unregisterOpener(variable: AIVariable, opener: AIVariableOpener): void;
84
95
  getOpener(name: string, arg: string | undefined, context: AIVariableContext): Promise<AIVariableOpener | undefined>;
@@ -92,6 +103,7 @@ export interface FrontendVariableContribution {
92
103
  @injectable()
93
104
  export class DefaultFrontendVariableService extends DefaultAIVariableService implements FrontendApplicationContribution, FrontendVariableService {
94
105
  protected dropHandlers = new Set<AIVariableDropHandler>();
106
+ protected pasteHandlers = new Set<AIVariablePasteHandler>();
95
107
 
96
108
  @inject(MessageService) protected readonly messageService: MessageService;
97
109
  @inject(AIVariableResourceResolver) protected readonly aiResourceResolver: AIVariableResourceResolver;
@@ -125,6 +137,30 @@ export class DefaultFrontendVariableService extends DefaultAIVariableService imp
125
137
  return { variables, text };
126
138
  }
127
139
 
140
+ registerPasteHandler(handler: AIVariablePasteHandler): Disposable {
141
+ this.pasteHandlers.add(handler);
142
+ return Disposable.create(() => this.unregisterPasteHandler(handler));
143
+ }
144
+
145
+ unregisterPasteHandler(handler: AIVariablePasteHandler): void {
146
+ this.pasteHandlers.delete(handler);
147
+ }
148
+
149
+ async getPasteResult(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult> {
150
+ let text: string | undefined = undefined;
151
+ const variables: AIVariableResolutionRequest[] = [];
152
+ for (const handler of this.pasteHandlers) {
153
+ const result = await handler(event, context);
154
+ if (result) {
155
+ variables.push(...result.variables);
156
+ if (text === undefined) {
157
+ text = result.text;
158
+ }
159
+ }
160
+ }
161
+ return { variables, text };
162
+ }
163
+
128
164
  registerOpener(variable: AIVariable, opener: AIVariableOpener): Disposable {
129
165
  const key = this.getKey(variable.name);
130
166
  if (!this.variables.get(key)) {
@@ -14,7 +14,11 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
+ export * from './agent-completion-notification-service';
18
+ export * from './os-notification-service';
19
+ export * from './window-blink-service';
17
20
  export * from './ai-activation-service';
21
+ export * from './ai-command-handler-factory';
18
22
  export * from './ai-core-frontend-application-contribution';
19
23
  export * from './ai-core-frontend-module';
20
24
  export * from './ai-core-preferences';
@@ -24,6 +28,7 @@ export * from './frontend-language-model-registry';
24
28
  export * from './frontend-variable-service';
25
29
  export * from './prompttemplate-contribution';
26
30
  export * from './theia-variable-contribution';
31
+ export * from './open-editors-variable-contribution';
27
32
  export * from './frontend-variable-service';
28
33
  export * from './ai-core-command-contribution';
29
34
  export * from '../common/language-model-service';
@@ -0,0 +1,88 @@
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 { MaybePromise, nls } from '@theia/core';
18
+ import { injectable, inject } from '@theia/core/shared/inversify';
19
+ import { EditorManager } from '@theia/editor/lib/browser';
20
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
21
+ import URI from '@theia/core/lib/common/uri';
22
+ import { AIVariable, ResolvedAIVariable, AIVariableContribution, AIVariableResolver, AIVariableService, AIVariableResolutionRequest, AIVariableContext } from '../common';
23
+
24
+ export const OPEN_EDITORS_VARIABLE: AIVariable = {
25
+ id: 'openEditors',
26
+ description: nls.localize('theia/ai/core/openEditorsVariable/description', 'A comma-separated list of all currently open files, relative to the workspace root.'),
27
+ name: 'openEditors',
28
+ };
29
+
30
+ export const OPEN_EDITORS_SHORT_VARIABLE: AIVariable = {
31
+ id: 'openEditorsShort',
32
+ description: nls.localize('theia/ai/core/openEditorsShortVariable/description', 'Short reference to all currently open files (relative paths, comma-separated)'),
33
+ name: '_ff',
34
+ };
35
+
36
+ @injectable()
37
+ export class OpenEditorsVariableContribution implements AIVariableContribution, AIVariableResolver {
38
+
39
+ @inject(EditorManager)
40
+ protected readonly editorManager: EditorManager;
41
+
42
+ @inject(WorkspaceService)
43
+ protected readonly workspaceService: WorkspaceService;
44
+
45
+ registerVariables(service: AIVariableService): void {
46
+ service.registerResolver(OPEN_EDITORS_VARIABLE, this);
47
+ service.registerResolver(OPEN_EDITORS_SHORT_VARIABLE, this);
48
+ }
49
+
50
+ canResolve(request: AIVariableResolutionRequest, _context: AIVariableContext): MaybePromise<number> {
51
+ return (request.variable.name === OPEN_EDITORS_VARIABLE.name || request.variable.name === OPEN_EDITORS_SHORT_VARIABLE.name) ? 50 : 0;
52
+ }
53
+
54
+ async resolve(request: AIVariableResolutionRequest, _context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
55
+ if (request.variable.name !== OPEN_EDITORS_VARIABLE.name && request.variable.name !== OPEN_EDITORS_SHORT_VARIABLE.name) {
56
+ return undefined;
57
+ }
58
+
59
+ const openFiles = this.getAllOpenFilesRelative();
60
+ return {
61
+ variable: request.variable,
62
+ value: openFiles
63
+ };
64
+ }
65
+
66
+ protected getAllOpenFilesRelative(): string {
67
+ const openFiles: string[] = [];
68
+
69
+ // Get all open editors from the editor manager
70
+ for (const editor of this.editorManager.all) {
71
+ const uri = editor.getResourceUri();
72
+ if (uri) {
73
+ const relativePath = this.getWorkspaceRelativePath(uri);
74
+ if (relativePath) {
75
+ openFiles.push(`'${relativePath}'`);
76
+ }
77
+ }
78
+ }
79
+
80
+ return openFiles.join(', ');
81
+ }
82
+
83
+ protected getWorkspaceRelativePath(uri: URI): string | undefined {
84
+ const workspaceRootUri = this.workspaceService.getWorkspaceRootUri(uri);
85
+ const path = workspaceRootUri && workspaceRootUri.path.relative(uri.path);
86
+ return path && path.toString();
87
+ }
88
+ }