@theia/ai-ide 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/ai-configuration/agent-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/agent-configuration-widget.js +7 -2
- package/lib/browser/ai-configuration/agent-configuration-widget.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.js +1 -1
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-widget.d.ts +0 -1
- package/lib/browser/ai-configuration/ai-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-widget.js +0 -1
- package/lib/browser/ai-configuration/ai-configuration-widget.js.map +1 -1
- package/lib/browser/ai-configuration/mcp-configuration-widget.d.ts +4 -1
- package/lib/browser/ai-configuration/mcp-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/mcp-configuration-widget.js +59 -11
- package/lib/browser/ai-configuration/mcp-configuration-widget.js.map +1 -1
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.d.ts +0 -1
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js +1 -4
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js.map +1 -1
- package/lib/browser/ai-configuration/template-settings-renderer.js +1 -1
- package/lib/browser/ai-configuration/template-settings-renderer.js.map +1 -1
- package/lib/browser/ai-configuration/tools-configuration-widget.d.ts +0 -1
- package/lib/browser/ai-configuration/tools-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/tools-configuration-widget.js +6 -21
- package/lib/browser/ai-configuration/tools-configuration-widget.js.map +1 -1
- package/lib/browser/app-tester-chat-agent.d.ts +5 -3
- package/lib/browser/app-tester-chat-agent.d.ts.map +1 -1
- package/lib/browser/app-tester-chat-agent.js +41 -31
- package/lib/browser/app-tester-chat-agent.js.map +1 -1
- package/lib/browser/architect-agent.d.ts.map +1 -1
- package/lib/browser/architect-agent.js +6 -3
- package/lib/browser/architect-agent.js.map +1 -1
- package/lib/browser/frontend-module.d.ts +1 -0
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +1 -0
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/summarize-session-command-contribution.d.ts +8 -1
- package/lib/browser/summarize-session-command-contribution.d.ts.map +1 -1
- package/lib/browser/summarize-session-command-contribution.js +60 -6
- package/lib/browser/summarize-session-command-contribution.js.map +1 -1
- package/lib/browser/task-context-file-storage-service.d.ts +4 -2
- package/lib/browser/task-context-file-storage-service.d.ts.map +1 -1
- package/lib/browser/task-context-file-storage-service.js +19 -9
- package/lib/browser/task-context-file-storage-service.js.map +1 -1
- package/lib/browser/workspace-preferences.d.ts +1 -0
- package/lib/browser/workspace-preferences.d.ts.map +1 -1
- package/lib/browser/workspace-preferences.js +9 -1
- package/lib/browser/workspace-preferences.js.map +1 -1
- package/lib/browser/workspace-search-provider.d.ts +5 -1
- package/lib/browser/workspace-search-provider.d.ts.map +1 -1
- package/lib/browser/workspace-search-provider.js +57 -17
- package/lib/browser/workspace-search-provider.js.map +1 -1
- package/lib/browser/workspace-search-provider.spec.d.ts +2 -0
- package/lib/browser/workspace-search-provider.spec.d.ts.map +1 -0
- package/lib/browser/workspace-search-provider.spec.js +227 -0
- package/lib/browser/workspace-search-provider.spec.js.map +1 -0
- package/lib/common/architect-prompt-template.d.ts +4 -2
- package/lib/common/architect-prompt-template.d.ts.map +1 -1
- package/lib/common/architect-prompt-template.js +201 -35
- package/lib/common/architect-prompt-template.js.map +1 -1
- package/lib/common/coder-replace-prompt-template.d.ts +4 -4
- package/lib/common/coder-replace-prompt-template.d.ts.map +1 -1
- package/lib/common/coder-replace-prompt-template.js +8 -8
- package/lib/common/coder-replace-prompt-template.js.map +1 -1
- package/lib/common/summarize-session-commands.d.ts +1 -0
- package/lib/common/summarize-session-commands.d.ts.map +1 -1
- package/lib/common/summarize-session-commands.js +5 -1
- package/lib/common/summarize-session-commands.js.map +1 -1
- package/lib/common/universal-chat-agent.js +2 -2
- package/lib/common/workspace-search-provider-util.d.ts +17 -0
- package/lib/common/workspace-search-provider-util.d.ts.map +1 -0
- package/lib/common/workspace-search-provider-util.js +51 -0
- package/lib/common/workspace-search-provider-util.js.map +1 -0
- package/package.json +19 -18
- package/src/browser/ai-configuration/agent-configuration-widget.tsx +7 -5
- package/src/browser/ai-configuration/ai-configuration-view-contribution.ts +1 -1
- package/src/browser/ai-configuration/ai-configuration-widget.tsx +0 -1
- package/src/browser/ai-configuration/mcp-configuration-widget.tsx +82 -14
- package/src/browser/ai-configuration/prompt-fragments-configuration-widget.tsx +1 -4
- package/src/browser/ai-configuration/template-settings-renderer.tsx +1 -1
- package/src/browser/ai-configuration/tools-configuration-widget.tsx +8 -23
- package/src/browser/app-tester-chat-agent.ts +43 -33
- package/src/browser/architect-agent.ts +8 -5
- package/src/browser/frontend-module.ts +2 -0
- package/src/browser/style/index.css +23 -28
- package/src/browser/summarize-session-command-contribution.ts +64 -8
- package/src/browser/task-context-file-storage-service.ts +20 -10
- package/src/browser/workspace-preferences.ts +9 -0
- package/src/browser/workspace-search-provider.spec.ts +255 -0
- package/src/browser/workspace-search-provider.ts +62 -16
- package/src/common/architect-prompt-template.ts +201 -35
- package/src/common/coder-replace-prompt-template.ts +8 -8
- package/src/common/summarize-session-commands.ts +5 -0
- package/src/common/universal-chat-agent.ts +2 -2
- package/src/common/workspace-search-provider-util.ts +50 -0
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
import { ChatAgentLocation, ChatService } from '@theia/ai-chat/lib/common';
|
|
18
18
|
import { CommandContribution, CommandRegistry, CommandService } from '@theia/core';
|
|
19
|
+
import { TaskContextStorageService, TaskContextService } from '@theia/ai-chat/lib/browser/task-context-service';
|
|
19
20
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
20
|
-
import { AI_SUMMARIZE_SESSION_AS_TASK_FOR_CODER } from '../common/summarize-session-commands';
|
|
21
|
-
import { TaskContextService } from '@theia/ai-chat/lib/browser/task-context-service';
|
|
21
|
+
import { AI_SUMMARIZE_SESSION_AS_TASK_FOR_CODER, AI_UPDATE_TASK_CONTEXT_COMMAND } from '../common/summarize-session-commands';
|
|
22
22
|
import { CoderAgent } from './coder-agent';
|
|
23
23
|
import { TASK_CONTEXT_VARIABLE } from '@theia/ai-chat/lib/browser/task-context-variable';
|
|
24
|
-
import { ARCHITECT_TASK_SUMMARY_PROMPT_TEMPLATE_ID } from '../common/architect-prompt-template';
|
|
24
|
+
import { ARCHITECT_TASK_SUMMARY_PROMPT_TEMPLATE_ID, ARCHITECT_TASK_SUMMARY_UPDATE_PROMPT_TEMPLATE_ID } from '../common/architect-prompt-template';
|
|
25
|
+
import { FILE_VARIABLE } from '@theia/ai-core/lib/browser/file-variable-contribution';
|
|
26
|
+
import { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
27
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
28
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
29
|
+
import { AICommandHandlerFactory } from '@theia/ai-core/lib/browser';
|
|
25
30
|
|
|
26
31
|
@injectable()
|
|
27
32
|
export class SummarizeSessionCommandContribution implements CommandContribution {
|
|
@@ -37,8 +42,39 @@ export class SummarizeSessionCommandContribution implements CommandContribution
|
|
|
37
42
|
@inject(CoderAgent)
|
|
38
43
|
protected readonly coderAgent: CoderAgent;
|
|
39
44
|
|
|
45
|
+
@inject(TaskContextStorageService)
|
|
46
|
+
protected readonly taskContextStorageService: TaskContextStorageService;
|
|
47
|
+
|
|
48
|
+
@inject(FileService)
|
|
49
|
+
protected readonly fileService: FileService;
|
|
50
|
+
|
|
51
|
+
@inject(WorkspaceService)
|
|
52
|
+
protected readonly wsService: WorkspaceService;
|
|
53
|
+
|
|
54
|
+
@inject(AICommandHandlerFactory)
|
|
55
|
+
protected readonly commandHandlerFactory: AICommandHandlerFactory;
|
|
56
|
+
|
|
40
57
|
registerCommands(registry: CommandRegistry): void {
|
|
41
|
-
registry.registerCommand(
|
|
58
|
+
registry.registerCommand(AI_UPDATE_TASK_CONTEXT_COMMAND, this.commandHandlerFactory({
|
|
59
|
+
execute: async () => {
|
|
60
|
+
const activeSession = this.chatService.getActiveSession();
|
|
61
|
+
|
|
62
|
+
if (!activeSession) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if there is an existing summary for this session
|
|
67
|
+
if (!this.taskContextService.hasSummary(activeSession)) {
|
|
68
|
+
// If no summary exists, create one first
|
|
69
|
+
await this.taskContextService.summarize(activeSession, ARCHITECT_TASK_SUMMARY_PROMPT_TEMPLATE_ID);
|
|
70
|
+
} else {
|
|
71
|
+
// Update existing summary
|
|
72
|
+
await this.taskContextService.update(activeSession, ARCHITECT_TASK_SUMMARY_UPDATE_PROMPT_TEMPLATE_ID);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
registry.registerCommand(AI_SUMMARIZE_SESSION_AS_TASK_FOR_CODER, this.commandHandlerFactory({
|
|
42
78
|
execute: async () => {
|
|
43
79
|
const activeSession = this.chatService.getActiveSession();
|
|
44
80
|
|
|
@@ -48,10 +84,30 @@ export class SummarizeSessionCommandContribution implements CommandContribution
|
|
|
48
84
|
|
|
49
85
|
const summaryId = await this.taskContextService.summarize(activeSession, ARCHITECT_TASK_SUMMARY_PROMPT_TEMPLATE_ID);
|
|
50
86
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
// Open the summary in a new editor
|
|
88
|
+
await this.taskContextStorageService.open(summaryId);
|
|
89
|
+
|
|
90
|
+
// Add the summary file to the context of the active Architect session
|
|
91
|
+
const summary = this.taskContextService.getAll().find(s => s.id === summaryId);
|
|
92
|
+
if (summary?.uri) {
|
|
93
|
+
if (await this.fileService.exists(summary?.uri)) {
|
|
94
|
+
const wsRelativePath = await this.wsService.getWorkspaceRelativePath(summary?.uri);
|
|
95
|
+
// Create a file variable for the summary
|
|
96
|
+
const fileVariable: AIVariableResolutionRequest = {
|
|
97
|
+
variable: FILE_VARIABLE,
|
|
98
|
+
arg: wsRelativePath
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Add the file to the active session's context
|
|
102
|
+
activeSession.model.context.addVariables(fileVariable);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create a new session with the coder agent
|
|
106
|
+
const newSession = this.chatService.createSession(ChatAgentLocation.Panel, { focus: true }, this.coderAgent);
|
|
107
|
+
const summaryVariable = { variable: TASK_CONTEXT_VARIABLE, arg: summaryId };
|
|
108
|
+
newSession.model.context.addVariables(summaryVariable);
|
|
109
|
+
}
|
|
54
110
|
}
|
|
55
|
-
});
|
|
111
|
+
}));
|
|
56
112
|
}
|
|
57
113
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { Summary, SummaryMetadata, TaskContextStorageService } from '@theia/ai-chat/lib/browser/task-context-service';
|
|
18
18
|
import { InMemoryTaskContextStorage } from '@theia/ai-chat/lib/browser/task-context-storage-service';
|
|
19
19
|
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
20
|
-
import { DisposableCollection, EOL, Emitter, Path, URI, unreachable } from '@theia/core';
|
|
20
|
+
import { DisposableCollection, EOL, Emitter, ILogger, Path, URI, unreachable } from '@theia/core';
|
|
21
21
|
import { PreferenceService, OpenerService, open } from '@theia/core/lib/browser';
|
|
22
22
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
23
23
|
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
@@ -33,10 +33,16 @@ export class TaskContextFileStorageService implements TaskContextStorageService
|
|
|
33
33
|
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
|
|
34
34
|
@inject(FileService) protected readonly fileService: FileService;
|
|
35
35
|
@inject(OpenerService) protected readonly openerService: OpenerService;
|
|
36
|
+
@inject(ILogger) protected readonly logger: ILogger;
|
|
36
37
|
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
37
38
|
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
38
39
|
|
|
39
|
-
protected
|
|
40
|
+
protected sanitizeLabel(label: string): string {
|
|
41
|
+
return label.replace(/^[^\p{L}\p{N}]+/vg, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected async getStorageLocation(): Promise<URI | undefined> {
|
|
45
|
+
await this.workspaceService.ready;
|
|
40
46
|
if (!this.workspaceService.opened) { return; }
|
|
41
47
|
const values = this.preferenceService.inspect(TASK_CONTEXT_STORAGE_DIRECTORY_PREF);
|
|
42
48
|
const configuredPath = values?.globalValue === undefined ? values?.defaultValue : values?.globalValue;
|
|
@@ -47,9 +53,11 @@ export class TaskContextFileStorageService implements TaskContextStorageService
|
|
|
47
53
|
|
|
48
54
|
@postConstruct()
|
|
49
55
|
protected init(): void {
|
|
50
|
-
this.watchStorage();
|
|
56
|
+
this.watchStorage().catch(error => this.logger.error(error));
|
|
51
57
|
this.preferenceService.onPreferenceChanged(e => {
|
|
52
|
-
if (e.affects(TASK_CONTEXT_STORAGE_DIRECTORY_PREF)) {
|
|
58
|
+
if (e.affects(TASK_CONTEXT_STORAGE_DIRECTORY_PREF)) {
|
|
59
|
+
this.watchStorage().catch(error => this.logger.error(error));
|
|
60
|
+
}
|
|
53
61
|
});
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -57,7 +65,7 @@ export class TaskContextFileStorageService implements TaskContextStorageService
|
|
|
57
65
|
protected async watchStorage(): Promise<void> {
|
|
58
66
|
this.toDisposeOnStorageChange?.dispose();
|
|
59
67
|
this.toDisposeOnStorageChange = undefined;
|
|
60
|
-
const newStorage = this.getStorageLocation();
|
|
68
|
+
const newStorage = await this.getStorageLocation();
|
|
61
69
|
if (!newStorage) { return; }
|
|
62
70
|
this.toDisposeOnStorageChange = new DisposableCollection(
|
|
63
71
|
this.fileService.watch(newStorage, { recursive: true, excludes: [] }),
|
|
@@ -109,10 +117,11 @@ export class TaskContextFileStorageService implements TaskContextStorageService
|
|
|
109
117
|
const content = await this.fileService.read(uri).then(read => read.value).catch(() => undefined);
|
|
110
118
|
if (content === undefined) { return; }
|
|
111
119
|
const { frontmatter, body } = this.maybeReadFrontmatter(content);
|
|
120
|
+
const rawLabel = frontmatter?.label || uri.path.base.slice(0, (-1 * uri.path.ext.length) || uri.path.base.length);
|
|
112
121
|
const summary = {
|
|
113
122
|
...frontmatter,
|
|
114
123
|
summary: body,
|
|
115
|
-
label:
|
|
124
|
+
label: this.sanitizeLabel(rawLabel),
|
|
116
125
|
uri,
|
|
117
126
|
id: frontmatter?.sessionId || uri.path.base
|
|
118
127
|
};
|
|
@@ -124,21 +133,22 @@ export class TaskContextFileStorageService implements TaskContextStorageService
|
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
async store(summary: Summary): Promise<void> {
|
|
127
|
-
const
|
|
136
|
+
const label = this.sanitizeLabel(summary.label);
|
|
137
|
+
const storageLocation = await this.getStorageLocation();
|
|
128
138
|
if (storageLocation) {
|
|
129
139
|
const frontmatter = {
|
|
130
140
|
sessionId: summary.sessionId,
|
|
131
141
|
date: new Date().toISOString(),
|
|
132
|
-
label
|
|
142
|
+
label,
|
|
133
143
|
};
|
|
134
|
-
const derivedName =
|
|
144
|
+
const derivedName = label.trim().replace(/[^\p{L}\p{N}]/vg, '-').replace(/^-+|-+$/g, '');
|
|
135
145
|
const filename = (derivedName.length > 32 ? derivedName.slice(0, derivedName.indexOf('-', 32)) : derivedName) + '.md';
|
|
136
146
|
const content = yaml.dump(frontmatter).trim() + `${EOL}---${EOL}` + summary.summary;
|
|
137
147
|
const uri = storageLocation.resolve(filename);
|
|
138
148
|
summary.uri = uri;
|
|
139
149
|
await this.fileService.writeFile(uri, BinaryBuffer.fromString(content));
|
|
140
150
|
}
|
|
141
|
-
this.inMemoryStorage.store(summary);
|
|
151
|
+
this.inMemoryStorage.store({ ...summary, label });
|
|
142
152
|
this.onDidChangeEmitter.fire();
|
|
143
153
|
}
|
|
144
154
|
|
|
@@ -19,6 +19,7 @@ import { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference
|
|
|
19
19
|
|
|
20
20
|
export const CONSIDER_GITIGNORE_PREF = 'ai-features.workspaceFunctions.considerGitIgnore';
|
|
21
21
|
export const USER_EXCLUDE_PATTERN_PREF = 'ai-features.workspaceFunctions.userExcludes';
|
|
22
|
+
export const SEARCH_IN_WORKSPACE_MAX_RESULTS_PREF = 'ai-features.workspaceFunctions.searchMaxResults';
|
|
22
23
|
export const PROMPT_TEMPLATE_WORKSPACE_DIRECTORIES_PREF = 'ai-features.promptTemplates.WorkspaceTemplateDirectories';
|
|
23
24
|
export const PROMPT_TEMPLATE_ADDITIONAL_EXTENSIONS_PREF = 'ai-features.promptTemplates.TemplateExtensions';
|
|
24
25
|
export const PROMPT_TEMPLATE_WORKSPACE_FILES_PREF = 'ai-features.promptTemplates.WorkspaceTemplateFiles';
|
|
@@ -46,6 +47,14 @@ export const WorkspacePreferencesSchema: PreferenceSchema = {
|
|
|
46
47
|
type: 'string'
|
|
47
48
|
}
|
|
48
49
|
},
|
|
50
|
+
[SEARCH_IN_WORKSPACE_MAX_RESULTS_PREF]: {
|
|
51
|
+
type: 'number',
|
|
52
|
+
title: nls.localize('theia/ai/workspace/searchMaxResults/title', 'Maximum Search Results'),
|
|
53
|
+
description: nls.localize('theia/ai/workspace/searchMaxResults/description',
|
|
54
|
+
'Maximum number of search results returned by the workspace search function.'),
|
|
55
|
+
default: 30,
|
|
56
|
+
minimum: 1
|
|
57
|
+
},
|
|
49
58
|
[PROMPT_TEMPLATE_WORKSPACE_DIRECTORIES_PREF]: {
|
|
50
59
|
type: 'array',
|
|
51
60
|
title: nls.localize('theia/ai/promptTemplates/directories/title', 'Workspace-specific Prompt Template Directories'),
|
|
@@ -0,0 +1,255 @@
|
|
|
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
|
+
|
|
17
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
18
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { expect } from 'chai';
|
|
23
|
+
import { URI } from '@theia/core';
|
|
24
|
+
import { SearchInWorkspaceResult, LinePreview } from '@theia/search-in-workspace/lib/common/search-in-workspace-interface';
|
|
25
|
+
import { optimizeSearchResults } from '../common/workspace-search-provider-util';
|
|
26
|
+
|
|
27
|
+
disableJSDOM();
|
|
28
|
+
|
|
29
|
+
describe('WorkspaceSearchProvider - Token Optimization', () => {
|
|
30
|
+
|
|
31
|
+
before(() => {
|
|
32
|
+
disableJSDOM = enableJSDOM();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
after(() => {
|
|
36
|
+
disableJSDOM();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('optimizeSearchResults method', () => {
|
|
40
|
+
it('should preserve all information while optimizing format', () => {
|
|
41
|
+
const workspaceRoot = new URI('file:///workspace');
|
|
42
|
+
const mockResults: SearchInWorkspaceResult[] = [
|
|
43
|
+
{
|
|
44
|
+
root: 'file:///workspace',
|
|
45
|
+
fileUri: 'file:///workspace/src/test.ts',
|
|
46
|
+
matches: [
|
|
47
|
+
{
|
|
48
|
+
line: 1,
|
|
49
|
+
character: 5,
|
|
50
|
+
length: 8,
|
|
51
|
+
lineText: ' const test = "hello"; '
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
line: 5,
|
|
55
|
+
character: 10,
|
|
56
|
+
length: 4,
|
|
57
|
+
lineText: '\t\tfunction test() { }\n'
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
root: 'file:///workspace',
|
|
63
|
+
fileUri: 'file:///workspace/lib/utils.js',
|
|
64
|
+
matches: [
|
|
65
|
+
{
|
|
66
|
+
line: 10,
|
|
67
|
+
character: 0,
|
|
68
|
+
length: 6,
|
|
69
|
+
lineText: 'export default function() {}'
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const result = optimizeSearchResults(mockResults, workspaceRoot);
|
|
76
|
+
|
|
77
|
+
expect(result).to.have.length(2);
|
|
78
|
+
|
|
79
|
+
// First file
|
|
80
|
+
expect(result[0]).to.deep.equal({
|
|
81
|
+
file: 'src/test.ts',
|
|
82
|
+
matches: [
|
|
83
|
+
{
|
|
84
|
+
line: 1,
|
|
85
|
+
text: 'const test = "hello";'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
line: 5,
|
|
89
|
+
text: 'function test() { }'
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Second file
|
|
95
|
+
expect(result[1]).to.deep.equal({
|
|
96
|
+
file: 'lib/utils.js',
|
|
97
|
+
matches: [
|
|
98
|
+
{
|
|
99
|
+
line: 10,
|
|
100
|
+
text: 'export default function() {}'
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle LinePreview objects correctly', () => {
|
|
107
|
+
const workspaceRoot = new URI('file:///workspace');
|
|
108
|
+
const linePreview: LinePreview = {
|
|
109
|
+
text: ' preview text with spaces ',
|
|
110
|
+
character: 5
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const mockResults: SearchInWorkspaceResult[] = [
|
|
114
|
+
{
|
|
115
|
+
root: 'file:///workspace',
|
|
116
|
+
fileUri: 'file:///workspace/preview.ts',
|
|
117
|
+
matches: [
|
|
118
|
+
{
|
|
119
|
+
line: 3,
|
|
120
|
+
character: 5,
|
|
121
|
+
length: 7,
|
|
122
|
+
lineText: linePreview
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const result = optimizeSearchResults(mockResults, workspaceRoot);
|
|
129
|
+
|
|
130
|
+
expect(result[0].matches[0]).to.deep.equal({
|
|
131
|
+
line: 3,
|
|
132
|
+
text: 'preview text with spaces'
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle empty LinePreview text gracefully', () => {
|
|
137
|
+
const workspaceRoot = new URI('file:///workspace');
|
|
138
|
+
const linePreview: LinePreview = {
|
|
139
|
+
text: '',
|
|
140
|
+
character: 0
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const mockResults: SearchInWorkspaceResult[] = [
|
|
144
|
+
{
|
|
145
|
+
root: 'file:///workspace',
|
|
146
|
+
fileUri: 'file:///workspace/empty.ts',
|
|
147
|
+
matches: [
|
|
148
|
+
{
|
|
149
|
+
line: 1,
|
|
150
|
+
character: 0,
|
|
151
|
+
length: 0,
|
|
152
|
+
lineText: linePreview
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const result = optimizeSearchResults(mockResults, workspaceRoot);
|
|
159
|
+
|
|
160
|
+
expect(result[0].matches[0]).to.deep.equal({
|
|
161
|
+
line: 1,
|
|
162
|
+
text: ''
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should preserve semantic whitespace within lines', () => {
|
|
167
|
+
const workspaceRoot = new URI('file:///workspace');
|
|
168
|
+
const mockResults: SearchInWorkspaceResult[] = [
|
|
169
|
+
{
|
|
170
|
+
root: 'file:///workspace',
|
|
171
|
+
fileUri: 'file:///workspace/spaces.ts',
|
|
172
|
+
matches: [
|
|
173
|
+
{
|
|
174
|
+
line: 1,
|
|
175
|
+
character: 0,
|
|
176
|
+
length: 20,
|
|
177
|
+
lineText: ' if (a && b) { '
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const result = optimizeSearchResults(mockResults, workspaceRoot);
|
|
184
|
+
|
|
185
|
+
expect(result[0].matches[0].text).to.equal('if (a && b) {');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should use absolute URI when relative path cannot be determined', () => {
|
|
189
|
+
const workspaceRoot = new URI('file:///different-workspace');
|
|
190
|
+
const mockResults: SearchInWorkspaceResult[] = [
|
|
191
|
+
{
|
|
192
|
+
root: 'file:///workspace',
|
|
193
|
+
fileUri: 'file:///workspace/outside.ts',
|
|
194
|
+
matches: [
|
|
195
|
+
{
|
|
196
|
+
line: 1,
|
|
197
|
+
character: 0,
|
|
198
|
+
length: 4,
|
|
199
|
+
lineText: 'test'
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const result = optimizeSearchResults(mockResults, workspaceRoot);
|
|
206
|
+
|
|
207
|
+
expect(result[0].file).to.equal('file:///workspace/outside.ts');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('token efficiency validation', () => {
|
|
212
|
+
it('should produce more compact JSON than original format', () => {
|
|
213
|
+
const workspaceRoot = new URI('file:///workspace');
|
|
214
|
+
const mockResults: SearchInWorkspaceResult[] = [
|
|
215
|
+
{
|
|
216
|
+
root: 'file:///workspace',
|
|
217
|
+
fileUri: 'file:///workspace/src/test.ts',
|
|
218
|
+
matches: [
|
|
219
|
+
{
|
|
220
|
+
line: 1,
|
|
221
|
+
character: 5,
|
|
222
|
+
length: 8,
|
|
223
|
+
lineText: ' const test = "hello"; '
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
// Original format (simulated)
|
|
230
|
+
const originalFormat = JSON.stringify([{
|
|
231
|
+
root: 'file:///workspace',
|
|
232
|
+
fileUri: 'file:///workspace/src/test.ts',
|
|
233
|
+
matches: [{
|
|
234
|
+
line: 1,
|
|
235
|
+
character: 5,
|
|
236
|
+
length: 8,
|
|
237
|
+
lineText: ' const test = "hello"; '
|
|
238
|
+
}]
|
|
239
|
+
}]);
|
|
240
|
+
|
|
241
|
+
// Optimized format
|
|
242
|
+
const optimizedResults = optimizeSearchResults(mockResults, workspaceRoot);
|
|
243
|
+
const optimizedFormat = JSON.stringify(optimizedResults);
|
|
244
|
+
|
|
245
|
+
// The optimized format should be significantly shorter
|
|
246
|
+
expect(optimizedFormat.length).to.be.lessThan(originalFormat.length);
|
|
247
|
+
|
|
248
|
+
// But should preserve essential information
|
|
249
|
+
const parsed = JSON.parse(optimizedFormat);
|
|
250
|
+
expect(parsed[0].file).to.equal('src/test.ts');
|
|
251
|
+
expect(parsed[0].matches[0].line).to.equal(1);
|
|
252
|
+
expect(parsed[0].matches[0].text).to.equal('const test = "hello";');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -16,12 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import { MutableChatRequestModel } from '@theia/ai-chat';
|
|
18
18
|
import { ToolProvider, ToolRequest } from '@theia/ai-core';
|
|
19
|
-
import { CancellationToken
|
|
19
|
+
import { CancellationToken } from '@theia/core';
|
|
20
|
+
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
|
|
20
21
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
22
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
21
23
|
import { SearchInWorkspaceService, SearchInWorkspaceCallbacks } from '@theia/search-in-workspace/lib/browser/search-in-workspace-service';
|
|
22
24
|
import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from '@theia/search-in-workspace/lib/common/search-in-workspace-interface';
|
|
23
25
|
import { SEARCH_IN_WORKSPACE_FUNCTION_ID } from '../common/workspace-functions';
|
|
24
26
|
import { WorkspaceFunctionScope } from './workspace-functions';
|
|
27
|
+
import { SEARCH_IN_WORKSPACE_MAX_RESULTS_PREF } from './workspace-preferences';
|
|
28
|
+
import { optimizeSearchResults } from '../common/workspace-search-provider-util';
|
|
25
29
|
|
|
26
30
|
@injectable()
|
|
27
31
|
export class WorkspaceSearchProvider implements ToolProvider {
|
|
@@ -32,7 +36,11 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
32
36
|
@inject(WorkspaceFunctionScope)
|
|
33
37
|
protected readonly workspaceScope: WorkspaceFunctionScope;
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
@inject(PreferenceService)
|
|
40
|
+
protected readonly preferenceService: PreferenceService;
|
|
41
|
+
|
|
42
|
+
@inject(FileService)
|
|
43
|
+
protected readonly fileService: FileService;
|
|
36
44
|
|
|
37
45
|
getTool(): ToolRequest {
|
|
38
46
|
return {
|
|
@@ -42,7 +50,7 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
42
50
|
The search uses case-insensitive string matching or regular expressions (controlled by the `useRegExp` parameter). \
|
|
43
51
|
It returns a list of matching files, including the file path (URI), the line number, and the full text content of each matching line. \
|
|
44
52
|
Multi-word patterns must match exactly (including spaces, case-insensitively). \
|
|
45
|
-
For best results, use specific search terms and consider filtering by file extensions to avoid overwhelming results. \
|
|
53
|
+
For best results, use specific search terms and consider filtering by file extensions or limiting to specific subdirectories to avoid overwhelming results. \
|
|
46
54
|
For complex searches, prefer multiple simpler queries over one complex query or regular expression.',
|
|
47
55
|
parameters: {
|
|
48
56
|
type: 'object',
|
|
@@ -61,6 +69,11 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
61
69
|
type: 'string'
|
|
62
70
|
},
|
|
63
71
|
description: 'Optional array of file extensions to search in (e.g., ["ts", "js", "py"]). If not specified, searches all files.'
|
|
72
|
+
},
|
|
73
|
+
subDirectoryPath: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Optional subdirectory path to limit search scope. Use relative paths from workspace root ' +
|
|
76
|
+
'(e.g., "packages/ai-ide/src", "packages/core/src/browser"). If not specified, searches entire workspace.'
|
|
64
77
|
}
|
|
65
78
|
},
|
|
66
79
|
required: ['query', 'useRegExp']
|
|
@@ -69,14 +82,36 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
69
82
|
};
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
private async determineSearchRoots(subDirectoryPath?: string): Promise<string[]> {
|
|
86
|
+
const workspaceRoot = await this.workspaceScope.getWorkspaceRoot();
|
|
87
|
+
|
|
88
|
+
if (!subDirectoryPath) {
|
|
89
|
+
return [workspaceRoot.toString()];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const subDirUri = workspaceRoot.resolve(subDirectoryPath);
|
|
93
|
+
this.workspaceScope.ensureWithinWorkspace(subDirUri, workspaceRoot);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const stat = await this.fileService.resolve(subDirUri);
|
|
97
|
+
if (!stat || !stat.isDirectory) {
|
|
98
|
+
throw new Error(`Subdirectory '${subDirectoryPath}' does not exist or is not a directory`);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Invalid subdirectory path '${subDirectoryPath}': ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [subDirUri.toString()];
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
private async handleSearch(argString: string, cancellationToken?: CancellationToken): Promise<string> {
|
|
73
108
|
try {
|
|
74
|
-
const args: { query: string, useRegExp: boolean, fileExtensions?: string[] } = JSON.parse(argString);
|
|
109
|
+
const args: { query: string, useRegExp: boolean, fileExtensions?: string[], subDirectoryPath?: string } = JSON.parse(argString);
|
|
75
110
|
const results: SearchInWorkspaceResult[] = [];
|
|
76
111
|
let expectedSearchId: number | undefined;
|
|
77
112
|
let searchCompleted = false;
|
|
78
113
|
|
|
79
|
-
const searchPromise = new Promise<SearchInWorkspaceResult[]>((resolve, reject) => {
|
|
114
|
+
const searchPromise = new Promise<SearchInWorkspaceResult[]>(async (resolve, reject) => {
|
|
80
115
|
const callbacks: SearchInWorkspaceCallbacks = {
|
|
81
116
|
onResult: (id, result) => {
|
|
82
117
|
if (expectedSearchId !== undefined && id !== expectedSearchId) {
|
|
@@ -100,25 +135,28 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
100
135
|
|
|
101
136
|
searchCompleted = true;
|
|
102
137
|
if (error) {
|
|
103
|
-
reject(new Error(
|
|
138
|
+
reject(new Error('Search failed: ' + error));
|
|
104
139
|
} else {
|
|
105
140
|
resolve(results);
|
|
106
141
|
}
|
|
107
142
|
}
|
|
108
143
|
};
|
|
109
144
|
|
|
145
|
+
// Use one more than our actual maximum. this way we can determine if we have more results than our maximum and warn the user
|
|
146
|
+
const maxResultsForTheiaAPI = this.preferenceService.get<number>(SEARCH_IN_WORKSPACE_MAX_RESULTS_PREF, 30) + 1;
|
|
110
147
|
const options: SearchInWorkspaceOptions = {
|
|
111
148
|
useRegExp: args.useRegExp,
|
|
112
149
|
matchCase: false,
|
|
113
150
|
matchWholeWord: false,
|
|
114
|
-
maxResults:
|
|
151
|
+
maxResults: maxResultsForTheiaAPI,
|
|
115
152
|
};
|
|
116
153
|
|
|
117
154
|
if (args.fileExtensions && args.fileExtensions.length > 0) {
|
|
118
155
|
options.include = args.fileExtensions.map(ext => `**/*.${ext}`);
|
|
119
156
|
}
|
|
120
157
|
|
|
121
|
-
this.
|
|
158
|
+
await this.determineSearchRoots(args.subDirectoryPath)
|
|
159
|
+
.then(rootUris => this.searchService.searchWithCallback(args.query, rootUris, callbacks, options))
|
|
122
160
|
.then(id => {
|
|
123
161
|
expectedSearchId = id;
|
|
124
162
|
cancellationToken?.onCancellationRequested(() => {
|
|
@@ -143,16 +181,23 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
143
181
|
});
|
|
144
182
|
|
|
145
183
|
const finalResults = await Promise.race([searchPromise, timeoutPromise]);
|
|
184
|
+
const maxResults = this.preferenceService.get<number>(SEARCH_IN_WORKSPACE_MAX_RESULTS_PREF, 30);
|
|
146
185
|
|
|
147
186
|
const workspaceRoot = await this.workspaceScope.getWorkspaceRoot();
|
|
148
|
-
const formattedResults = finalResults
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
187
|
+
const formattedResults = optimizeSearchResults(finalResults, workspaceRoot);
|
|
188
|
+
|
|
189
|
+
let numberOfMatchesInFinalResults = 0;
|
|
190
|
+
for (const result of finalResults) {
|
|
191
|
+
numberOfMatchesInFinalResults += result.matches.length;
|
|
192
|
+
}
|
|
193
|
+
if (numberOfMatchesInFinalResults > maxResults) {
|
|
194
|
+
return JSON.stringify({
|
|
195
|
+
info: 'Search limit exceeded: Found ' + maxResults + '+ results. ' +
|
|
196
|
+
'Please refine your search with more specific terms or use file extension filters. ' +
|
|
197
|
+
'You can increase the limit in preferences under \'ai-features.workspaceFunctions.searchMaxResults\'.',
|
|
198
|
+
incompleteResults: formattedResults
|
|
199
|
+
});
|
|
200
|
+
}
|
|
156
201
|
|
|
157
202
|
return JSON.stringify(formattedResults);
|
|
158
203
|
|
|
@@ -161,3 +206,4 @@ export class WorkspaceSearchProvider implements ToolProvider {
|
|
|
161
206
|
}
|
|
162
207
|
}
|
|
163
208
|
}
|
|
209
|
+
|