@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.
- package/lib/browser/agent-delegation-tool.d.ts +25 -0
- package/lib/browser/agent-delegation-tool.d.ts.map +1 -0
- package/lib/browser/agent-delegation-tool.js +171 -0
- package/lib/browser/agent-delegation-tool.js.map +1 -0
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +8 -0
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-file-element.d.ts +47 -8
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +207 -31
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/chat-tool-preferences.d.ts +1 -1
- package/lib/browser/chat-tool-preferences.d.ts.map +1 -1
- package/lib/browser/chat-tool-preferences.js +4 -4
- package/lib/browser/chat-tool-preferences.js.map +1 -1
- package/lib/browser/chat-tool-request-service.js +1 -1
- package/lib/browser/chat-tool-request-service.js.map +1 -1
- package/lib/browser/delegation-response-content.d.ts +20 -0
- package/lib/browser/delegation-response-content.d.ts.map +1 -0
- package/lib/browser/delegation-response-content.js +51 -0
- package/lib/browser/delegation-response-content.js.map +1 -0
- package/lib/browser/file-chat-variable-contribution.d.ts +15 -1
- package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +111 -5
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/frontend-chat-service.d.ts +1 -1
- package/lib/browser/frontend-chat-service.d.ts.map +1 -1
- package/lib/browser/frontend-chat-service.js +2 -13
- package/lib/browser/frontend-chat-service.js.map +1 -1
- package/lib/browser/image-context-variable-contribution.d.ts +27 -0
- package/lib/browser/image-context-variable-contribution.d.ts.map +1 -0
- package/lib/browser/image-context-variable-contribution.js +149 -0
- package/lib/browser/image-context-variable-contribution.js.map +1 -0
- package/lib/browser/task-context-service.d.ts +9 -3
- package/lib/browser/task-context-service.d.ts.map +1 -1
- package/lib/browser/task-context-service.js +111 -9
- package/lib/browser/task-context-service.js.map +1 -1
- package/lib/browser/task-context-storage-service.d.ts +1 -0
- package/lib/browser/task-context-storage-service.d.ts.map +1 -1
- package/lib/browser/task-context-storage-service.js +4 -1
- package/lib/browser/task-context-storage-service.js.map +1 -1
- package/lib/common/change-set.js +1 -1
- package/lib/common/change-set.js.map +1 -1
- package/lib/common/chat-agent-service.d.ts +1 -0
- package/lib/common/chat-agent-service.d.ts.map +1 -1
- package/lib/common/chat-agent-service.js +2 -1
- package/lib/common/chat-agent-service.js.map +1 -1
- package/lib/common/chat-agents.d.ts +2 -2
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +21 -5
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-model.d.ts +7 -7
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +1 -1
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +3 -6
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-service.d.ts +14 -2
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +36 -10
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-naming-service.js +2 -2
- package/lib/common/chat-session-naming-service.js.map +1 -1
- package/lib/common/chat-session-summary-agent-prompt.js +3 -3
- package/lib/common/chat-session-summary-agent-prompt.js.map +1 -1
- package/lib/common/chat-tool-request-service.d.ts +2 -2
- package/lib/common/chat-tool-request-service.d.ts.map +1 -1
- package/lib/common/image-context-variable.d.ts +29 -0
- package/lib/common/image-context-variable.d.ts.map +1 -0
- package/lib/common/image-context-variable.js +99 -0
- package/lib/common/image-context-variable.js.map +1 -0
- package/package.json +11 -10
- package/src/browser/agent-delegation-tool.ts +207 -0
- package/src/browser/ai-chat-frontend-module.ts +20 -2
- package/src/browser/change-set-file-element.ts +236 -32
- package/src/browser/chat-tool-preferences.ts +4 -4
- package/src/browser/chat-tool-request-service.ts +1 -1
- package/src/browser/delegation-response-content.ts +55 -0
- package/src/browser/file-chat-variable-contribution.ts +120 -6
- package/src/browser/frontend-chat-service.ts +2 -11
- package/src/browser/image-context-variable-contribution.ts +153 -0
- package/src/browser/task-context-service.ts +115 -9
- package/src/browser/task-context-storage-service.ts +5 -1
- package/src/common/change-set.ts +1 -1
- package/src/common/chat-agent-service.ts +1 -0
- package/src/common/chat-agents.ts +26 -9
- package/src/common/chat-model.ts +16 -7
- package/src/common/chat-request-parser.ts +3 -12
- package/src/common/chat-service.ts +40 -10
- package/src/common/chat-session-naming-service.ts +2 -2
- package/src/common/chat-session-summary-agent-prompt.ts +3 -3
- package/src/common/chat-tool-request-service.ts +2 -2
- 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 {
|
|
25
|
-
import {
|
|
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(
|
|
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
|
-
|
|
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
|
|
84
|
-
protected _changeResource
|
|
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.
|
|
93
|
-
|
|
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.
|
|
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({
|
|
175
|
+
this._readOnlyResource.update({
|
|
176
|
+
autosaveable: false,
|
|
177
|
+
readOnly: true,
|
|
178
|
+
contents: this._originalContent ?? ''
|
|
179
|
+
});
|
|
124
180
|
this.toDispose.push(this._readOnlyResource);
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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({
|
|
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.
|
|
226
|
-
await this.changeSetFileService.write(this.uri, this.
|
|
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
|
|
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 !!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
36
|
-
|
|
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 {
|