@theia/ai-chat 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 (94) hide show
  1. package/lib/browser/agent-delegation-tool.d.ts +25 -0
  2. package/lib/browser/agent-delegation-tool.d.ts.map +1 -0
  3. package/lib/browser/agent-delegation-tool.js +171 -0
  4. package/lib/browser/agent-delegation-tool.js.map +1 -0
  5. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-frontend-module.js +8 -0
  7. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  8. package/lib/browser/change-set-file-element.d.ts +47 -8
  9. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  10. package/lib/browser/change-set-file-element.js +207 -31
  11. package/lib/browser/change-set-file-element.js.map +1 -1
  12. package/lib/browser/chat-tool-preferences.d.ts +1 -1
  13. package/lib/browser/chat-tool-preferences.d.ts.map +1 -1
  14. package/lib/browser/chat-tool-preferences.js +4 -4
  15. package/lib/browser/chat-tool-preferences.js.map +1 -1
  16. package/lib/browser/chat-tool-request-service.js +1 -1
  17. package/lib/browser/chat-tool-request-service.js.map +1 -1
  18. package/lib/browser/delegation-response-content.d.ts +20 -0
  19. package/lib/browser/delegation-response-content.d.ts.map +1 -0
  20. package/lib/browser/delegation-response-content.js +51 -0
  21. package/lib/browser/delegation-response-content.js.map +1 -0
  22. package/lib/browser/file-chat-variable-contribution.d.ts +15 -1
  23. package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
  24. package/lib/browser/file-chat-variable-contribution.js +111 -5
  25. package/lib/browser/file-chat-variable-contribution.js.map +1 -1
  26. package/lib/browser/frontend-chat-service.d.ts +1 -1
  27. package/lib/browser/frontend-chat-service.d.ts.map +1 -1
  28. package/lib/browser/frontend-chat-service.js +2 -13
  29. package/lib/browser/frontend-chat-service.js.map +1 -1
  30. package/lib/browser/image-context-variable-contribution.d.ts +27 -0
  31. package/lib/browser/image-context-variable-contribution.d.ts.map +1 -0
  32. package/lib/browser/image-context-variable-contribution.js +149 -0
  33. package/lib/browser/image-context-variable-contribution.js.map +1 -0
  34. package/lib/browser/task-context-service.d.ts +9 -3
  35. package/lib/browser/task-context-service.d.ts.map +1 -1
  36. package/lib/browser/task-context-service.js +111 -9
  37. package/lib/browser/task-context-service.js.map +1 -1
  38. package/lib/browser/task-context-storage-service.d.ts +1 -0
  39. package/lib/browser/task-context-storage-service.d.ts.map +1 -1
  40. package/lib/browser/task-context-storage-service.js +4 -1
  41. package/lib/browser/task-context-storage-service.js.map +1 -1
  42. package/lib/common/change-set.js +1 -1
  43. package/lib/common/change-set.js.map +1 -1
  44. package/lib/common/chat-agent-service.d.ts +1 -0
  45. package/lib/common/chat-agent-service.d.ts.map +1 -1
  46. package/lib/common/chat-agent-service.js +2 -1
  47. package/lib/common/chat-agent-service.js.map +1 -1
  48. package/lib/common/chat-agents.d.ts +2 -2
  49. package/lib/common/chat-agents.d.ts.map +1 -1
  50. package/lib/common/chat-agents.js +21 -5
  51. package/lib/common/chat-agents.js.map +1 -1
  52. package/lib/common/chat-model.d.ts +7 -7
  53. package/lib/common/chat-model.d.ts.map +1 -1
  54. package/lib/common/chat-model.js +1 -1
  55. package/lib/common/chat-model.js.map +1 -1
  56. package/lib/common/chat-request-parser.d.ts.map +1 -1
  57. package/lib/common/chat-request-parser.js +3 -6
  58. package/lib/common/chat-request-parser.js.map +1 -1
  59. package/lib/common/chat-service.d.ts +14 -2
  60. package/lib/common/chat-service.d.ts.map +1 -1
  61. package/lib/common/chat-service.js +36 -10
  62. package/lib/common/chat-service.js.map +1 -1
  63. package/lib/common/chat-session-naming-service.js +2 -2
  64. package/lib/common/chat-session-naming-service.js.map +1 -1
  65. package/lib/common/chat-session-summary-agent-prompt.js +3 -3
  66. package/lib/common/chat-session-summary-agent-prompt.js.map +1 -1
  67. package/lib/common/chat-tool-request-service.d.ts +2 -2
  68. package/lib/common/chat-tool-request-service.d.ts.map +1 -1
  69. package/lib/common/image-context-variable.d.ts +29 -0
  70. package/lib/common/image-context-variable.d.ts.map +1 -0
  71. package/lib/common/image-context-variable.js +99 -0
  72. package/lib/common/image-context-variable.js.map +1 -0
  73. package/package.json +11 -10
  74. package/src/browser/agent-delegation-tool.ts +207 -0
  75. package/src/browser/ai-chat-frontend-module.ts +20 -2
  76. package/src/browser/change-set-file-element.ts +236 -32
  77. package/src/browser/chat-tool-preferences.ts +4 -4
  78. package/src/browser/chat-tool-request-service.ts +1 -1
  79. package/src/browser/delegation-response-content.ts +55 -0
  80. package/src/browser/file-chat-variable-contribution.ts +120 -6
  81. package/src/browser/frontend-chat-service.ts +2 -11
  82. package/src/browser/image-context-variable-contribution.ts +153 -0
  83. package/src/browser/task-context-service.ts +115 -9
  84. package/src/browser/task-context-storage-service.ts +5 -1
  85. package/src/common/change-set.ts +1 -1
  86. package/src/common/chat-agent-service.ts +1 -0
  87. package/src/common/chat-agents.ts +26 -9
  88. package/src/common/chat-model.ts +16 -7
  89. package/src/common/chat-request-parser.ts +3 -12
  90. package/src/common/chat-service.ts +40 -10
  91. package/src/common/chat-session-naming-service.ts +2 -2
  92. package/src/common/chat-session-summary-agent-prompt.ts +3 -3
  93. package/src/common/chat-tool-request-service.ts +2 -2
  94. package/src/common/image-context-variable.ts +116 -0
@@ -14,16 +14,29 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { DisposableCollection, Emitter, URI } from '@theia/core';
18
- import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19
- import { Replacement } from '@theia/core/lib/common/content-replacer';
20
17
  import { ConfigurableInMemoryResources, ConfigurableMutableReferenceResource } from '@theia/ai-core';
18
+ import { CancellationToken, DisposableCollection, Emitter, URI } from '@theia/core';
19
+ import { ConfirmDialog } from '@theia/core/lib/browser';
20
+ import { Replacement } from '@theia/core/lib/common/content-replacer';
21
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
22
+ import { EditorPreferences } from '@theia/editor/lib/browser';
23
+ import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
24
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
25
+ import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
26
+ import { TrimTrailingWhitespaceCommand } from '@theia/monaco-editor-core/esm/vs/editor/common/commands/trimTrailingWhitespaceCommand';
27
+ import { Selection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection';
28
+ import { CommandExecutor } from '@theia/monaco-editor-core/esm/vs/editor/common/cursor/cursor';
29
+ import { formatDocumentWithSelectedProvider, FormattingMode } from '@theia/monaco-editor-core/esm/vs/editor/contrib/format/browser/format';
30
+ import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
31
+ import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
32
+ import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
33
+ import { insertFinalNewline } from '@theia/monaco/lib/browser/monaco-utilities';
34
+ import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
21
35
  import { ChangeSetElement } from '../common';
22
36
  import { createChangeSetFileUri } from './change-set-file-resource';
23
37
  import { ChangeSetFileService } from './change-set-file-service';
24
- import { FileService } from '@theia/filesystem/lib/browser/file-service';
25
- import { ConfirmDialog } from '@theia/core/lib/browser';
26
- import { ChangeSetDecoratorService } from './change-set-decorator-service';
38
+ import { Deferred } from '@theia/core/lib/common/promise-util';
39
+ import { MonacoCodeActionService } from '@theia/monaco/lib/browser';
27
40
 
28
41
  export const ChangeSetFileElementFactory = Symbol('ChangeSetFileElementFactory');
29
42
  export type ChangeSetFileElementFactory = (elementProps: ChangeSetElementArgs) => ChangeSetFileElement;
@@ -62,35 +75,70 @@ export class ChangeSetFileElement implements ChangeSetElement {
62
75
  @inject(ChangeSetFileService)
63
76
  protected readonly changeSetFileService: ChangeSetFileService;
64
77
 
65
- @inject(ChangeSetDecoratorService)
66
- protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
67
-
68
78
  @inject(FileService)
69
79
  protected readonly fileService: FileService;
70
80
 
71
81
  @inject(ConfigurableInMemoryResources)
72
82
  protected readonly inMemoryResources: ConfigurableInMemoryResources;
73
83
 
74
- @inject(ChangeSetFileElementFactory) protected readonly factory: ChangeSetFileElementFactory;
84
+ @inject(MonacoTextModelService)
85
+ protected readonly monacoTextModelService: MonacoTextModelService;
86
+
87
+ @inject(EditorPreferences)
88
+ protected readonly editorPreferences: EditorPreferences;
89
+
90
+ @inject(FileSystemPreferences)
91
+ protected readonly fileSystemPreferences: FileSystemPreferences;
92
+
93
+ @inject(MonacoCodeActionService)
94
+ protected readonly codeActionService: MonacoCodeActionService;
75
95
 
76
96
  protected readonly toDispose = new DisposableCollection();
77
97
  protected _state: ChangeSetElementState;
78
98
 
79
- protected originalContent: string | undefined;
99
+ private _originalContent: string | undefined;
100
+ protected _initialized = false;
101
+ protected _initializationPromise: Promise<void> | undefined;
102
+ protected _targetStateWithCodeActions: string | undefined;
103
+ protected codeActionDeferred?: Deferred<string>;
80
104
 
81
105
  protected readonly onDidChangeEmitter = new Emitter<void>();
82
106
  readonly onDidChange = this.onDidChangeEmitter.event;
83
- protected _readOnlyResource: ConfigurableMutableReferenceResource;
84
- protected _changeResource: ConfigurableMutableReferenceResource;
107
+ protected _readOnlyResource?: ConfigurableMutableReferenceResource;
108
+ protected _changeResource?: ConfigurableMutableReferenceResource;
85
109
 
86
110
  @postConstruct()
87
111
  init(): void {
112
+ this._initializationPromise = this.initializeAsync();
88
113
  this.toDispose.push(this.onDidChangeEmitter);
89
114
  }
90
115
 
116
+ protected async initializeAsync(): Promise<void> {
117
+ await this.obtainOriginalContent();
118
+ this.listenForOriginalFileChanges();
119
+ this._initialized = true;
120
+ }
121
+
122
+ /**
123
+ * Ensures that the element is fully initialized before proceeding.
124
+ * This includes loading the original content from the file system.
125
+ */
126
+ async ensureInitialized(): Promise<void> {
127
+ await this._initializationPromise;
128
+ }
129
+
130
+ /**
131
+ * Returns true if the element has been fully initialized.
132
+ */
133
+ get isInitialized(): boolean {
134
+ return this._initialized;
135
+ }
136
+
91
137
  protected async obtainOriginalContent(): Promise<void> {
92
- this.originalContent = await this.changeSetFileService.read(this.uri);
93
- this.readOnlyResource.update({ contents: this.originalContent ?? '' });
138
+ this._originalContent = await this.changeSetFileService.read(this.uri);
139
+ if (this._readOnlyResource) {
140
+ this.readOnlyResource.update({ contents: this._originalContent ?? '' });
141
+ }
94
142
  }
95
143
 
96
144
  protected getInMemoryUri(uri: URI): ConfigurableMutableReferenceResource {
@@ -100,10 +148,14 @@ export class ChangeSetFileElement implements ChangeSetElement {
100
148
  protected listenForOriginalFileChanges(): void {
101
149
  this.toDispose.push(this.fileService.onDidFilesChange(async event => {
102
150
  if (!event.contains(this.uri)) { return; }
151
+ if (!this._initialized && this._initializationPromise) {
152
+ // make sure we are initialized
153
+ await this._initializationPromise;
154
+ }
103
155
  // If we are applied, the tricky thing becomes the question what to revert to; otherwise, what to apply.
104
156
  const newContent = await this.changeSetFileService.read(this.uri).catch(() => '');
105
157
  this.readOnlyResource.update({ contents: newContent });
106
- if (newContent === this.originalContent) {
158
+ if (newContent === this._originalContent) {
107
159
  this.state = 'pending';
108
160
  } else if (newContent === this.targetState) {
109
161
  this.state = 'applied';
@@ -120,10 +172,19 @@ export class ChangeSetFileElement implements ChangeSetElement {
120
172
  protected get readOnlyResource(): ConfigurableMutableReferenceResource {
121
173
  if (!this._readOnlyResource) {
122
174
  this._readOnlyResource = this.getInMemoryUri(ChangeSetFileElement.toReadOnlyUri(this.uri, this.elementProps.chatSessionId));
123
- this._readOnlyResource.update({ autosaveable: false, readOnly: true });
175
+ this._readOnlyResource.update({
176
+ autosaveable: false,
177
+ readOnly: true,
178
+ contents: this._originalContent ?? ''
179
+ });
124
180
  this.toDispose.push(this._readOnlyResource);
125
- this.obtainOriginalContent();
126
- this.listenForOriginalFileChanges();
181
+
182
+ // If not yet initialized, update the resource once initialization completes
183
+ if (!this._initialized) {
184
+ this._initializationPromise?.then(() => {
185
+ this._readOnlyResource?.update({ contents: this._originalContent ?? '' });
186
+ });
187
+ }
127
188
  }
128
189
  return this._readOnlyResource;
129
190
  }
@@ -135,7 +196,8 @@ export class ChangeSetFileElement implements ChangeSetElement {
135
196
  protected get changeResource(): ConfigurableMutableReferenceResource {
136
197
  if (!this._changeResource) {
137
198
  this._changeResource = this.getInMemoryUri(createChangeSetFileUri(this.elementProps.chatSessionId, this.uri));
138
- this._changeResource.update({ autosaveable: false });
199
+ this._changeResource.update({ autosaveable: false, contents: this.targetState });
200
+ this.applyCodeActionsToTargetState();
139
201
  this.toDispose.push(this._changeResource);
140
202
  }
141
203
  return this._changeResource;
@@ -180,15 +242,37 @@ export class ChangeSetFileElement implements ChangeSetElement {
180
242
  return this.elementProps.data;
181
243
  };
182
244
 
245
+ get originalContent(): string | undefined {
246
+ if (!this._initialized && this._initializationPromise) {
247
+ console.warn('Accessing originalContent before initialization is complete. Consider using async methods.');
248
+ }
249
+ return this._originalContent;
250
+ }
251
+
252
+ /**
253
+ * Gets the original content of the file asynchronously.
254
+ * Ensures initialization is complete before returning the content.
255
+ */
256
+ async getOriginalContent(): Promise<string | undefined> {
257
+ await this.ensureInitialized();
258
+ return this._originalContent;
259
+ }
260
+
183
261
  get targetState(): string {
262
+ return this._targetStateWithCodeActions ?? this.elementProps.targetState ?? '';
263
+ }
264
+
265
+ get originalTargetState(): string {
184
266
  return this.elementProps.targetState ?? '';
185
267
  }
186
268
 
187
269
  async open(): Promise<void> {
270
+ await this.ensureInitialized();
188
271
  this.changeSetFileService.open(this);
189
272
  }
190
273
 
191
274
  async openChange(): Promise<void> {
275
+ await this.ensureInitialized();
192
276
  this.changeSetFileService.openDiff(
193
277
  this.readOnlyUri,
194
278
  this.changedUri
@@ -196,15 +280,18 @@ export class ChangeSetFileElement implements ChangeSetElement {
196
280
  }
197
281
 
198
282
  async apply(contents?: string): Promise<void> {
283
+ await this.ensureInitialized();
199
284
  if (!await this.confirm('Apply')) { return; }
200
- if (!(await this.changeSetFileService.trySave(this.changedUri))) {
201
- if (this.type === 'delete') {
202
- await this.changeSetFileService.delete(this.uri);
203
- this.state = 'applied';
204
- } else {
205
- await this.writeChanges(contents);
206
- }
285
+
286
+ if (this.type === 'delete') {
287
+ await this.changeSetFileService.delete(this.uri);
288
+ this.state = 'applied';
289
+ this.changeSetFileService.closeDiff(this.readOnlyUri);
290
+ return;
207
291
  }
292
+
293
+ // Load Monaco model for the base file URI and apply changes
294
+ await this.applyChangesWithMonaco(contents);
208
295
  this.changeSetFileService.closeDiff(this.readOnlyUri);
209
296
  }
210
297
 
@@ -213,28 +300,145 @@ export class ChangeSetFileElement implements ChangeSetElement {
213
300
  this.state = 'applied';
214
301
  }
215
302
 
303
+ /**
304
+ * Applies changes using Monaco utilities, including loading the model for the base file URI,
305
+ * setting the value to the intended state, and running code actions on save.
306
+ */
307
+ protected async applyChangesWithMonaco(contents?: string): Promise<void> {
308
+ let modelReference: IReference<MonacoEditorModel> | undefined;
309
+
310
+ try {
311
+ modelReference = await this.monacoTextModelService.createModelReference(this.uri);
312
+ const model = modelReference.object;
313
+ const targetContent = contents ?? this.targetState;
314
+ model.textEditorModel.setValue(targetContent);
315
+
316
+ const languageId = model.languageId;
317
+ const uriStr = this.uri.toString();
318
+
319
+ await this.codeActionService.applyOnSaveCodeActions(model.textEditorModel, languageId, uriStr, CancellationToken.None);
320
+ await this.applyFormatting(model, languageId, uriStr);
321
+
322
+ await model.save();
323
+ this.state = 'applied';
324
+
325
+ } catch (error) {
326
+ console.error('Failed to apply changes with Monaco:', error);
327
+ await this.writeChanges(contents);
328
+ } finally {
329
+ modelReference?.dispose();
330
+ }
331
+ }
332
+
333
+ protected applyCodeActionsToTargetState(): Promise<string> {
334
+ if (!this.codeActionDeferred) {
335
+ this.codeActionDeferred = new Deferred();
336
+ this.codeActionDeferred.resolve(this.doApplyCodeActionsToTargetState());
337
+ }
338
+ return this.codeActionDeferred.promise;
339
+ }
340
+
341
+ protected async doApplyCodeActionsToTargetState(): Promise<string> {
342
+ const targetState = this.originalTargetState;
343
+ if (!targetState) {
344
+ this._targetStateWithCodeActions = '';
345
+ return this._targetStateWithCodeActions;
346
+ }
347
+
348
+ let tempResource: ConfigurableMutableReferenceResource | undefined;
349
+ let tempModel: IReference<MonacoEditorModel> | undefined;
350
+ try {
351
+ // Create a temporary model to apply code actions
352
+ const tempUri = new URI(`untitled://changeset/${Date.now()}${this.uri.path.ext}`);
353
+ tempResource = this.inMemoryResources.add(tempUri, { contents: this.targetState });
354
+ tempModel = await this.monacoTextModelService.createModelReference(tempUri);
355
+ tempModel.object.suppressOpenEditorWhenDirty = true;
356
+ tempModel.object.textEditorModel.setValue(this.targetState);
357
+
358
+ const languageId = tempModel.object.languageId;
359
+ const uriStr = this.uri.toString();
360
+
361
+ await this.codeActionService.applyOnSaveCodeActions(tempModel.object.textEditorModel, languageId, uriStr, CancellationToken.None);
362
+
363
+ // Apply formatting and other editor preferences
364
+ await this.applyFormatting(tempModel.object, languageId, uriStr);
365
+
366
+ this._targetStateWithCodeActions = tempModel.object.textEditorModel.getValue();
367
+ if (this._changeResource?.contents === this.elementProps.targetState) {
368
+ this._changeResource?.update({ contents: this.targetState });
369
+ }
370
+ } catch (error) {
371
+ console.warn('Failed to apply code actions to target state:', error);
372
+ this._targetStateWithCodeActions = targetState;
373
+ } finally {
374
+ tempModel?.dispose();
375
+ tempResource?.dispose();
376
+ }
377
+
378
+ return this.targetState;
379
+ }
380
+
381
+ /**
382
+ * Applies formatting preferences like format on save, trim trailing whitespace, and insert final newline.
383
+ */
384
+ protected async applyFormatting(model: MonacoEditorModel, languageId: string, uriStr: string): Promise<void> {
385
+ try {
386
+ const formatOnSave = this.editorPreferences.get({ preferenceName: 'editor.formatOnSave', overrideIdentifier: languageId }, undefined, uriStr);
387
+ if (formatOnSave) {
388
+ const instantiation = StandaloneServices.get(IInstantiationService);
389
+ await instantiation.invokeFunction(
390
+ formatDocumentWithSelectedProvider,
391
+ model.textEditorModel,
392
+ FormattingMode.Explicit,
393
+ { report(): void { } },
394
+ CancellationToken.None, true
395
+ );
396
+ }
397
+
398
+ const trimTrailingWhitespace = this.fileSystemPreferences.get({ preferenceName: 'files.trimTrailingWhitespace', overrideIdentifier: languageId }, undefined, uriStr);
399
+ if (trimTrailingWhitespace) {
400
+ const ttws = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), [], false);
401
+ CommandExecutor.executeCommands(model.textEditorModel, [], [ttws]);
402
+ }
403
+
404
+ const shouldInsertFinalNewline = this.fileSystemPreferences.get({ preferenceName: 'files.insertFinalNewline', overrideIdentifier: languageId }, undefined, uriStr);
405
+ if (shouldInsertFinalNewline) {
406
+ insertFinalNewline(model);
407
+ }
408
+ } catch (error) {
409
+ console.warn('Failed to apply formatting:', error);
410
+ }
411
+ }
412
+
216
413
  onShow(): void {
217
- this.changeResource.update({ contents: this.targetState, onSave: content => this.writeChanges(content) });
414
+ this.changeResource.update({
415
+ contents: this.targetState,
416
+ onSave: async content => {
417
+ // Use Monaco utilities when saving from the change resource
418
+ await this.applyChangesWithMonaco(content);
419
+ }
420
+ });
218
421
  }
219
422
 
220
423
  async revert(): Promise<void> {
424
+ await this.ensureInitialized();
221
425
  if (!await this.confirm('Revert')) { return; }
222
426
  this.state = 'pending';
223
427
  if (this.type === 'add') {
224
428
  await this.changeSetFileService.delete(this.uri);
225
- } else if (this.originalContent) {
226
- await this.changeSetFileService.write(this.uri, this.originalContent);
429
+ } else if (this._originalContent) {
430
+ await this.changeSetFileService.write(this.uri, this._originalContent);
227
431
  }
228
432
  }
229
433
 
230
434
  async confirm(verb: string): Promise<boolean> {
231
435
  if (this._state !== 'stale') { return true; }
232
436
  await this.openChange();
233
- const thing = await new ConfirmDialog({
437
+ const answer = await new ConfirmDialog({
234
438
  title: `${verb} suggestion.`,
235
439
  msg: `The file ${this.uri.path.toString()} has changed since this suggestion was created. Are you certain you wish to ${verb.toLowerCase()} the change?`
236
440
  }).open(true);
237
- return !!thing;
441
+ return !!answer;
238
442
  }
239
443
 
240
444
  dispose(): void {
@@ -29,7 +29,7 @@ import {
29
29
  * Enum for tool confirmation modes
30
30
  */
31
31
  export enum ToolConfirmationMode {
32
- YOLO = 'yolo',
32
+ ALWAYS_ALLOW = 'always_allow',
33
33
  CONFIRM = 'confirm',
34
34
  DISABLED = 'disabled'
35
35
  }
@@ -43,7 +43,7 @@ export const chatToolPreferences: PreferenceSchema = {
43
43
  type: 'object',
44
44
  additionalProperties: {
45
45
  type: 'string',
46
- enum: [ToolConfirmationMode.YOLO, ToolConfirmationMode.CONFIRM, ToolConfirmationMode.DISABLED],
46
+ enum: [ToolConfirmationMode.ALWAYS_ALLOW, ToolConfirmationMode.CONFIRM, ToolConfirmationMode.DISABLED],
47
47
  enumDescriptions: [
48
48
  nls.localize('theia/ai/chat/toolConfirmation/yolo/description', 'Execute tools automatically without confirmation'),
49
49
  nls.localize('theia/ai/chat/toolConfirmation/confirm/description', 'Ask for confirmation before executing tools'),
@@ -110,7 +110,7 @@ export class ToolConfirmationManager {
110
110
  if (toolConfirmation['*']) {
111
111
  return toolConfirmation['*'];
112
112
  }
113
- return ToolConfirmationMode.YOLO; // Default to YOLO
113
+ return ToolConfirmationMode.ALWAYS_ALLOW; // Default to Always Allow
114
114
  }
115
115
 
116
116
  /**
@@ -121,7 +121,7 @@ export class ToolConfirmationManager {
121
121
  // Determine the global default (star entry), or fallback to schema default
122
122
  let starMode = current['*'];
123
123
  if (starMode === undefined) {
124
- starMode = ToolConfirmationMode.YOLO;
124
+ starMode = ToolConfirmationMode.ALWAYS_ALLOW;
125
125
  }
126
126
  if (mode === starMode) {
127
127
  // Remove the toolId entry if it exists
@@ -42,7 +42,7 @@ export class FrontendChatToolRequestService extends ChatToolRequestService {
42
42
  case ToolConfirmationMode.DISABLED:
43
43
  return { denied: true, message: `Tool ${toolRequest.id} is disabled` };
44
44
 
45
- case ToolConfirmationMode.YOLO:
45
+ case ToolConfirmationMode.ALWAYS_ALLOW:
46
46
  // Execute immediately without confirmation
47
47
  return toolRequest.handler(arg_string, request);
48
48
 
@@ -0,0 +1,55 @@
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
+ import { isObject } from '@theia/core';
17
+ import { ChatRequestInvocation, ChatResponseContent } from '../common';
18
+
19
+ /**
20
+ * Response Content created when an Agent delegates a prompt to another agent.
21
+ * Contains agent id, delegated prompt, and the response.
22
+ */
23
+ export class DelegationResponseContent implements ChatResponseContent {
24
+ kind = 'AgentDelegation';
25
+
26
+ /**
27
+ * @param agentId The id of the agent to whom the task was delegated
28
+ * @param prompt The prompt that was delegated
29
+ * @param response The response from the delegated agent
30
+ */
31
+ constructor(
32
+ public agentId: string,
33
+ public prompt: string,
34
+ public response: ChatRequestInvocation
35
+ ) {
36
+ // Empty
37
+ }
38
+
39
+ asString(): string {
40
+ const json = {
41
+ agentId: this.agentId,
42
+ prompt: this.prompt
43
+ };
44
+ return JSON.stringify(json);
45
+ }
46
+ }
47
+
48
+ export function isDelegationResponseContent(
49
+ value: unknown
50
+ ): value is DelegationResponseContent {
51
+ return (
52
+ isObject<DelegationResponseContent>(value) &&
53
+ value.kind === 'AgentDelegation'
54
+ );
55
+ }
@@ -17,13 +17,14 @@
17
17
  import { AIVariableContext, AIVariableResolutionRequest, PromptText } from '@theia/ai-core';
18
18
  import { AIVariableCompletionContext, AIVariableDropResult, FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
19
19
  import { FILE_VARIABLE } from '@theia/ai-core/lib/browser/file-variable-contribution';
20
- import { CancellationToken, QuickInputService, URI } from '@theia/core';
20
+ import { CancellationToken, ILogger, QuickInputService, URI } from '@theia/core';
21
21
  import { inject, injectable } from '@theia/core/shared/inversify';
22
22
  import * as monaco from '@theia/monaco-editor-core';
23
23
  import { FileQuickPickItem, QuickFileSelectService } from '@theia/file-search/lib/browser/quick-file-select-service';
24
24
  import { WorkspaceService } from '@theia/workspace/lib/browser';
25
25
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
26
26
  import { VARIABLE_ADD_CONTEXT_COMMAND } from './ai-chat-frontend-contribution';
27
+ import { IMAGE_CONTEXT_VARIABLE, ImageContextVariable } from '../common/image-context-variable';
27
28
 
28
29
  @injectable()
29
30
  export class FileChatVariableContribution implements FrontendVariableContribution {
@@ -39,8 +40,12 @@ export class FileChatVariableContribution implements FrontendVariableContributio
39
40
  @inject(QuickFileSelectService)
40
41
  protected readonly quickFileSelectService: QuickFileSelectService;
41
42
 
43
+ @inject(ILogger)
44
+ protected readonly logger: ILogger;
45
+
42
46
  registerVariables(service: FrontendVariableService): void {
43
47
  service.registerArgumentPicker(FILE_VARIABLE, this.triggerArgumentPicker.bind(this));
48
+ service.registerArgumentPicker(IMAGE_CONTEXT_VARIABLE, this.imageArgumentPicker.bind(this));
44
49
  service.registerArgumentCompletionProvider(FILE_VARIABLE, this.provideArgumentCompletionItems.bind(this));
45
50
  service.registerDropHandler(this.handleDrop.bind(this));
46
51
  }
@@ -68,6 +73,57 @@ export class FileChatVariableContribution implements FrontendVariableContributio
68
73
  });
69
74
  }
70
75
 
76
+ protected async imageArgumentPicker(): Promise<string | undefined> {
77
+ const quickPick = this.quickInputService.createQuickPick();
78
+ quickPick.title = 'Select an image file';
79
+
80
+ // Get all files and filter only image files
81
+ const allPicks = await this.quickFileSelectService.getPicks();
82
+ quickPick.items = allPicks.filter(item => {
83
+ if (FileQuickPickItem.is(item)) {
84
+ return this.isImageFile(item.uri.path.toString());
85
+ }
86
+ return false;
87
+ });
88
+
89
+ const updateItems = async (value: string) => {
90
+ const filteredPicks = await this.quickFileSelectService.getPicks(value, CancellationToken.None);
91
+ quickPick.items = filteredPicks.filter(item => {
92
+ if (FileQuickPickItem.is(item)) {
93
+ return this.isImageFile(item.uri.path.toString());
94
+ }
95
+ return false;
96
+ });
97
+ };
98
+
99
+ const onChangeListener = quickPick.onDidChangeValue(updateItems);
100
+ quickPick.show();
101
+
102
+ return new Promise(resolve => {
103
+ quickPick.onDispose(onChangeListener.dispose);
104
+ quickPick.onDidAccept(async () => {
105
+ const selectedItem = quickPick.selectedItems[0];
106
+ if (selectedItem && FileQuickPickItem.is(selectedItem)) {
107
+ quickPick.dispose();
108
+ const filePath = await this.wsService.getWorkspaceRelativePath(selectedItem.uri);
109
+ const fileName = selectedItem.uri.displayName;
110
+ const base64Data = await this.fileToBase64(selectedItem.uri);
111
+ const mimeType = this.getMimeTypeFromExtension(selectedItem.uri.path.toString());
112
+
113
+ // Create the argument string in the required format
114
+ const imageVarArgs: ImageContextVariable = {
115
+ name: fileName,
116
+ wsRelativePath: filePath,
117
+ data: base64Data,
118
+ mimeType: mimeType
119
+ };
120
+
121
+ resolve(ImageContextVariable.createArgString(imageVarArgs));
122
+ }
123
+ });
124
+ });
125
+ }
126
+
71
127
  protected async provideArgumentCompletionItems(
72
128
  model: monaco.editor.ITextModel,
73
129
  position: monaco.Position,
@@ -106,6 +162,50 @@ export class FileChatVariableContribution implements FrontendVariableContributio
106
162
  );
107
163
  }
108
164
 
165
+ /**
166
+ * Checks if a file is an image based on its extension.
167
+ */
168
+ protected isImageFile(filePath: string): boolean {
169
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'];
170
+ const extension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
171
+ return imageExtensions.includes(extension);
172
+ }
173
+
174
+ /**
175
+ * Determines the MIME type based on file extension.
176
+ */
177
+ protected getMimeTypeFromExtension(filePath: string): string {
178
+ const extension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
179
+ const mimeTypes: { [key: string]: string } = {
180
+ '.jpg': 'image/jpeg',
181
+ '.jpeg': 'image/jpeg',
182
+ '.png': 'image/png',
183
+ '.gif': 'image/gif',
184
+ '.bmp': 'image/bmp',
185
+ '.svg': 'image/svg+xml',
186
+ '.webp': 'image/webp'
187
+ };
188
+ return mimeTypes[extension] || 'application/octet-stream';
189
+ }
190
+
191
+ /**
192
+ * Converts a file to base64 data URL.
193
+ */
194
+ protected async fileToBase64(uri: URI): Promise<string> {
195
+ try {
196
+ const fileContent = await this.fileService.readFile(uri);
197
+ const uint8Array = new Uint8Array(fileContent.value.buffer);
198
+ let binary = '';
199
+ for (let i = 0; i < uint8Array.length; i++) {
200
+ binary += String.fromCharCode(uint8Array[i]);
201
+ }
202
+ return btoa(binary);
203
+ } catch (error) {
204
+ this.logger.error('Error reading file content:', error);
205
+ return '';
206
+ }
207
+ }
208
+
109
209
  protected async handleDrop(event: DragEvent, _: AIVariableContext): Promise<AIVariableDropResult | undefined> {
110
210
  const data = event.dataTransfer?.getData('selected-tree-nodes');
111
211
  if (!data) {
@@ -126,11 +226,25 @@ export class FileChatVariableContribution implements FrontendVariableContributio
126
226
  const uri = URI.fromFilePath(filePath);
127
227
  if (await this.fileService.exists(uri)) {
128
228
  const wsRelativePath = await this.wsService.getWorkspaceRelativePath(uri);
129
- variables.push({
130
- variable: FILE_VARIABLE,
131
- arg: wsRelativePath
132
- });
133
- texts.push(`${PromptText.VARIABLE_CHAR}${FILE_VARIABLE.name}${PromptText.VARIABLE_SEPARATOR_CHAR}${wsRelativePath}`);
229
+ const fileName = uri.displayName;
230
+
231
+ if (this.isImageFile(filePath)) {
232
+ const base64Data = await this.fileToBase64(uri);
233
+ const mimeType = this.getMimeTypeFromExtension(filePath);
234
+ variables.push(ImageContextVariable.createRequest({
235
+ [ImageContextVariable.name]: fileName,
236
+ [ImageContextVariable.wsRelativePath]: wsRelativePath,
237
+ [ImageContextVariable.data]: base64Data,
238
+ [ImageContextVariable.mimeType]: mimeType
239
+ }));
240
+ // we do not want to push a text for image variables
241
+ } else {
242
+ variables.push({
243
+ variable: FILE_VARIABLE,
244
+ arg: wsRelativePath
245
+ });
246
+ texts.push(`${PromptText.VARIABLE_CHAR}${FILE_VARIABLE.name}${PromptText.VARIABLE_SEPARATOR_CHAR}${wsRelativePath}`);
247
+ }
134
248
  }
135
249
  }
136
250
 
@@ -32,17 +32,8 @@ export class FrontendChatServiceImpl extends ChatServiceImpl {
32
32
  @inject(ChangeSetFileService)
33
33
  protected readonly changeSetFileService: ChangeSetFileService;
34
34
 
35
- protected override getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined {
36
- let agent = this.initialAgentSelection(parsedRequest);
37
- if (!this.preferenceService.get<boolean>(PIN_CHAT_AGENT_PREF)) {
38
- return agent;
39
- }
40
- if (!session.pinnedAgent && agent && agent.id !== this.defaultChatAgentId?.id) {
41
- session.pinnedAgent = agent;
42
- } else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) {
43
- agent = session.pinnedAgent;
44
- }
45
- return agent;
35
+ protected override isPinChatAgentEnabled(): boolean {
36
+ return this.preferenceService.get<boolean>(PIN_CHAT_AGENT_PREF, true);
46
37
  }
47
38
 
48
39
  protected override initialAgentSelection(parsedRequest: ParsedChatRequest): ChatAgent | undefined {