@theia/ai-ide 1.75.0-next.21 → 1.75.0-next.28
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/file-changeset-functions.d.ts +5 -1
- package/lib/browser/file-changeset-functions.d.ts.map +1 -1
- package/lib/browser/file-changeset-functions.js +23 -0
- package/lib/browser/file-changeset-functions.js.map +1 -1
- package/lib/browser/file-changeset-functions.spec.js +36 -0
- package/lib/browser/file-changeset-functions.spec.js.map +1 -1
- package/lib/browser/workspace-functions.d.ts +8 -0
- package/lib/browser/workspace-functions.d.ts.map +1 -1
- package/lib/browser/workspace-functions.js +24 -6
- package/lib/browser/workspace-functions.js.map +1 -1
- package/lib/browser/workspace-functions.spec.js +6 -0
- package/lib/browser/workspace-functions.spec.js.map +1 -1
- package/package.json +23 -23
- package/src/browser/file-changeset-functions.spec.ts +46 -1
- package/src/browser/file-changeset-functions.ts +24 -2
- package/src/browser/workspace-functions.spec.ts +8 -0
- package/src/browser/workspace-functions.ts +26 -6
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
-
import { assertChatContext, ChatToolContext } from '@theia/ai-chat';
|
|
16
|
+
import { assertChatContext, ChatToolContext, FileReadTracker } from '@theia/ai-chat';
|
|
17
17
|
import { ChangeSet } from '@theia/ai-chat/lib/common/change-set';
|
|
18
18
|
import { ChangeSetElementArgs, ChangeSetFileElement, ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
|
|
19
19
|
import { ToolInvocationContext, ToolProvider, ToolRequest, ToolRequestParameters, ToolRequestParametersProperties } from '@theia/ai-core';
|
|
20
20
|
import { ContentReplacerV1Impl, Replacement, ContentReplacer } from '@theia/core/lib/common/content-replacer';
|
|
21
21
|
import { ContentReplacerV2Impl } from '@theia/core/lib/common/content-replacer-v2-impl';
|
|
22
22
|
import { URI } from '@theia/core/lib/common/uri';
|
|
23
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
23
|
+
import { inject, injectable, optional } from '@theia/core/shared/inversify';
|
|
24
24
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
25
25
|
import { WorkspaceFunctionScope } from './workspace-functions';
|
|
26
26
|
|
|
@@ -54,6 +54,14 @@ function createPathShortLabel(args: string, hasMore: boolean): { label: string;
|
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Whole-file writes from an outdated read would silently discard whatever changed in between. The replacement
|
|
59
|
+
* tools need no such guard: they re-read and fail when their matched content is gone.
|
|
60
|
+
*/
|
|
61
|
+
function staleFileError(path: string): string {
|
|
62
|
+
return `File ${path} changed since you last read it. Read it again before overwriting it, so that the changes made in the meantime are not lost.`;
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export const FileChangeSetTitleProvider = Symbol('FileChangeSetTitleProvider');
|
|
58
66
|
|
|
59
67
|
export interface FileChangeSetTitleProvider {
|
|
@@ -76,6 +84,10 @@ export class SuggestFileContent implements ToolProvider {
|
|
|
76
84
|
@inject(FileChangeSetTitleProvider)
|
|
77
85
|
protected readonly fileChangeSetTitleProvider: FileChangeSetTitleProvider;
|
|
78
86
|
|
|
87
|
+
/** Optional: the guard is advisory, so containers without a tracker still get a working tool. */
|
|
88
|
+
@inject(FileReadTracker) @optional()
|
|
89
|
+
protected readonly fileReadTracker: FileReadTracker | undefined;
|
|
90
|
+
|
|
79
91
|
getTool(): ToolRequest {
|
|
80
92
|
return {
|
|
81
93
|
id: SuggestFileContent.ID,
|
|
@@ -114,6 +126,9 @@ export class SuggestFileContent implements ToolProvider {
|
|
|
114
126
|
} catch (error) {
|
|
115
127
|
return JSON.stringify({ error: error.message });
|
|
116
128
|
}
|
|
129
|
+
if (await this.fileReadTracker?.isStale(chatSessionId, uri)) {
|
|
130
|
+
return JSON.stringify({ error: staleFileError(path) });
|
|
131
|
+
}
|
|
117
132
|
let type: ChangeSetElementArgs['type'] = 'modify';
|
|
118
133
|
if (content === '') {
|
|
119
134
|
type = 'delete';
|
|
@@ -156,6 +171,10 @@ export class WriteFileContent implements ToolProvider {
|
|
|
156
171
|
@inject(FileChangeSetTitleProvider)
|
|
157
172
|
protected readonly fileChangeSetTitleProvider: FileChangeSetTitleProvider;
|
|
158
173
|
|
|
174
|
+
/** Optional: the guard is advisory, so containers without a tracker still get a working tool. */
|
|
175
|
+
@inject(FileReadTracker) @optional()
|
|
176
|
+
protected readonly fileReadTracker: FileReadTracker | undefined;
|
|
177
|
+
|
|
159
178
|
getTool(): ToolRequest {
|
|
160
179
|
return {
|
|
161
180
|
id: WriteFileContent.ID,
|
|
@@ -194,6 +213,9 @@ export class WriteFileContent implements ToolProvider {
|
|
|
194
213
|
} catch (error) {
|
|
195
214
|
return JSON.stringify({ error: error.message });
|
|
196
215
|
}
|
|
216
|
+
if (await this.fileReadTracker?.isStale(chatSessionId, uri)) {
|
|
217
|
+
return JSON.stringify({ error: staleFileError(path) });
|
|
218
|
+
}
|
|
197
219
|
let type = 'modify';
|
|
198
220
|
if (content === '') {
|
|
199
221
|
type = 'delete';
|
|
@@ -2164,6 +2164,13 @@ describe('WorkspaceFunctionScope Multi-Root Tests', () => {
|
|
|
2164
2164
|
const result = workspaceScope.resolveRelativePath('app/src/index.ts');
|
|
2165
2165
|
expect(result.toString()).to.equal('file:///workspace/a/app/src/index.ts');
|
|
2166
2166
|
});
|
|
2167
|
+
|
|
2168
|
+
// The forms the external file change notice names files by: a root name for the mapped root, a uri for the other.
|
|
2169
|
+
it('resolves a uri for the unmapped root', async () => {
|
|
2170
|
+
workspaceScope = createScope(['file:///workspace/a/app', 'file:///workspace/b/app']);
|
|
2171
|
+
const unmapped = 'file:///workspace/b/app/src/index.ts';
|
|
2172
|
+
expect((await workspaceScope.resolveAccessiblePath(unmapped)).toString()).to.equal(unmapped);
|
|
2173
|
+
});
|
|
2167
2174
|
});
|
|
2168
2175
|
});
|
|
2169
2176
|
|
|
@@ -2318,5 +2325,6 @@ describe('WorkspaceFunctionScope Multi-Root Tests', () => {
|
|
|
2318
2325
|
expect(resolved.toString()).to.equal(fileUri.toString());
|
|
2319
2326
|
}
|
|
2320
2327
|
});
|
|
2328
|
+
|
|
2321
2329
|
});
|
|
2322
2330
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import { AiConfigurationService, ToolInvocationContext, ToolProvider, ToolRequest } from '@theia/ai-core';
|
|
17
|
+
import { ChatToolContext, FileReadTracker } from '@theia/ai-chat';
|
|
17
18
|
import { CancellationToken, Disposable, OS, PreferenceService, URI, Path } from '@theia/core';
|
|
18
19
|
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
|
19
20
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
@@ -790,7 +791,7 @@ export class FileContentFunction implements ToolProvider {
|
|
|
790
791
|
},
|
|
791
792
|
handler: (arg_string: string, ctx?: ToolInvocationContext) => {
|
|
792
793
|
const { file, offset, limit } = this.parseArg(arg_string);
|
|
793
|
-
return this.getFileContent(file, ctx
|
|
794
|
+
return this.getFileContent(file, ctx, offset, limit);
|
|
794
795
|
},
|
|
795
796
|
providerName: undefined,
|
|
796
797
|
getArgumentsShortLabel: (args: string): { label: string; hasMore: boolean } | undefined => {
|
|
@@ -823,12 +824,17 @@ export class FileContentFunction implements ToolProvider {
|
|
|
823
824
|
@inject(PreferenceService)
|
|
824
825
|
protected readonly preferences: PreferenceService;
|
|
825
826
|
|
|
827
|
+
/** Optional: tracking is advisory, so containers without a tracker still get a working tool. */
|
|
828
|
+
@inject(FileReadTracker) @optional()
|
|
829
|
+
protected readonly fileReadTracker: FileReadTracker | undefined;
|
|
830
|
+
|
|
826
831
|
private parseArg(arg_string: string): { file: string; offset?: number; limit?: number } {
|
|
827
832
|
const result = JSON.parse(arg_string);
|
|
828
833
|
return { file: result.file, offset: result.offset, limit: result.limit };
|
|
829
834
|
}
|
|
830
835
|
|
|
831
|
-
private async getFileContent(file: string,
|
|
836
|
+
private async getFileContent(file: string, ctx?: ToolInvocationContext, offset?: number, limit?: number): Promise<string> {
|
|
837
|
+
const cancellationToken = ctx?.cancellationToken;
|
|
832
838
|
if (cancellationToken?.isCancellationRequested) {
|
|
833
839
|
return JSON.stringify({ error: 'Operation cancelled by user' });
|
|
834
840
|
}
|
|
@@ -857,20 +863,23 @@ export class FileContentFunction implements ToolProvider {
|
|
|
857
863
|
const isPaginated = offset !== undefined || limit !== undefined;
|
|
858
864
|
|
|
859
865
|
if (isEditorOpen) {
|
|
860
|
-
return this.handleEditorContent(openEditorValue!, maxSizeKB, offset, limit);
|
|
866
|
+
return this.handleEditorContent(targetUri, openEditorValue!, maxSizeKB, ctx, offset, limit);
|
|
861
867
|
} else if (isPaginated) {
|
|
862
868
|
return this.readStreamedSlice(targetUri, maxSizeKB, offset, limit);
|
|
863
869
|
} else {
|
|
864
|
-
return this.handleFullDiskRead(targetUri, maxSizeKB);
|
|
870
|
+
return this.handleFullDiskRead(targetUri, maxSizeKB, ctx);
|
|
865
871
|
}
|
|
866
872
|
}
|
|
867
873
|
|
|
868
|
-
private handleEditorContent(
|
|
874
|
+
private async handleEditorContent(
|
|
875
|
+
targetUri: URI, content: string, maxSizeKB: number, ctx?: ToolInvocationContext, offset?: number, limit?: number
|
|
876
|
+
): Promise<string> {
|
|
869
877
|
if (offset === undefined && limit === undefined) {
|
|
870
878
|
const sizeKB = this.sizeInKB(content);
|
|
871
879
|
if (sizeKB > maxSizeKB) {
|
|
872
880
|
return this.buildFileSizeLimitError(sizeKB, maxSizeKB);
|
|
873
881
|
}
|
|
882
|
+
await this.trackRead(targetUri, content, ctx);
|
|
874
883
|
return content;
|
|
875
884
|
}
|
|
876
885
|
|
|
@@ -888,7 +897,17 @@ export class FileContentFunction implements ToolProvider {
|
|
|
888
897
|
return `${header}\n${result}`;
|
|
889
898
|
}
|
|
890
899
|
|
|
891
|
-
|
|
900
|
+
/**
|
|
901
|
+
* Remembers the content handed to the agent, if it came from a chat session at all. Only full reads are
|
|
902
|
+
* tracked: a slice says nothing about the rest of the file, so the streaming path is skipped.
|
|
903
|
+
*/
|
|
904
|
+
private async trackRead(targetUri: URI, content: string, ctx?: ToolInvocationContext): Promise<void> {
|
|
905
|
+
if (ChatToolContext.is(ctx)) {
|
|
906
|
+
await this.fileReadTracker?.recordRead(ctx.request.session.id, targetUri, content);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private async handleFullDiskRead(targetUri: URI, maxSizeKB: number, ctx?: ToolInvocationContext): Promise<string> {
|
|
892
911
|
try {
|
|
893
912
|
const stat = await this.fileService.resolve(targetUri);
|
|
894
913
|
if (stat.size !== undefined) {
|
|
@@ -907,6 +926,7 @@ export class FileContentFunction implements ToolProvider {
|
|
|
907
926
|
if (sizeKB > maxSizeKB) {
|
|
908
927
|
return this.buildFileSizeLimitError(sizeKB, maxSizeKB);
|
|
909
928
|
}
|
|
929
|
+
await this.trackRead(targetUri, rawContent, ctx);
|
|
910
930
|
return rawContent;
|
|
911
931
|
} catch (error) {
|
|
912
932
|
if (error instanceof FileOperationError) {
|