@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.
- package/lib/browser/context-file-validation-service-impl.d.ts +13 -0
- package/lib/browser/context-file-validation-service-impl.d.ts.map +1 -0
- package/lib/browser/context-file-validation-service-impl.js +123 -0
- package/lib/browser/context-file-validation-service-impl.js.map +1 -0
- package/lib/browser/context-file-validation-service-impl.spec.d.ts +2 -0
- package/lib/browser/context-file-validation-service-impl.spec.d.ts.map +1 -0
- package/lib/browser/context-file-validation-service-impl.spec.js +340 -0
- package/lib/browser/context-file-validation-service-impl.spec.js.map +1 -0
- package/lib/browser/context-functions.d.ts +2 -0
- package/lib/browser/context-functions.d.ts.map +1 -1
- package/lib/browser/context-functions.js +41 -5
- package/lib/browser/context-functions.js.map +1 -1
- package/lib/browser/context-functions.spec.js +120 -0
- package/lib/browser/context-functions.spec.js.map +1 -1
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +4 -0
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/workspace-functions.d.ts +3 -0
- package/lib/browser/workspace-functions.d.ts.map +1 -1
- package/lib/browser/workspace-functions.js +56 -0
- package/lib/browser/workspace-functions.js.map +1 -1
- package/package.json +21 -21
- package/src/browser/context-file-validation-service-impl.spec.ts +405 -0
- package/src/browser/context-file-validation-service-impl.ts +120 -0
- package/src/browser/context-functions.spec.ts +155 -1
- package/src/browser/context-functions.ts +40 -6
- package/src/browser/frontend-module.ts +5 -0
- 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
|
|
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
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
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(
|
|
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;
|