@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.
@@ -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?.cancellationToken, offset, limit);
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, cancellationToken?: CancellationToken, offset?: number, limit?: number): Promise<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(content: string, maxSizeKB: number, offset?: number, limit?: number): string {
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
- private async handleFullDiskRead(targetUri: URI, maxSizeKB: number): Promise<string> {
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) {