@theia/ai-ide 1.66.2 → 1.67.0-next.13

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 (28) hide show
  1. package/lib/browser/context-file-validation-service-impl.d.ts +13 -0
  2. package/lib/browser/context-file-validation-service-impl.d.ts.map +1 -0
  3. package/lib/browser/context-file-validation-service-impl.js +123 -0
  4. package/lib/browser/context-file-validation-service-impl.js.map +1 -0
  5. package/lib/browser/context-file-validation-service-impl.spec.d.ts +2 -0
  6. package/lib/browser/context-file-validation-service-impl.spec.d.ts.map +1 -0
  7. package/lib/browser/context-file-validation-service-impl.spec.js +340 -0
  8. package/lib/browser/context-file-validation-service-impl.spec.js.map +1 -0
  9. package/lib/browser/context-functions.d.ts +2 -0
  10. package/lib/browser/context-functions.d.ts.map +1 -1
  11. package/lib/browser/context-functions.js +41 -5
  12. package/lib/browser/context-functions.js.map +1 -1
  13. package/lib/browser/context-functions.spec.js +120 -0
  14. package/lib/browser/context-functions.spec.js.map +1 -1
  15. package/lib/browser/frontend-module.d.ts.map +1 -1
  16. package/lib/browser/frontend-module.js +4 -0
  17. package/lib/browser/frontend-module.js.map +1 -1
  18. package/lib/browser/workspace-functions.d.ts +3 -0
  19. package/lib/browser/workspace-functions.d.ts.map +1 -1
  20. package/lib/browser/workspace-functions.js +56 -0
  21. package/lib/browser/workspace-functions.js.map +1 -1
  22. package/package.json +21 -21
  23. package/src/browser/context-file-validation-service-impl.spec.ts +405 -0
  24. package/src/browser/context-file-validation-service-impl.ts +120 -0
  25. package/src/browser/context-functions.spec.ts +155 -1
  26. package/src/browser/context-functions.ts +40 -6
  27. package/src/browser/frontend-module.ts +5 -0
  28. package/src/browser/workspace-functions.ts +68 -1
@@ -22,7 +22,8 @@ import { ListChatContext, ResolveChatContext, AddFileToChatContext } from './con
22
22
  import { CancellationTokenSource } from '@theia/core';
23
23
  import { ChatContextManager, MutableChatModel, MutableChatRequestModel, MutableChatResponseModel } from '@theia/ai-chat';
24
24
  import { fail } from 'assert';
25
- import { ResolvedAIContextVariable } from '@theia/ai-core';
25
+ import { AIVariableResolutionRequest, ResolvedAIContextVariable } from '@theia/ai-core';
26
+ import { ContextFileValidationService, FileValidationState } from '@theia/ai-chat/lib/browser/context-file-validation-service';
26
27
  disableJSDOM();
27
28
 
28
29
  describe('Context Functions Cancellation Tests', () => {
@@ -100,3 +101,156 @@ describe('Context Functions Cancellation Tests', () => {
100
101
  expect(jsonResponse.error).to.equal('Operation cancelled by user');
101
102
  });
102
103
  });
104
+
105
+ describe('AddFileToChatContext Validation Tests', () => {
106
+ let mockCtx: Partial<MutableChatRequestModel>;
107
+ let addedFiles: AIVariableResolutionRequest[];
108
+
109
+ before(() => {
110
+ disableJSDOM = enableJSDOM();
111
+ });
112
+ after(() => {
113
+ disableJSDOM();
114
+ });
115
+
116
+ beforeEach(() => {
117
+ addedFiles = [];
118
+ const context: Partial<ChatContextManager> = {
119
+ addVariables: (...vars: AIVariableResolutionRequest[]) => {
120
+ addedFiles.push(...vars);
121
+ },
122
+ getVariables: () => []
123
+ };
124
+ mockCtx = {
125
+ response: {
126
+ cancellationToken: new CancellationTokenSource().token
127
+ } as MutableChatResponseModel,
128
+ context: {
129
+ variables: []
130
+ },
131
+ session: {
132
+ context
133
+ } as MutableChatModel
134
+ };
135
+ });
136
+
137
+ it('should add valid files to context', async () => {
138
+ const mockValidationService: ContextFileValidationService = {
139
+ validateFile: async () => ({ state: FileValidationState.VALID })
140
+ };
141
+
142
+ const addFileToChatContext = new AddFileToChatContext();
143
+ (addFileToChatContext as unknown as { validationService: ContextFileValidationService }).validationService = mockValidationService;
144
+
145
+ const result = await addFileToChatContext.getTool().handler(
146
+ '{"filesToAdd":["/valid/file1.ts","/valid/file2.ts"]}',
147
+ mockCtx
148
+ );
149
+
150
+ if (typeof result !== 'string') {
151
+ fail(`Wrong tool call result type: ${result}`);
152
+ }
153
+
154
+ const jsonResponse = JSON.parse(result);
155
+ expect(jsonResponse.added).to.have.lengthOf(2);
156
+ expect(jsonResponse.added).to.include('/valid/file1.ts');
157
+ expect(jsonResponse.added).to.include('/valid/file2.ts');
158
+ expect(jsonResponse.rejected).to.have.lengthOf(0);
159
+ expect(jsonResponse.summary.totalRequested).to.equal(2);
160
+ expect(jsonResponse.summary.added).to.equal(2);
161
+ expect(jsonResponse.summary.rejected).to.equal(0);
162
+ expect(addedFiles).to.have.lengthOf(2);
163
+ });
164
+
165
+ it('should reject non-existent files', async () => {
166
+ const mockValidationService: ContextFileValidationService = {
167
+ validateFile: async file => {
168
+ if (file === '/nonexistent/file.ts') {
169
+ return {
170
+ state: FileValidationState.INVALID_NOT_FOUND,
171
+ message: 'File does not exist'
172
+ };
173
+ }
174
+ return { state: FileValidationState.VALID };
175
+ }
176
+ };
177
+
178
+ const addFileToChatContext = new AddFileToChatContext();
179
+ (addFileToChatContext as unknown as { validationService: ContextFileValidationService }).validationService = mockValidationService;
180
+
181
+ const result = await addFileToChatContext.getTool().handler(
182
+ '{"filesToAdd":["/valid/file.ts","/nonexistent/file.ts"]}',
183
+ mockCtx
184
+ );
185
+
186
+ if (typeof result !== 'string') {
187
+ fail(`Wrong tool call result type: ${result}`);
188
+ }
189
+
190
+ const jsonResponse = JSON.parse(result);
191
+ expect(jsonResponse.added).to.have.lengthOf(1);
192
+ expect(jsonResponse.added).to.include('/valid/file.ts');
193
+ expect(jsonResponse.rejected).to.have.lengthOf(1);
194
+ expect(jsonResponse.rejected[0].file).to.equal('/nonexistent/file.ts');
195
+ expect(jsonResponse.rejected[0].reason).to.equal('File does not exist');
196
+ expect(jsonResponse.rejected[0].state).to.equal(FileValidationState.INVALID_NOT_FOUND);
197
+ expect(jsonResponse.summary.totalRequested).to.equal(2);
198
+ expect(jsonResponse.summary.added).to.equal(1);
199
+ expect(jsonResponse.summary.rejected).to.equal(1);
200
+ expect(addedFiles).to.have.lengthOf(1);
201
+ });
202
+
203
+ it('should reject files in secondary workspace roots', async () => {
204
+ const mockValidationService: ContextFileValidationService = {
205
+ validateFile: async file => {
206
+ if (file === '/secondary/root/file.ts') {
207
+ return {
208
+ state: FileValidationState.INVALID_SECONDARY,
209
+ message: 'File is in a secondary workspace root. AI agents can only access files in the first workspace root.'
210
+ };
211
+ }
212
+ return { state: FileValidationState.VALID };
213
+ }
214
+ };
215
+
216
+ const addFileToChatContext = new AddFileToChatContext();
217
+ (addFileToChatContext as unknown as { validationService: ContextFileValidationService }).validationService = mockValidationService;
218
+
219
+ const result = await addFileToChatContext.getTool().handler(
220
+ '{"filesToAdd":["/secondary/root/file.ts"]}',
221
+ mockCtx
222
+ );
223
+
224
+ if (typeof result !== 'string') {
225
+ fail(`Wrong tool call result type: ${result}`);
226
+ }
227
+
228
+ const jsonResponse = JSON.parse(result);
229
+ expect(jsonResponse.added).to.have.lengthOf(0);
230
+ expect(jsonResponse.rejected).to.have.lengthOf(1);
231
+ expect(jsonResponse.rejected[0].file).to.equal('/secondary/root/file.ts');
232
+ expect(jsonResponse.rejected[0].state).to.equal(FileValidationState.INVALID_SECONDARY);
233
+ expect(addedFiles).to.have.lengthOf(0);
234
+ });
235
+
236
+ it('should add all files when validation service is not available', async () => {
237
+ const addFileToChatContext = new AddFileToChatContext();
238
+
239
+ const result = await addFileToChatContext.getTool().handler(
240
+ '{"filesToAdd":["/file1.ts","/file2.ts"]}',
241
+ mockCtx
242
+ );
243
+
244
+ if (typeof result !== 'string') {
245
+ fail(`Wrong tool call result type: ${result}`);
246
+ }
247
+
248
+ const jsonResponse = JSON.parse(result);
249
+ expect(jsonResponse.added).to.have.lengthOf(2);
250
+ expect(jsonResponse.rejected).to.have.lengthOf(0);
251
+ expect(jsonResponse.summary.totalRequested).to.equal(2);
252
+ expect(jsonResponse.summary.added).to.equal(2);
253
+ expect(jsonResponse.summary.rejected).to.equal(0);
254
+ expect(addedFiles).to.have.lengthOf(2);
255
+ });
256
+ });
@@ -16,9 +16,10 @@
16
16
 
17
17
  import { MutableChatRequestModel } from '@theia/ai-chat';
18
18
  import { ToolProvider, ToolRequest } from '@theia/ai-core';
19
- import { injectable } from '@theia/core/shared/inversify';
19
+ import { inject, injectable, optional } from '@theia/core/shared/inversify';
20
20
  import { LIST_CHAT_CONTEXT_FUNCTION_ID, RESOLVE_CHAT_CONTEXT_FUNCTION_ID, UPDATE_CONTEXT_FILES_FUNCTION_ID } from '../common/context-functions';
21
21
  import { FILE_VARIABLE } from '@theia/ai-core/lib/browser/file-variable-contribution';
22
+ import { ContextFileValidationService, FileValidationState } from '@theia/ai-chat/lib/browser/context-file-validation-service';
22
23
 
23
24
  @injectable()
24
25
  export class ListChatContext implements ToolProvider {
@@ -91,6 +92,9 @@ export class ResolveChatContext implements ToolProvider {
91
92
  export class AddFileToChatContext implements ToolProvider {
92
93
  static ID = UPDATE_CONTEXT_FILES_FUNCTION_ID;
93
94
 
95
+ @inject(ContextFileValidationService) @optional()
96
+ protected readonly validationService: ContextFileValidationService | undefined;
97
+
94
98
  getTool(): ToolRequest {
95
99
  return {
96
100
  id: AddFileToChatContext.ID,
@@ -106,7 +110,10 @@ export class AddFileToChatContext implements ToolProvider {
106
110
  },
107
111
  required: ['filesToAdd']
108
112
  },
109
- description: 'Adds one or more files to the context of the current chat session, and returns the current list of files in the context.',
113
+ description: 'Adds one or more files to the context of the current chat session. ' +
114
+ 'Only files that exist within the workspace boundaries will be added. ' +
115
+ 'Files outside the workspace or non-existent files will be rejected. ' +
116
+ 'Returns a detailed status for each file, including which were successfully added and which were rejected with reasons.',
110
117
  handler: async (arg: string, ctx: MutableChatRequestModel): Promise<string> => {
111
118
  if (ctx?.response?.cancellationToken?.isCancellationRequested) {
112
119
  return JSON.stringify({ error: 'Operation cancelled by user' });
@@ -114,11 +121,38 @@ export class AddFileToChatContext implements ToolProvider {
114
121
 
115
122
  const { filesToAdd } = JSON.parse(arg) as { filesToAdd: string[] };
116
123
 
117
- ctx.session.context.addVariables(...filesToAdd.map(file => ({ arg: file, variable: FILE_VARIABLE })));
118
- const result = ctx.session.context.getVariables().filter(candidate => candidate.variable.id === FILE_VARIABLE.id && !!candidate.arg)
119
- .map(fileRequest => fileRequest.arg);
124
+ const added: string[] = [];
125
+ const rejected: Array<{ file: string; reason: string; state: string }> = [];
126
+
127
+ for (const file of filesToAdd) {
128
+ if (this.validationService) {
129
+ const validationResult = await this.validationService.validateFile(file);
130
+
131
+ if (validationResult.state === FileValidationState.VALID) {
132
+ ctx.session.context.addVariables({ arg: file, variable: FILE_VARIABLE });
133
+ added.push(file);
134
+ } else {
135
+ rejected.push({
136
+ file,
137
+ reason: validationResult.message || 'File validation failed',
138
+ state: validationResult.state
139
+ });
140
+ }
141
+ } else {
142
+ ctx.session.context.addVariables({ arg: file, variable: FILE_VARIABLE });
143
+ added.push(file);
144
+ }
145
+ }
120
146
 
121
- return JSON.stringify(result);
147
+ return JSON.stringify({
148
+ added,
149
+ rejected,
150
+ summary: {
151
+ totalRequested: filesToAdd.length,
152
+ added: added.length,
153
+ rejected: rejected.length
154
+ }
155
+ });
122
156
  }
123
157
  };
124
158
  }
@@ -96,6 +96,8 @@ import { AiConfigurationPreferences } from '../common/ai-configuration-preferenc
96
96
  import { TaskContextAgent } from './task-context-agent';
97
97
  import { ProjectInfoAgent } from './project-info-agent';
98
98
  import { SuggestTerminalCommand } from './ai-terminal-functions';
99
+ import { ContextFileValidationService } from '@theia/ai-chat/lib/browser/context-file-validation-service';
100
+ import { ContextFileValidationServiceImpl } from './context-file-validation-service-impl';
99
101
 
100
102
  export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
101
103
  bind(PreferenceContribution).toConstantValue({ schema: aiIdePreferenceSchema });
@@ -270,4 +272,7 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
270
272
  .inSingletonScope();
271
273
 
272
274
  bindToolProvider(SuggestTerminalCommand, bind);
275
+
276
+ bind(ContextFileValidationServiceImpl).toSelf().inSingletonScope();
277
+ bind(ContextFileValidationService).toService(ContextFileValidationServiceImpl);
273
278
  });
@@ -14,7 +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 { ToolProvider, ToolRequest } from '@theia/ai-core';
17
- import { CancellationToken, Disposable, PreferenceService, URI } from '@theia/core';
17
+ import { CancellationToken, Disposable, PreferenceService, URI, Path } from '@theia/core';
18
18
  import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
20
20
  import { FileStat } from '@theia/filesystem/lib/common/files';
@@ -69,6 +69,73 @@ export class WorkspaceFunctionScope {
69
69
  return workspaceRoot.resolve(relativePath);
70
70
  }
71
71
 
72
+ isInWorkspace(uri: URI): boolean {
73
+ try {
74
+ const wsRoots = this.workspaceService.tryGetRoots();
75
+
76
+ if (wsRoots.length === 0) {
77
+ return false;
78
+ }
79
+
80
+ for (const root of wsRoots) {
81
+ const rootUri = root.resource;
82
+ if (rootUri.scheme === uri.scheme && rootUri.isEqualOrParent(uri)) {
83
+ return true;
84
+ }
85
+ }
86
+
87
+ return false;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ isInPrimaryWorkspace(uri: URI): boolean {
94
+ try {
95
+ const wsRoots = this.workspaceService.tryGetRoots();
96
+
97
+ if (wsRoots.length === 0) {
98
+ return false;
99
+ }
100
+
101
+ const primaryRoot = wsRoots[0].resource;
102
+ return primaryRoot.scheme === uri.scheme && primaryRoot.isEqualOrParent(uri);
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ async resolveToUri(pathOrUri: string | URI): Promise<URI | undefined> {
109
+ if (pathOrUri instanceof URI) {
110
+ return pathOrUri;
111
+ }
112
+
113
+ if (!pathOrUri) {
114
+ return undefined;
115
+ }
116
+
117
+ if (pathOrUri.includes('://')) {
118
+ try {
119
+ const uri = new URI(pathOrUri);
120
+ return uri;
121
+ } catch (error) {
122
+ }
123
+ }
124
+
125
+ const normalizedPath = Path.normalizePathSeparator(pathOrUri);
126
+ const path = new Path(normalizedPath);
127
+
128
+ if (normalizedPath.includes('..')) {
129
+ return undefined;
130
+ }
131
+
132
+ if (path.isAbsolute) {
133
+ return URI.fromFilePath(normalizedPath);
134
+ }
135
+
136
+ return this.resolveRelativePath(normalizedPath);
137
+ }
138
+
72
139
  private async initializeGitignoreWatcher(workspaceRoot: URI): Promise<void> {
73
140
  if (this.gitignoreWatcherInitialized) {
74
141
  return;