@lobehub/chat 1.141.7 → 1.141.9
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/CHANGELOG.md +50 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +279 -52
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +392 -0
- package/changelog/v1.json +18 -0
- package/docs/usage/features/{group-chat.mdx → agent-team.mdx} +14 -14
- package/docs/usage/features/agent-team.zh-CN.mdx +52 -0
- package/locales/ar/chat.json +17 -17
- package/locales/ar/setting.json +15 -19
- package/locales/ar/welcome.json +1 -1
- package/locales/bg-BG/chat.json +17 -17
- package/locales/bg-BG/setting.json +15 -19
- package/locales/de-DE/chat.json +17 -17
- package/locales/de-DE/setting.json +15 -19
- package/locales/de-DE/welcome.json +1 -1
- package/locales/en-US/chat.json +17 -17
- package/locales/en-US/setting.json +15 -19
- package/locales/en-US/welcome.json +1 -1
- package/locales/es-ES/chat.json +17 -17
- package/locales/es-ES/setting.json +15 -19
- package/locales/es-ES/welcome.json +1 -1
- package/locales/fa-IR/chat.json +17 -17
- package/locales/fa-IR/setting.json +15 -19
- package/locales/fa-IR/welcome.json +1 -1
- package/locales/fr-FR/chat.json +16 -16
- package/locales/fr-FR/setting.json +15 -19
- package/locales/fr-FR/welcome.json +1 -1
- package/locales/it-IT/chat.json +17 -17
- package/locales/it-IT/setting.json +15 -19
- package/locales/it-IT/welcome.json +1 -1
- package/locales/ja-JP/chat.json +17 -17
- package/locales/ja-JP/setting.json +15 -19
- package/locales/ja-JP/welcome.json +1 -1
- package/locales/ko-KR/chat.json +17 -17
- package/locales/ko-KR/setting.json +15 -19
- package/locales/ko-KR/welcome.json +1 -1
- package/locales/nl-NL/chat.json +17 -17
- package/locales/nl-NL/setting.json +15 -19
- package/locales/nl-NL/welcome.json +1 -1
- package/locales/pl-PL/chat.json +17 -17
- package/locales/pl-PL/setting.json +15 -19
- package/locales/pt-BR/chat.json +17 -17
- package/locales/pt-BR/setting.json +15 -19
- package/locales/pt-BR/welcome.json +1 -1
- package/locales/ru-RU/chat.json +17 -17
- package/locales/ru-RU/setting.json +15 -19
- package/locales/ru-RU/welcome.json +1 -1
- package/locales/tr-TR/chat.json +17 -17
- package/locales/tr-TR/setting.json +15 -19
- package/locales/vi-VN/chat.json +15 -15
- package/locales/vi-VN/setting.json +15 -19
- package/locales/zh-CN/chat.json +17 -17
- package/locales/zh-CN/setting.json +15 -19
- package/locales/zh-CN/welcome.json +1 -1
- package/locales/zh-TW/chat.json +17 -17
- package/locales/zh-TW/setting.json +15 -19
- package/locales/zh-TW/welcome.json +1 -1
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/InterventionChecker.ts +173 -0
- package/packages/agent-runtime/src/core/UsageCounter.ts +248 -0
- package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +334 -0
- package/packages/agent-runtime/src/core/__tests__/UsageCounter.test.ts +873 -0
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +32 -26
- package/packages/agent-runtime/src/core/index.ts +2 -0
- package/packages/agent-runtime/src/core/runtime.ts +31 -18
- package/packages/agent-runtime/src/types/instruction.ts +1 -1
- package/packages/agent-runtime/src/types/state.ts +3 -3
- package/packages/agent-runtime/src/types/usage.ts +34 -25
- package/packages/const/src/settings/systemAgent.ts +0 -1
- package/packages/context-engine/src/index.ts +1 -0
- package/packages/context-engine/src/tools/ToolNameResolver.ts +2 -2
- package/packages/context-engine/src/tools/ToolsEngine.ts +37 -8
- package/packages/context-engine/src/tools/__tests__/ToolsEngine.test.ts +149 -5
- package/packages/context-engine/src/tools/__tests__/utils.test.ts +2 -2
- package/packages/context-engine/src/tools/index.ts +1 -0
- package/packages/context-engine/src/tools/types.ts +18 -3
- package/packages/context-engine/src/tools/utils.ts +4 -4
- package/packages/types/src/tool/builtin.ts +54 -1
- package/packages/types/src/tool/index.ts +1 -0
- package/packages/types/src/tool/intervention.ts +114 -0
- package/packages/types/src/user/settings/systemAgent.ts +0 -1
- package/packages/types/src/user/settings/tool.ts +37 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/OrchestratorThinking.tsx +2 -3
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/GroupConfig/GroupMember.tsx +34 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +1 -1
- package/src/app/[variants]/(main)/chat/(workspace)/features/{GroupChatSettings → AgentTeamSettings}/index.tsx +4 -5
- package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +2 -2
- package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +2 -0
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/CollapseGroup/Actions.tsx +18 -1
- package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +33 -5
- package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +170 -26
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +7 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +4 -2
- package/src/features/Conversation/Messages/User/Actions.tsx +8 -2
- package/src/features/GroupChatSettings/{ChatGroupSettings.tsx → AgentTeamChatSettings.tsx} +6 -5
- package/src/features/GroupChatSettings/{GroupMembers.tsx → AgentTeamMembersSettings.tsx} +64 -19
- package/src/features/GroupChatSettings/{ChatGroupMeta.tsx → AgentTeamMetaSettings.tsx} +2 -2
- package/src/features/GroupChatSettings/AgentTeamSettings.tsx +54 -0
- package/src/features/GroupChatSettings/index.ts +4 -5
- package/src/locales/default/chat.ts +17 -17
- package/src/locales/default/setting.ts +15 -19
- package/src/locales/default/welcome.ts +1 -1
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +2 -1
- package/src/store/chat/slices/builtinTool/actions/{dalle.test.ts → __tests__/dalle.test.ts} +2 -5
- package/src/store/chat/slices/builtinTool/actions/__tests__/{localFile.test.ts → localSystem.test.ts} +4 -4
- package/src/store/chat/slices/builtinTool/actions/index.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/{localFile.ts → localSystem.ts} +183 -69
- package/src/store/chatGroup/action.ts +36 -1
- package/src/store/electron/selectors/__tests__/desktopState.test.ts +3 -3
- package/src/store/electron/selectors/desktopState.ts +11 -2
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +0 -4
- package/src/store/user/slices/settings/selectors/systemAgent.ts +0 -2
- package/src/tools/local-system/Placeholder/ListFiles.tsx +10 -8
- package/src/tools/local-system/Placeholder/SearchFiles.tsx +12 -10
- package/src/tools/local-system/Placeholder/index.tsx +1 -1
- package/src/tools/local-system/Render/ReadLocalFile/ReadFileSkeleton.tsx +8 -18
- package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +21 -6
- package/src/tools/local-system/Render/SearchFiles/Result.tsx +5 -4
- package/src/tools/local-system/Render/SearchFiles/SearchQuery/SearchView.tsx +4 -15
- package/src/tools/local-system/Render/SearchFiles/index.tsx +3 -2
- package/src/tools/local-system/type.ts +39 -0
- package/docs/usage/features/group-chat.zh-CN.mdx +0 -52
- package/src/features/GroupChatSettings/GroupSettings.tsx +0 -30
- package/src/features/GroupChatSettings/GroupSettingsContent.tsx +0 -24
- package/src/tools/local-system/Placeholder/ReadLocalFile.tsx +0 -9
- package/src/tools/local-system/Render/ReadLocalFile/style.ts +0 -37
- /package/src/store/chat/slices/builtinTool/actions/{search.test.ts → __tests__/search.test.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.141.9](https://github.com/lobehub/lobe-chat/compare/v1.141.8...v1.141.9)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-10-23**</sup>
|
|
8
|
+
|
|
9
|
+
#### 💄 Styles
|
|
10
|
+
|
|
11
|
+
- **misc**: Improve local system tools render.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Styles
|
|
19
|
+
|
|
20
|
+
- **misc**: Improve local system tools render, closes [#9853](https://github.com/lobehub/lobe-chat/issues/9853) ([295e8fc](https://github.com/lobehub/lobe-chat/commit/295e8fc))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 1.141.8](https://github.com/lobehub/lobe-chat/compare/v1.141.7...v1.141.8)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2025-10-23**</sup>
|
|
33
|
+
|
|
34
|
+
#### 💄 Styles
|
|
35
|
+
|
|
36
|
+
- **misc**: Improvement for Agent Team After Alpha Launch \[LOB-517].
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### Styles
|
|
44
|
+
|
|
45
|
+
- **misc**: Improvement for Agent Team After Alpha Launch \[LOB-517], closes [#9748](https://github.com/lobehub/lobe-chat/issues/9748) ([28245be](https://github.com/lobehub/lobe-chat/commit/28245be))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
### [Version 1.141.7](https://github.com/lobehub/lobe-chat/compare/v1.141.6...v1.141.7)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2025-10-23**</sup>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
EditLocalFileParams,
|
|
3
|
+
EditLocalFileResult,
|
|
4
|
+
GlobFilesParams,
|
|
5
|
+
GlobFilesResult,
|
|
6
|
+
GrepContentParams,
|
|
7
|
+
GrepContentResult,
|
|
2
8
|
ListLocalFileParams,
|
|
3
9
|
LocalMoveFilesResultItem,
|
|
4
10
|
LocalReadFileParams,
|
|
@@ -13,10 +19,10 @@ import {
|
|
|
13
19
|
} from '@lobechat/electron-client-ipc';
|
|
14
20
|
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
|
|
15
21
|
import { shell } from 'electron';
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
22
|
+
import fg from 'fast-glob';
|
|
23
|
+
import { Stats, constants } from 'node:fs';
|
|
24
|
+
import { access, mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
|
|
18
25
|
import * as path from 'node:path';
|
|
19
|
-
import { promisify } from 'node:util';
|
|
20
26
|
|
|
21
27
|
import FileSearchService from '@/services/fileSearchSrv';
|
|
22
28
|
import { FileResult, SearchOptions } from '@/types/fileSearch';
|
|
@@ -25,40 +31,15 @@ import { createLogger } from '@/utils/logger';
|
|
|
25
31
|
|
|
26
32
|
import { ControllerModule, ipcClientEvent } from './index';
|
|
27
33
|
|
|
28
|
-
//
|
|
34
|
+
// Create logger
|
|
29
35
|
const logger = createLogger('controllers:LocalFileCtr');
|
|
30
36
|
|
|
31
|
-
const statPromise = promisify(fs.stat);
|
|
32
|
-
const readdirPromise = promisify(fs.readdir);
|
|
33
|
-
const renamePromiseFs = promisify(fs.rename);
|
|
34
|
-
const accessPromise = promisify(fs.access);
|
|
35
|
-
const writeFilePromise = promisify(fs.writeFile);
|
|
36
|
-
|
|
37
37
|
export default class LocalFileCtr extends ControllerModule {
|
|
38
38
|
private get searchService() {
|
|
39
39
|
return this.app.getService(FileSearchService);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
* Handle IPC event for local file search
|
|
44
|
-
*/
|
|
45
|
-
@ipcClientEvent('searchLocalFiles')
|
|
46
|
-
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
|
|
47
|
-
logger.debug('Received file search request:', { keywords: params.keywords });
|
|
48
|
-
|
|
49
|
-
const options: Omit<SearchOptions, 'keywords'> = {
|
|
50
|
-
limit: 30,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const results = await this.searchService.search(params.keywords, options);
|
|
55
|
-
logger.debug('File search completed', { count: results.length });
|
|
56
|
-
return results;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
logger.error('File search failed:', error);
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
42
|
+
// ==================== File Operation ====================
|
|
62
43
|
|
|
63
44
|
@ipcClientEvent('openLocalFile')
|
|
64
45
|
async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
|
|
@@ -102,7 +83,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
102
83
|
const results: LocalReadFileResult[] = [];
|
|
103
84
|
|
|
104
85
|
for (const filePath of paths) {
|
|
105
|
-
//
|
|
86
|
+
// Initialize result object
|
|
106
87
|
logger.debug('Reading single file:', { filePath });
|
|
107
88
|
const result = await this.readFile({ path: filePath });
|
|
108
89
|
results.push(result);
|
|
@@ -158,7 +139,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
158
139
|
};
|
|
159
140
|
|
|
160
141
|
try {
|
|
161
|
-
const stats = await
|
|
142
|
+
const stats = await stat(filePath);
|
|
162
143
|
if (stats.isDirectory()) {
|
|
163
144
|
logger.warn('Attempted to read directory content:', { filePath });
|
|
164
145
|
result.content = 'This is a directory and cannot be read as plain text.';
|
|
@@ -197,7 +178,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
197
178
|
|
|
198
179
|
const results: FileResult[] = [];
|
|
199
180
|
try {
|
|
200
|
-
const entries = await
|
|
181
|
+
const entries = await readdir(dirPath);
|
|
201
182
|
logger.debug('Directory entries retrieved successfully:', {
|
|
202
183
|
dirPath,
|
|
203
184
|
entriesCount: entries.length,
|
|
@@ -212,7 +193,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
212
193
|
|
|
213
194
|
const fullPath = path.join(dirPath, entry);
|
|
214
195
|
try {
|
|
215
|
-
const stats = await
|
|
196
|
+
const stats = await stat(fullPath);
|
|
216
197
|
const isDirectory = stats.isDirectory();
|
|
217
198
|
results.push({
|
|
218
199
|
createdTime: stats.birthtime,
|
|
@@ -260,7 +241,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
260
241
|
return [];
|
|
261
242
|
}
|
|
262
243
|
|
|
263
|
-
//
|
|
244
|
+
// Process each move request
|
|
264
245
|
for (const item of items) {
|
|
265
246
|
const { oldPath: sourcePath, newPath } = item;
|
|
266
247
|
const logPrefix = `[Moving file ${sourcePath} -> ${newPath}]`;
|
|
@@ -272,7 +253,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
272
253
|
success: false,
|
|
273
254
|
};
|
|
274
255
|
|
|
275
|
-
//
|
|
256
|
+
// Basic validation
|
|
276
257
|
if (!sourcePath || !newPath) {
|
|
277
258
|
logger.error(`${logPrefix} Parameter validation failed: source or target path is empty`);
|
|
278
259
|
resultItem.error = 'Both oldPath and newPath are required for each item.';
|
|
@@ -281,9 +262,9 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
281
262
|
}
|
|
282
263
|
|
|
283
264
|
try {
|
|
284
|
-
//
|
|
265
|
+
// Check if source exists
|
|
285
266
|
try {
|
|
286
|
-
await
|
|
267
|
+
await access(sourcePath, constants.F_OK);
|
|
287
268
|
logger.debug(`${logPrefix} Source file exists`);
|
|
288
269
|
} catch (accessError: any) {
|
|
289
270
|
if (accessError.code === 'ENOENT') {
|
|
@@ -297,28 +278,28 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
297
278
|
}
|
|
298
279
|
}
|
|
299
280
|
|
|
300
|
-
//
|
|
281
|
+
// Check if target path is the same as source path
|
|
301
282
|
if (path.normalize(sourcePath) === path.normalize(newPath)) {
|
|
302
283
|
logger.info(`${logPrefix} Source and target paths are identical, skipping move`);
|
|
303
284
|
resultItem.success = true;
|
|
304
|
-
resultItem.newPath = newPath; //
|
|
285
|
+
resultItem.newPath = newPath; // Report target path even if not moved
|
|
305
286
|
results.push(resultItem);
|
|
306
287
|
continue;
|
|
307
288
|
}
|
|
308
289
|
|
|
309
|
-
// LBYL:
|
|
290
|
+
// LBYL: Ensure target directory exists
|
|
310
291
|
const targetDir = path.dirname(newPath);
|
|
311
292
|
makeSureDirExist(targetDir);
|
|
312
293
|
logger.debug(`${logPrefix} Ensured target directory exists: ${targetDir}`);
|
|
313
294
|
|
|
314
|
-
//
|
|
315
|
-
await
|
|
295
|
+
// Execute move (rename)
|
|
296
|
+
await rename(sourcePath, newPath);
|
|
316
297
|
resultItem.success = true;
|
|
317
298
|
resultItem.newPath = newPath;
|
|
318
299
|
logger.info(`${logPrefix} Move successful`);
|
|
319
300
|
} catch (error) {
|
|
320
301
|
logger.error(`${logPrefix} Move failed:`, error);
|
|
321
|
-
//
|
|
302
|
+
// Use similar error handling logic as handleMoveFile
|
|
322
303
|
let errorMessage = (error as Error).message;
|
|
323
304
|
if ((error as any).code === 'ENOENT')
|
|
324
305
|
errorMessage = `Source path not found: ${sourcePath}.`;
|
|
@@ -334,7 +315,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
334
315
|
errorMessage = `The target directory ${newPath} is not empty (relevant on some systems if target exists and is a directory).`;
|
|
335
316
|
else if ((error as any).code === 'EEXIST')
|
|
336
317
|
errorMessage = `An item already exists at the target path: ${newPath}.`;
|
|
337
|
-
//
|
|
318
|
+
// Keep more specific errors from access or directory checks
|
|
338
319
|
else if (
|
|
339
320
|
!errorMessage.startsWith('Source path not found') &&
|
|
340
321
|
!errorMessage.startsWith('Permission denied accessing source path') &&
|
|
@@ -411,9 +392,9 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
411
392
|
};
|
|
412
393
|
}
|
|
413
394
|
|
|
414
|
-
// Perform the rename operation using
|
|
395
|
+
// Perform the rename operation using rename directly
|
|
415
396
|
try {
|
|
416
|
-
await
|
|
397
|
+
await rename(currentPath, newPath);
|
|
417
398
|
logger.info(`${logPrefix} Rename successful: ${currentPath} -> ${newPath}`);
|
|
418
399
|
// Optionally return the newPath if frontend needs it
|
|
419
400
|
// return { success: true, newPath: newPath };
|
|
@@ -444,7 +425,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
444
425
|
const logPrefix = `[Writing file ${filePath}]`;
|
|
445
426
|
logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
|
|
446
427
|
|
|
447
|
-
//
|
|
428
|
+
// Validate parameters
|
|
448
429
|
if (!filePath) {
|
|
449
430
|
logger.error(`${logPrefix} Parameter validation failed: path is empty`);
|
|
450
431
|
return { error: 'Path cannot be empty', success: false };
|
|
@@ -456,14 +437,14 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
456
437
|
}
|
|
457
438
|
|
|
458
439
|
try {
|
|
459
|
-
//
|
|
440
|
+
// Ensure target directory exists (use async to avoid blocking main thread)
|
|
460
441
|
const dirname = path.dirname(filePath);
|
|
461
442
|
logger.debug(`${logPrefix} Creating directory: ${dirname}`);
|
|
462
|
-
|
|
443
|
+
await mkdir(dirname, { recursive: true });
|
|
463
444
|
|
|
464
|
-
//
|
|
445
|
+
// Write file content
|
|
465
446
|
logger.debug(`${logPrefix} Starting to write content to file`);
|
|
466
|
-
await
|
|
447
|
+
await writeFile(filePath, content, 'utf8');
|
|
467
448
|
logger.info(`${logPrefix} File written successfully`, {
|
|
468
449
|
path: filePath,
|
|
469
450
|
size: content.length,
|
|
@@ -478,4 +459,250 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
478
459
|
};
|
|
479
460
|
}
|
|
480
461
|
}
|
|
462
|
+
|
|
463
|
+
// ==================== Search & Find ====================
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Handle IPC event for local file search
|
|
467
|
+
*/
|
|
468
|
+
@ipcClientEvent('searchLocalFiles')
|
|
469
|
+
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
|
|
470
|
+
logger.debug('Received file search request:', { keywords: params.keywords });
|
|
471
|
+
|
|
472
|
+
const options: Omit<SearchOptions, 'keywords'> = {
|
|
473
|
+
limit: 30,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const results = await this.searchService.search(params.keywords, options);
|
|
478
|
+
logger.debug('File search completed', { count: results.length });
|
|
479
|
+
return results;
|
|
480
|
+
} catch (error) {
|
|
481
|
+
logger.error('File search failed:', error);
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@ipcClientEvent('grepContent')
|
|
487
|
+
async handleGrepContent(params: GrepContentParams): Promise<GrepContentResult> {
|
|
488
|
+
const {
|
|
489
|
+
pattern,
|
|
490
|
+
path: searchPath = process.cwd(),
|
|
491
|
+
output_mode = 'files_with_matches',
|
|
492
|
+
} = params;
|
|
493
|
+
const logPrefix = `[grepContent: ${pattern}]`;
|
|
494
|
+
logger.debug(`${logPrefix} Starting content search`, { output_mode, searchPath });
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const regex = new RegExp(
|
|
498
|
+
pattern,
|
|
499
|
+
`g${params['-i'] ? 'i' : ''}${params.multiline ? 's' : ''}`,
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// Determine files to search
|
|
503
|
+
let filesToSearch: string[] = [];
|
|
504
|
+
const stats = await stat(searchPath);
|
|
505
|
+
|
|
506
|
+
if (stats.isFile()) {
|
|
507
|
+
filesToSearch = [searchPath];
|
|
508
|
+
} else {
|
|
509
|
+
// Use glob pattern if provided, otherwise search all files
|
|
510
|
+
const globPattern = params.glob || '**/*';
|
|
511
|
+
filesToSearch = await fg(globPattern, {
|
|
512
|
+
absolute: true,
|
|
513
|
+
cwd: searchPath,
|
|
514
|
+
dot: true,
|
|
515
|
+
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Filter by type if provided
|
|
519
|
+
if (params.type) {
|
|
520
|
+
const ext = `.${params.type}`;
|
|
521
|
+
filesToSearch = filesToSearch.filter((file) => file.endsWith(ext));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
logger.debug(`${logPrefix} Found ${filesToSearch.length} files to search`);
|
|
526
|
+
|
|
527
|
+
const matches: string[] = [];
|
|
528
|
+
let totalMatches = 0;
|
|
529
|
+
|
|
530
|
+
for (const filePath of filesToSearch) {
|
|
531
|
+
try {
|
|
532
|
+
const fileStats = await stat(filePath);
|
|
533
|
+
if (!fileStats.isFile()) continue;
|
|
534
|
+
|
|
535
|
+
const content = await readFile(filePath, 'utf8');
|
|
536
|
+
const lines = content.split('\n');
|
|
537
|
+
|
|
538
|
+
switch (output_mode) {
|
|
539
|
+
case 'files_with_matches': {
|
|
540
|
+
if (regex.test(content)) {
|
|
541
|
+
matches.push(filePath);
|
|
542
|
+
totalMatches++;
|
|
543
|
+
if (params.head_limit && matches.length >= params.head_limit) break;
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case 'content': {
|
|
548
|
+
const matchedLines: string[] = [];
|
|
549
|
+
for (let i = 0; i < lines.length; i++) {
|
|
550
|
+
if (regex.test(lines[i])) {
|
|
551
|
+
const contextBefore = params['-B'] || params['-C'] || 0;
|
|
552
|
+
const contextAfter = params['-A'] || params['-C'] || 0;
|
|
553
|
+
|
|
554
|
+
const startLine = Math.max(0, i - contextBefore);
|
|
555
|
+
const endLine = Math.min(lines.length - 1, i + contextAfter);
|
|
556
|
+
|
|
557
|
+
for (let j = startLine; j <= endLine; j++) {
|
|
558
|
+
const lineNum = params['-n'] ? `${j + 1}:` : '';
|
|
559
|
+
matchedLines.push(`${filePath}:${lineNum}${lines[j]}`);
|
|
560
|
+
}
|
|
561
|
+
totalMatches++;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
matches.push(...matchedLines);
|
|
565
|
+
if (params.head_limit && matches.length >= params.head_limit) break;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case 'count': {
|
|
569
|
+
const fileMatches = (content.match(regex) || []).length;
|
|
570
|
+
if (fileMatches > 0) {
|
|
571
|
+
matches.push(`${filePath}:${fileMatches}`);
|
|
572
|
+
totalMatches += fileMatches;
|
|
573
|
+
}
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
logger.debug(`${logPrefix} Skipping file ${filePath}:`, error);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
logger.info(`${logPrefix} Search completed`, {
|
|
583
|
+
matchCount: matches.length,
|
|
584
|
+
totalMatches,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
matches: params.head_limit ? matches.slice(0, params.head_limit) : matches,
|
|
589
|
+
success: true,
|
|
590
|
+
total_matches: totalMatches,
|
|
591
|
+
};
|
|
592
|
+
} catch (error) {
|
|
593
|
+
logger.error(`${logPrefix} Grep failed:`, error);
|
|
594
|
+
return {
|
|
595
|
+
matches: [],
|
|
596
|
+
success: false,
|
|
597
|
+
total_matches: 0,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
@ipcClientEvent('globLocalFiles')
|
|
603
|
+
async handleGlobFiles({
|
|
604
|
+
path: searchPath = process.cwd(),
|
|
605
|
+
pattern,
|
|
606
|
+
}: GlobFilesParams): Promise<GlobFilesResult> {
|
|
607
|
+
const logPrefix = `[globFiles: ${pattern}]`;
|
|
608
|
+
logger.debug(`${logPrefix} Starting glob search`, { searchPath });
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const files = await fg(pattern, {
|
|
612
|
+
absolute: true,
|
|
613
|
+
cwd: searchPath,
|
|
614
|
+
dot: true,
|
|
615
|
+
onlyFiles: false,
|
|
616
|
+
stats: true,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Sort by modification time (most recent first)
|
|
620
|
+
const sortedFiles = (files as unknown as Array<{ path: string; stats: Stats }>)
|
|
621
|
+
.sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime())
|
|
622
|
+
.map((f) => f.path);
|
|
623
|
+
|
|
624
|
+
logger.info(`${logPrefix} Glob completed`, { fileCount: sortedFiles.length });
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
files: sortedFiles,
|
|
628
|
+
success: true,
|
|
629
|
+
total_files: sortedFiles.length,
|
|
630
|
+
};
|
|
631
|
+
} catch (error) {
|
|
632
|
+
logger.error(`${logPrefix} Glob failed:`, error);
|
|
633
|
+
return {
|
|
634
|
+
files: [],
|
|
635
|
+
success: false,
|
|
636
|
+
total_files: 0,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ==================== File Editing ====================
|
|
642
|
+
|
|
643
|
+
@ipcClientEvent('editLocalFile')
|
|
644
|
+
async handleEditFile({
|
|
645
|
+
file_path: filePath,
|
|
646
|
+
new_string,
|
|
647
|
+
old_string,
|
|
648
|
+
replace_all = false,
|
|
649
|
+
}: EditLocalFileParams): Promise<EditLocalFileResult> {
|
|
650
|
+
const logPrefix = `[editFile: ${filePath}]`;
|
|
651
|
+
logger.debug(`${logPrefix} Starting file edit`, { replace_all });
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
// Read file content
|
|
655
|
+
const content = await readFile(filePath, 'utf8');
|
|
656
|
+
|
|
657
|
+
// Check if old_string exists
|
|
658
|
+
if (!content.includes(old_string)) {
|
|
659
|
+
logger.error(`${logPrefix} Old string not found in file`);
|
|
660
|
+
return {
|
|
661
|
+
error: 'The specified old_string was not found in the file',
|
|
662
|
+
replacements: 0,
|
|
663
|
+
success: false,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Perform replacement
|
|
668
|
+
let newContent: string;
|
|
669
|
+
let replacements: number;
|
|
670
|
+
|
|
671
|
+
if (replace_all) {
|
|
672
|
+
const regex = new RegExp(old_string.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&'), 'g');
|
|
673
|
+
const matches = content.match(regex);
|
|
674
|
+
replacements = matches ? matches.length : 0;
|
|
675
|
+
newContent = content.replaceAll(old_string, new_string);
|
|
676
|
+
} else {
|
|
677
|
+
// Replace only first occurrence
|
|
678
|
+
const index = content.indexOf(old_string);
|
|
679
|
+
if (index === -1) {
|
|
680
|
+
return {
|
|
681
|
+
error: 'Old string not found',
|
|
682
|
+
replacements: 0,
|
|
683
|
+
success: false,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
newContent =
|
|
687
|
+
content.slice(0, index) + new_string + content.slice(index + old_string.length);
|
|
688
|
+
replacements = 1;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Write back to file
|
|
692
|
+
await writeFile(filePath, newContent, 'utf8');
|
|
693
|
+
|
|
694
|
+
logger.info(`${logPrefix} File edited successfully`, { replacements });
|
|
695
|
+
return {
|
|
696
|
+
replacements,
|
|
697
|
+
success: true,
|
|
698
|
+
};
|
|
699
|
+
} catch (error) {
|
|
700
|
+
logger.error(`${logPrefix} Edit failed:`, error);
|
|
701
|
+
return {
|
|
702
|
+
error: (error as Error).message,
|
|
703
|
+
replacements: 0,
|
|
704
|
+
success: false,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
481
708
|
}
|