@lobehub/chat 1.84.15 → 1.84.17

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 (27) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +121 -20
  3. package/apps/desktop/src/main/core/Browser.ts +6 -1
  4. package/apps/desktop/src/main/core/BrowserManager.ts +2 -2
  5. package/changelog/v1.json +18 -0
  6. package/package.json +4 -2
  7. package/packages/electron-client-ipc/src/events/localFile.ts +2 -0
  8. package/packages/electron-client-ipc/src/types/localFile.ts +16 -0
  9. package/src/app/[variants]/(main)/settings/provider/(detail)/ollama/CheckError.tsx +1 -1
  10. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +2 -1
  11. package/src/features/{Conversation/components/MarkdownElements/LocalFile/Render → LocalFile}/LocalFile.tsx +8 -10
  12. package/src/features/LocalFile/LocalFolder.tsx +65 -0
  13. package/src/features/LocalFile/index.tsx +2 -0
  14. package/src/features/OllamaSetupGuide/Desktop.tsx +36 -40
  15. package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +1 -3
  16. package/src/services/electron/localFileService.ts +5 -0
  17. package/src/store/chat/slices/builtinTool/actions/localFile.ts +27 -3
  18. package/src/store/electron/selectors/desktopState.ts +7 -0
  19. package/src/store/electron/selectors/index.ts +1 -0
  20. package/src/store/tool/slices/store/action.ts +1 -1
  21. package/src/tools/local-files/Render/ListFiles/index.tsx +2 -45
  22. package/src/tools/local-files/Render/RenameLocalFile/index.tsx +45 -0
  23. package/src/tools/local-files/Render/RunCommand/index.tsx +35 -0
  24. package/src/tools/local-files/Render/WriteFile/index.tsx +32 -0
  25. package/src/tools/local-files/Render/index.tsx +4 -0
  26. package/src/tools/local-files/index.ts +20 -21
  27. package/src/tools/local-files/systemRole.ts +7 -13
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.84.17](https://github.com/lobehub/lobe-chat/compare/v1.84.16...v1.84.17)
6
+
7
+ <sup>Released on **2025-05-03**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Add write file tool to local-file plugin.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Add write file tool to local-file plugin, closes [#7684](https://github.com/lobehub/lobe-chat/issues/7684) ([e22e932](https://github.com/lobehub/lobe-chat/commit/e22e932))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.84.16](https://github.com/lobehub/lobe-chat/compare/v1.84.15...v1.84.16)
31
+
32
+ <sup>Released on **2025-05-02**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix desktop quiting with reopen window.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix desktop quiting with reopen window, closes [#7675](https://github.com/lobehub/lobe-chat/issues/7675) ([edeabcf](https://github.com/lobehub/lobe-chat/commit/edeabcf))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.84.15](https://github.com/lobehub/lobe-chat/compare/v1.84.14...v1.84.15)
6
56
 
7
57
  <sup>Released on **2025-05-01**</sup>
@@ -9,6 +9,7 @@ import {
9
9
  OpenLocalFileParams,
10
10
  OpenLocalFolderParams,
11
11
  RenameLocalFileResult,
12
+ WriteLocalFileParams,
12
13
  } from '@lobechat/electron-client-ipc';
13
14
  import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
14
15
  import { shell } from 'electron';
@@ -20,13 +21,18 @@ import { promisify } from 'node:util';
20
21
  import FileSearchService from '@/services/fileSearchSrv';
21
22
  import { FileResult, SearchOptions } from '@/types/fileSearch';
22
23
  import { makeSureDirExist } from '@/utils/file-system';
24
+ import { createLogger } from '@/utils/logger';
23
25
 
24
26
  import { ControllerModule, ipcClientEvent } from './index';
25
27
 
28
+ // 创建日志记录器
29
+ const logger = createLogger('controllers:LocalFileCtr');
30
+
26
31
  const statPromise = promisify(fs.stat);
27
32
  const readdirPromise = promisify(fs.readdir);
28
33
  const renamePromiseFs = promisify(fs.rename);
29
34
  const accessPromise = promisify(fs.access);
35
+ const writeFilePromise = promisify(fs.writeFile);
30
36
 
31
37
  export default class LocalFileCtr extends ControllerModule {
32
38
  private get searchService() {
@@ -38,11 +44,20 @@ export default class LocalFileCtr extends ControllerModule {
38
44
  */
39
45
  @ipcClientEvent('searchLocalFiles')
40
46
  async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
47
+ logger.debug('Received file search request:', { keywords: params.keywords });
48
+
41
49
  const options: Omit<SearchOptions, 'keywords'> = {
42
50
  limit: 30,
43
51
  };
44
52
 
45
- return this.searchService.search(params.keywords, options);
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
+ }
46
61
  }
47
62
 
48
63
  @ipcClientEvent('openLocalFile')
@@ -50,11 +65,14 @@ export default class LocalFileCtr extends ControllerModule {
50
65
  error?: string;
51
66
  success: boolean;
52
67
  }> {
68
+ logger.debug('Attempting to open file:', { filePath });
69
+
53
70
  try {
54
71
  await shell.openPath(filePath);
72
+ logger.debug('File opened successfully:', { filePath });
55
73
  return { success: true };
56
74
  } catch (error) {
57
- console.error(`Failed to open file ${filePath}:`, error);
75
+ logger.error(`Failed to open file ${filePath}:`, error);
58
76
  return { error: (error as Error).message, success: false };
59
77
  }
60
78
  }
@@ -64,35 +82,42 @@ export default class LocalFileCtr extends ControllerModule {
64
82
  error?: string;
65
83
  success: boolean;
66
84
  }> {
85
+ const folderPath = isDirectory ? targetPath : path.dirname(targetPath);
86
+ logger.debug('Attempting to open folder:', { folderPath, isDirectory, targetPath });
87
+
67
88
  try {
68
- const folderPath = isDirectory ? targetPath : path.dirname(targetPath);
69
89
  await shell.openPath(folderPath);
90
+ logger.debug('Folder opened successfully:', { folderPath });
70
91
  return { success: true };
71
92
  } catch (error) {
72
- console.error(`Failed to open folder for path ${targetPath}:`, error);
93
+ logger.error(`Failed to open folder ${folderPath}:`, error);
73
94
  return { error: (error as Error).message, success: false };
74
95
  }
75
96
  }
76
97
 
77
98
  @ipcClientEvent('readLocalFiles')
78
99
  async readFiles({ paths }: LocalReadFilesParams): Promise<LocalReadFileResult[]> {
100
+ logger.debug('Starting batch file reading:', { count: paths.length });
101
+
79
102
  const results: LocalReadFileResult[] = [];
80
103
 
81
104
  for (const filePath of paths) {
82
105
  // 初始化结果对象
106
+ logger.debug('Reading single file:', { filePath });
83
107
  const result = await this.readFile({ path: filePath });
84
-
85
108
  results.push(result);
86
109
  }
87
110
 
111
+ logger.debug('Batch file reading completed', { count: results.length });
88
112
  return results;
89
113
  }
90
114
 
91
115
  @ipcClientEvent('readLocalFile')
92
116
  async readFile({ path: filePath, loc }: LocalReadFileParams): Promise<LocalReadFileResult> {
93
- try {
94
- const effectiveLoc = loc ?? [0, 200];
117
+ const effectiveLoc = loc ?? [0, 200];
118
+ logger.debug('Starting to read file:', { filePath, loc: effectiveLoc });
95
119
 
120
+ try {
96
121
  const fileDocument = await loadFile(filePath);
97
122
 
98
123
  const [startLine, endLine] = effectiveLoc;
@@ -106,6 +131,13 @@ export default class LocalFileCtr extends ControllerModule {
106
131
  const charCount = content.length;
107
132
  const lineCount = selectedLines.length;
108
133
 
134
+ logger.debug('File read successfully:', {
135
+ filePath,
136
+ selectedLineCount: lineCount,
137
+ totalCharCount,
138
+ totalLineCount,
139
+ });
140
+
109
141
  const result: LocalReadFileResult = {
110
142
  // Char count for the selected range
111
143
  charCount,
@@ -128,6 +160,7 @@ export default class LocalFileCtr extends ControllerModule {
128
160
  try {
129
161
  const stats = await statPromise(filePath);
130
162
  if (stats.isDirectory()) {
163
+ logger.warn('Attempted to read directory content:', { filePath });
131
164
  result.content = 'This is a directory and cannot be read as plain text.';
132
165
  result.charCount = 0;
133
166
  result.lineCount = 0;
@@ -136,12 +169,12 @@ export default class LocalFileCtr extends ControllerModule {
136
169
  result.totalLineCount = 0;
137
170
  }
138
171
  } catch (statError) {
139
- console.error(`Stat failed for ${filePath} after loadFile:`, statError);
172
+ logger.error(`Failed to get file status ${filePath}:`, statError);
140
173
  }
141
174
 
142
175
  return result;
143
176
  } catch (error) {
144
- console.error(`Error processing file ${filePath}:`, error);
177
+ logger.error(`Failed to read file ${filePath}:`, error);
145
178
  const errorMessage = (error as Error).message;
146
179
  return {
147
180
  charCount: 0,
@@ -160,13 +193,20 @@ export default class LocalFileCtr extends ControllerModule {
160
193
 
161
194
  @ipcClientEvent('listLocalFiles')
162
195
  async listLocalFiles({ path: dirPath }: ListLocalFileParams): Promise<FileResult[]> {
196
+ logger.debug('Listing directory contents:', { dirPath });
197
+
163
198
  const results: FileResult[] = [];
164
199
  try {
165
200
  const entries = await readdirPromise(dirPath);
201
+ logger.debug('Directory entries retrieved successfully:', {
202
+ dirPath,
203
+ entriesCount: entries.length,
204
+ });
166
205
 
167
206
  for (const entry of entries) {
168
207
  // Skip specific system files based on the ignore list
169
208
  if (SYSTEM_FILES_TO_IGNORE.includes(entry)) {
209
+ logger.debug('Ignoring system file:', { fileName: entry });
170
210
  continue;
171
211
  }
172
212
 
@@ -186,7 +226,7 @@ export default class LocalFileCtr extends ControllerModule {
186
226
  });
187
227
  } catch (statError) {
188
228
  // Silently ignore files we can't stat (e.g. permissions)
189
- console.error(`Failed to stat ${fullPath}:`, statError);
229
+ logger.error(`Failed to get file status ${fullPath}:`, statError);
190
230
  }
191
231
  }
192
232
 
@@ -199,9 +239,10 @@ export default class LocalFileCtr extends ControllerModule {
199
239
  return (a.name || '').localeCompare(b.name || ''); // Then sort by name
200
240
  });
201
241
 
242
+ logger.debug('Directory listing successful', { dirPath, resultCount: results.length });
202
243
  return results;
203
244
  } catch (error) {
204
- console.error(`Failed to list directory ${dirPath}:`, error);
245
+ logger.error(`Failed to list directory ${dirPath}:`, error);
205
246
  // Rethrow or return an empty array/error object depending on desired behavior
206
247
  // For now, returning empty array on error listing directory itself
207
248
  return [];
@@ -210,16 +251,21 @@ export default class LocalFileCtr extends ControllerModule {
210
251
 
211
252
  @ipcClientEvent('moveLocalFiles')
212
253
  async handleMoveFiles({ items }: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
254
+ logger.debug('Starting batch file move:', { itemsCount: items?.length });
255
+
213
256
  const results: LocalMoveFilesResultItem[] = [];
214
257
 
215
258
  if (!items || items.length === 0) {
216
- console.warn('moveLocalFiles called with empty items array.');
259
+ logger.warn('moveLocalFiles called with empty parameters');
217
260
  return [];
218
261
  }
219
262
 
220
263
  // 逐个处理移动请求
221
264
  for (const item of items) {
222
265
  const { oldPath: sourcePath, newPath } = item;
266
+ const logPrefix = `[Moving file ${sourcePath} -> ${newPath}]`;
267
+ logger.debug(`${logPrefix} Starting process`);
268
+
223
269
  const resultItem: LocalMoveFilesResultItem = {
224
270
  newPath: undefined,
225
271
  sourcePath,
@@ -228,6 +274,7 @@ export default class LocalFileCtr extends ControllerModule {
228
274
 
229
275
  // 基本验证
230
276
  if (!sourcePath || !newPath) {
277
+ logger.error(`${logPrefix} Parameter validation failed: source or target path is empty`);
231
278
  resultItem.error = 'Both oldPath and newPath are required for each item.';
232
279
  results.push(resultItem);
233
280
  continue;
@@ -237,10 +284,13 @@ export default class LocalFileCtr extends ControllerModule {
237
284
  // 检查源是否存在
238
285
  try {
239
286
  await accessPromise(sourcePath, fs.constants.F_OK);
287
+ logger.debug(`${logPrefix} Source file exists`);
240
288
  } catch (accessError: any) {
241
289
  if (accessError.code === 'ENOENT') {
290
+ logger.error(`${logPrefix} Source file does not exist`);
242
291
  throw new Error(`Source path not found: ${sourcePath}`);
243
292
  } else {
293
+ logger.error(`${logPrefix} Permission error accessing source file:`, accessError);
244
294
  throw new Error(
245
295
  `Permission denied accessing source path: ${sourcePath}. ${accessError.message}`,
246
296
  );
@@ -249,7 +299,7 @@ export default class LocalFileCtr extends ControllerModule {
249
299
 
250
300
  // 检查目标路径是否与源路径相同
251
301
  if (path.normalize(sourcePath) === path.normalize(newPath)) {
252
- console.log(`Skipping move: source and target path are identical: ${sourcePath}`);
302
+ logger.info(`${logPrefix} Source and target paths are identical, skipping move`);
253
303
  resultItem.success = true;
254
304
  resultItem.newPath = newPath; // 即使未移动,也报告目标路径
255
305
  results.push(resultItem);
@@ -259,14 +309,15 @@ export default class LocalFileCtr extends ControllerModule {
259
309
  // LBYL: 确保目标目录存在
260
310
  const targetDir = path.dirname(newPath);
261
311
  makeSureDirExist(targetDir);
312
+ logger.debug(`${logPrefix} Ensured target directory exists: ${targetDir}`);
262
313
 
263
314
  // 执行移动 (rename)
264
315
  await renamePromiseFs(sourcePath, newPath);
265
316
  resultItem.success = true;
266
317
  resultItem.newPath = newPath;
267
- console.log(`Successfully moved ${sourcePath} to ${newPath}`);
318
+ logger.info(`${logPrefix} Move successful`);
268
319
  } catch (error) {
269
- console.error(`Error moving ${sourcePath} to ${newPath}:`, error);
320
+ logger.error(`${logPrefix} Move failed:`, error);
270
321
  // 使用与 handleMoveFile 类似的错误处理逻辑
271
322
  let errorMessage = (error as Error).message;
272
323
  if ((error as any).code === 'ENOENT')
@@ -296,6 +347,10 @@ export default class LocalFileCtr extends ControllerModule {
296
347
  results.push(resultItem);
297
348
  }
298
349
 
350
+ logger.debug('Batch file move completed', {
351
+ successCount: results.filter((r) => r.success).length,
352
+ totalCount: results.length,
353
+ });
299
354
  return results;
300
355
  }
301
356
 
@@ -307,8 +362,12 @@ export default class LocalFileCtr extends ControllerModule {
307
362
  newName: string;
308
363
  path: string;
309
364
  }): Promise<RenameLocalFileResult> {
365
+ const logPrefix = `[Renaming ${currentPath} -> ${newName}]`;
366
+ logger.debug(`${logPrefix} Starting rename request`);
367
+
310
368
  // Basic validation (can also be done in frontend action)
311
369
  if (!currentPath || !newName) {
370
+ logger.error(`${logPrefix} Parameter validation failed: path or new name is empty`);
312
371
  return { error: 'Both path and newName are required.', newPath: '', success: false };
313
372
  }
314
373
  // Prevent path traversal or using invalid characters/names
@@ -319,6 +378,7 @@ export default class LocalFileCtr extends ControllerModule {
319
378
  newName === '..' ||
320
379
  /["*/:<>?\\|]/.test(newName) // Check for typical invalid filename characters
321
380
  ) {
381
+ logger.error(`${logPrefix} New filename contains illegal characters: ${newName}`);
322
382
  return {
323
383
  error:
324
384
  'Invalid new name. It cannot contain path separators (/, \\), be "." or "..", or include characters like < > : " / \\ | ? *.',
@@ -331,18 +391,19 @@ export default class LocalFileCtr extends ControllerModule {
331
391
  try {
332
392
  const dir = path.dirname(currentPath);
333
393
  newPath = path.join(dir, newName);
394
+ logger.debug(`${logPrefix} Calculated new path: ${newPath}`);
334
395
 
335
396
  // Check if paths are identical after calculation
336
397
  if (path.normalize(currentPath) === path.normalize(newPath)) {
337
- console.log(
338
- `Skipping rename: oldPath and calculated newPath are identical: ${currentPath}`,
398
+ logger.info(
399
+ `${logPrefix} Source path and calculated target path are identical, skipping rename`,
339
400
  );
340
401
  // Consider success as no change is needed, but maybe inform the user?
341
402
  // Return success for now.
342
403
  return { newPath, success: true };
343
404
  }
344
405
  } catch (error) {
345
- console.error(`Error calculating new path for rename ${currentPath} to ${newName}:`, error);
406
+ logger.error(`${logPrefix} Failed to calculate new path:`, error);
346
407
  return {
347
408
  error: `Internal error calculating the new path: ${(error as Error).message}`,
348
409
  newPath: '',
@@ -353,12 +414,12 @@ export default class LocalFileCtr extends ControllerModule {
353
414
  // Perform the rename operation using fs.promises.rename directly
354
415
  try {
355
416
  await renamePromise(currentPath, newPath);
356
- console.log(`Successfully renamed ${currentPath} to ${newPath}`);
417
+ logger.info(`${logPrefix} Rename successful: ${currentPath} -> ${newPath}`);
357
418
  // Optionally return the newPath if frontend needs it
358
419
  // return { success: true, newPath: newPath };
359
420
  return { newPath, success: true };
360
421
  } catch (error) {
361
- console.error(`Error renaming ${currentPath} to ${newPath}:`, error);
422
+ logger.error(`${logPrefix} Rename failed:`, error);
362
423
  let errorMessage = (error as Error).message;
363
424
  // Provide more specific error messages based on common codes
364
425
  if ((error as any).code === 'ENOENT') {
@@ -377,4 +438,44 @@ export default class LocalFileCtr extends ControllerModule {
377
438
  return { error: errorMessage, newPath: '', success: false };
378
439
  }
379
440
  }
441
+
442
+ @ipcClientEvent('writeLocalFile')
443
+ async handleWriteFile({ path: filePath, content }: WriteLocalFileParams) {
444
+ const logPrefix = `[Writing file ${filePath}]`;
445
+ logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
446
+
447
+ // 验证参数
448
+ if (!filePath) {
449
+ logger.error(`${logPrefix} Parameter validation failed: path is empty`);
450
+ return { error: 'Path cannot be empty', success: false };
451
+ }
452
+
453
+ if (content === undefined) {
454
+ logger.error(`${logPrefix} Parameter validation failed: content is empty`);
455
+ return { error: 'Content cannot be empty', success: false };
456
+ }
457
+
458
+ try {
459
+ // 确保目标目录存在
460
+ const dirname = path.dirname(filePath);
461
+ logger.debug(`${logPrefix} Creating directory: ${dirname}`);
462
+ fs.mkdirSync(dirname, { recursive: true });
463
+
464
+ // 写入文件内容
465
+ logger.debug(`${logPrefix} Starting to write content to file`);
466
+ await writeFilePromise(filePath, content, 'utf8');
467
+ logger.info(`${logPrefix} File written successfully`, {
468
+ path: filePath,
469
+ size: content.length,
470
+ });
471
+
472
+ return { success: true };
473
+ } catch (error) {
474
+ logger.error(`${logPrefix} Failed to write file:`, error);
475
+ return {
476
+ error: `Failed to write file: ${(error as Error).message}`,
477
+ success: false,
478
+ };
479
+ }
480
+ }
380
481
  }
@@ -55,6 +55,12 @@ export default class Browser {
55
55
  return this.retrieveOrInitialize();
56
56
  }
57
57
 
58
+ get webContents() {
59
+ if (this._browserWindow.isDestroyed()) return null;
60
+
61
+ return this._browserWindow.webContents;
62
+ }
63
+
58
64
  /**
59
65
  * Method to construct BrowserWindows object
60
66
  * @param options
@@ -210,7 +216,6 @@ export default class Browser {
210
216
  session: browserWindow.webContents.session,
211
217
  });
212
218
 
213
- console.log('platform:',process.platform);
214
219
  // Windows 11 can use this new API
215
220
  if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
216
221
  logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
@@ -157,8 +157,8 @@ export default class BrowserManager {
157
157
  this.webContentsMap.set(browser.browserWindow.webContents, identifier);
158
158
 
159
159
  // 当窗口关闭时清理映射
160
- browser.browserWindow.on('closed', () => {
161
- this.webContentsMap.delete(browser.browserWindow.webContents);
160
+ browser.browserWindow.on('close', () => {
161
+ if (browser.webContents) this.webContentsMap.delete(browser.webContents);
162
162
  });
163
163
 
164
164
  return browser;
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add write file tool to local-file plugin."
6
+ ]
7
+ },
8
+ "date": "2025-05-03",
9
+ "version": "1.84.17"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Fix desktop quiting with reopen window."
15
+ ]
16
+ },
17
+ "date": "2025-05-02",
18
+ "version": "1.84.16"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.84.15",
3
+ "version": "1.84.17",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -147,7 +147,7 @@
147
147
  "@lobehub/icons": "^2.0.0",
148
148
  "@lobehub/tts": "^2.0.0",
149
149
  "@lobehub/ui": "^2.0.10",
150
- "@modelcontextprotocol/sdk": "^1.10.1",
150
+ "@modelcontextprotocol/sdk": "^1.11.0",
151
151
  "@neondatabase/serverless": "^1.0.0",
152
152
  "@next/third-parties": "^15.3.0",
153
153
  "@react-spring/web": "^9.7.5",
@@ -163,6 +163,7 @@
163
163
  "@vercel/edge-config": "^1.4.0",
164
164
  "@vercel/functions": "^2.0.0",
165
165
  "@vercel/speed-insights": "^1.2.0",
166
+ "@xterm/xterm": "^5.5.0",
166
167
  "ahooks": "^3.8.4",
167
168
  "ai": "^3.4.33",
168
169
  "antd": "^5.24.6",
@@ -212,6 +213,7 @@
212
213
  "openai": "^4.91.1",
213
214
  "openapi-fetch": "^0.9.8",
214
215
  "partial-json": "^0.1.7",
216
+ "path-browserify-esm": "^1.0.6",
215
217
  "pdf-parse": "^1.1.1",
216
218
  "pdfjs-dist": "4.8.69",
217
219
  "pg": "^8.14.1",
@@ -11,6 +11,7 @@ import {
11
11
  OpenLocalFolderParams,
12
12
  RenameLocalFileParams,
13
13
  RenameLocalFileResult,
14
+ WriteLocalFileParams,
14
15
  } from '../types';
15
16
 
16
17
  export interface LocalFilesDispatchEvents {
@@ -25,4 +26,5 @@ export interface LocalFilesDispatchEvents {
25
26
 
26
27
  renameLocalFile: (params: RenameLocalFileParams) => RenameLocalFileResult;
27
28
  searchLocalFiles: (params: LocalSearchFilesParams) => LocalFileItem[];
29
+ writeLocalFile: (params: WriteLocalFileParams) => RenameLocalFileResult;
28
30
  }
@@ -55,6 +55,22 @@ export interface LocalReadFilesParams {
55
55
  paths: string[];
56
56
  }
57
57
 
58
+ export interface WriteLocalFileParams {
59
+ /**
60
+ * 要写入的内容
61
+ */
62
+ content: string;
63
+
64
+ /**
65
+ * 要写入的文件路径
66
+ */
67
+ path: string;
68
+ }
69
+
70
+ export interface RunCommandParams {
71
+ command: string;
72
+ }
73
+
58
74
  export interface LocalReadFileResult {
59
75
  /**
60
76
  * Character count of the content within the specified `loc` range.
@@ -44,7 +44,7 @@ const CheckError = ({
44
44
 
45
45
  const errorMessage = errorBody.error?.message;
46
46
 
47
- if (error?.type === 'OllamaServiceUnavailable') return <OllamaSetupGuide container={false} />;
47
+ if (error?.type === 'OllamaServiceUnavailable') return <OllamaSetupGuide />;
48
48
 
49
49
  // error of not pull the model
50
50
  const unresolvedModel = errorMessage?.match(UNRESOLVED_MODEL_REGEXP)?.[1];
@@ -1,8 +1,9 @@
1
1
  import isEqual from 'fast-deep-equal';
2
2
  import React, { memo } from 'react';
3
3
 
4
+ import { LocalFile } from '@/features/LocalFile';
5
+
4
6
  import { MarkdownElementProps } from '../../type';
5
- import LocalFile from './LocalFile';
6
7
 
7
8
  interface LocalFileProps {
8
9
  isDirectory: boolean;
@@ -31,17 +31,17 @@ const useStyles = createStyles(({ css, token }) => ({
31
31
  `,
32
32
  }));
33
33
 
34
- const LocalFile = ({
35
- name,
36
- path,
37
- isDirectory,
38
- }: {
39
- isDirectory: boolean;
34
+ interface LocalFileProps {
35
+ isDirectory?: boolean;
40
36
  name: string;
41
- path: string;
42
- }) => {
37
+ path?: string;
38
+ }
39
+
40
+ export const LocalFile = ({ name, path, isDirectory = false }: LocalFileProps) => {
43
41
  const { styles } = useStyles();
44
42
  const handleClick = () => {
43
+ if (!path) return;
44
+
45
45
  localFileService.openLocalFileOrFolder(path, isDirectory);
46
46
  };
47
47
 
@@ -61,5 +61,3 @@ const LocalFile = ({
61
61
  </Flexbox>
62
62
  );
63
63
  };
64
-
65
- export default LocalFile;
@@ -0,0 +1,65 @@
1
+ import { createStyles } from 'antd-style';
2
+ import path from 'path-browserify-esm';
3
+ import React from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import FileIcon from '@/components/FileIcon';
7
+ import { localFileService } from '@/services/electron/localFileService';
8
+
9
+ const useStyles = createStyles(({ css, token }) => ({
10
+ container: css`
11
+ cursor: pointer;
12
+
13
+ padding-block: 2px;
14
+ padding-inline: 4px 8px;
15
+ border-radius: 4px;
16
+
17
+ color: ${token.colorTextSecondary};
18
+
19
+ :hover {
20
+ color: ${token.colorText};
21
+ background: ${token.colorFillTertiary};
22
+ }
23
+ `,
24
+ title: css`
25
+ overflow: hidden;
26
+ display: block;
27
+
28
+ line-height: 20px;
29
+ color: inherit;
30
+ text-overflow: ellipsis;
31
+ white-space: nowrap;
32
+ `,
33
+ }));
34
+
35
+ interface LocalFolderProps {
36
+ path: string;
37
+ size?: number;
38
+ }
39
+
40
+ export const LocalFolder = ({ path: pathname, size = 22 }: LocalFolderProps) => {
41
+ const { styles } = useStyles();
42
+ const handleClick = () => {
43
+ if (!path) return;
44
+
45
+ localFileService.openLocalFolder({ isDirectory: true, path: pathname });
46
+ };
47
+
48
+ const { base } = path.parse(pathname);
49
+
50
+ return (
51
+ <Flexbox
52
+ align={'center'}
53
+ className={styles.container}
54
+ gap={4}
55
+ horizontal
56
+ onClick={handleClick}
57
+ style={{ display: 'inline-flex', verticalAlign: 'middle' }}
58
+ >
59
+ <FileIcon fileName={base} isDirectory size={size} variant={'raw'} />
60
+ <Flexbox align={'baseline'} gap={4} horizontal style={{ overflow: 'hidden', width: '100%' }}>
61
+ <div className={styles.title}>{base}</div>
62
+ </Flexbox>
63
+ </Flexbox>
64
+ );
65
+ };
@@ -0,0 +1,2 @@
1
+ export { LocalFile } from './LocalFile';
2
+ export { LocalFolder } from './LocalFolder';
@@ -9,8 +9,6 @@ import { Center } from 'react-layout-kit';
9
9
  import FormAction from '@/components/FormAction';
10
10
  import { useChatStore } from '@/store/chat';
11
11
 
12
- import { ErrorActionContainer } from '../Conversation/Error/style';
13
-
14
12
  // TODO: 优化 Ollama setup 的流程,isDesktop 模式下可以直接做到端到端检测
15
13
  const OllamaDesktopSetupGuide = memo<{ id?: string }>(({ id }) => {
16
14
  const theme = useTheme();
@@ -22,44 +20,42 @@ const OllamaDesktopSetupGuide = memo<{ id?: string }>(({ id }) => {
22
20
  ]);
23
21
 
24
22
  return (
25
- <ErrorActionContainer style={{ paddingBlock: 0 }}>
26
- <Center gap={16} paddingBlock={32} style={{ maxWidth: 300, width: '100%' }}>
27
- <FormAction
28
- avatar={<Ollama color={theme.colorPrimary} size={64} />}
29
- description={
30
- <span>
31
- <Trans i18nKey={'OllamaSetupGuide.install.description'} ns={'components'}>
32
- 请确认你已经开启 Ollama ,如果没有安装 Ollama ,请前往官网
33
- <Link href={'https://ollama.com/download'}>下载</Link>
34
- </Trans>
35
- </span>
36
- }
37
- title={t('OllamaSetupGuide.install.title')}
38
- />
39
- {id && (
40
- <>
41
- <Button
42
- block
43
- onClick={() => {
44
- delAndRegenerateMessage(id);
45
- }}
46
- style={{ marginTop: 8 }}
47
- type={'primary'}
48
- >
49
- {t('OllamaSetupGuide.action.start')}
50
- </Button>
51
- <Button
52
- block
53
- onClick={() => {
54
- deleteMessage(id);
55
- }}
56
- >
57
- {t('OllamaSetupGuide.action.close')}
58
- </Button>
59
- </>
60
- )}
61
- </Center>
62
- </ErrorActionContainer>
23
+ <Center gap={16} paddingBlock={32} style={{ maxWidth: 300, width: '100%' }}>
24
+ <FormAction
25
+ avatar={<Ollama color={theme.colorPrimary} size={64} />}
26
+ description={
27
+ <span>
28
+ <Trans i18nKey={'OllamaSetupGuide.install.description'} ns={'components'}>
29
+ 请确认你已经开启 Ollama ,如果没有安装 Ollama ,请前往官网
30
+ <Link href={'https://ollama.com/download'}>下载</Link>
31
+ </Trans>
32
+ </span>
33
+ }
34
+ title={t('OllamaSetupGuide.install.title')}
35
+ />
36
+ {id && (
37
+ <>
38
+ <Button
39
+ block
40
+ onClick={() => {
41
+ delAndRegenerateMessage(id);
42
+ }}
43
+ style={{ marginTop: 8 }}
44
+ type={'primary'}
45
+ >
46
+ {t('OllamaSetupGuide.action.start')}
47
+ </Button>
48
+ <Button
49
+ block
50
+ onClick={() => {
51
+ deleteMessage(id);
52
+ }}
53
+ >
54
+ {t('OllamaSetupGuide.action.close')}
55
+ </Button>
56
+ </>
57
+ )}
58
+ </Center>
63
59
  );
64
60
  });
65
61
 
@@ -21,11 +21,9 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
21
21
  "name": "echo",
22
22
  },
23
23
  {
24
+ "annotations": {},
24
25
  "description": "Lists all available tools and methods",
25
26
  "inputSchema": {
26
- "$schema": "http://json-schema.org/draft-07/schema#",
27
- "additionalProperties": false,
28
- "properties": {},
29
27
  "type": "object",
30
28
  },
31
29
  "name": "debug",
@@ -10,6 +10,7 @@ import {
10
10
  OpenLocalFileParams,
11
11
  OpenLocalFolderParams,
12
12
  RenameLocalFileParams,
13
+ WriteLocalFileParams,
13
14
  dispatch,
14
15
  } from '@lobechat/electron-client-ipc';
15
16
 
@@ -46,6 +47,10 @@ class LocalFileService {
46
47
  return dispatch('renameLocalFile', params);
47
48
  }
48
49
 
50
+ async writeFile(params: WriteLocalFileParams) {
51
+ return dispatch('writeLocalFile', params);
52
+ }
53
+
49
54
  async openLocalFileOrFolder(path: string, isDirectory: boolean) {
50
55
  if (isDirectory) {
51
56
  return this.openLocalFolder({ isDirectory, path });
@@ -6,6 +6,7 @@ import {
6
6
  LocalSearchFilesParams,
7
7
  MoveLocalFilesParams,
8
8
  RenameLocalFileParams,
9
+ WriteLocalFileParams,
9
10
  } from '@lobechat/electron-client-ipc';
10
11
  import { StateCreator } from 'zustand/vanilla';
11
12
 
@@ -23,7 +24,7 @@ import {
23
24
  export interface LocalFileAction {
24
25
  internal_triggerLocalFileToolCalling: <T = any>(
25
26
  id: string,
26
- callingService: () => Promise<{ content: any; state: T }>,
27
+ callingService: () => Promise<{ content: any; state?: T }>,
27
28
  ) => Promise<boolean>;
28
29
 
29
30
  listLocalFiles: (id: string, params: ListLocalFileParams) => Promise<boolean>;
@@ -34,8 +35,9 @@ export interface LocalFileAction {
34
35
  renameLocalFile: (id: string, params: RenameLocalFileParams) => Promise<boolean>;
35
36
  // Added rename action
36
37
  searchLocalFiles: (id: string, params: LocalSearchFilesParams) => Promise<boolean>;
37
-
38
38
  toggleLocalFileLoading: (id: string, loading: boolean) => void;
39
+
40
+ writeLocalFile: (id: string, params: WriteLocalFileParams) => Promise<boolean>;
39
41
  }
40
42
 
41
43
  export const localFileSlice: StateCreator<
@@ -48,7 +50,9 @@ export const localFileSlice: StateCreator<
48
50
  get().toggleLocalFileLoading(id, true);
49
51
  try {
50
52
  const { state, content } = await callingService();
51
- await get().updatePluginState(id, state as any);
53
+ if (state) {
54
+ await get().updatePluginState(id, state as any);
55
+ }
52
56
  await get().internal_updateMessageContent(id, JSON.stringify(content));
53
57
  } catch (error) {
54
58
  await get().internal_updateMessagePluginError(id, {
@@ -183,4 +187,24 @@ export const localFileSlice: StateCreator<
183
187
  `toggleLocalFileLoading/${loading ? 'start' : 'end'}`,
184
188
  );
185
189
  },
190
+
191
+ writeLocalFile: async (id, params) => {
192
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
193
+ const result = await localFileService.writeFile(params);
194
+
195
+ let content: { message: string; success: boolean };
196
+
197
+ if (result.success) {
198
+ content = {
199
+ message: `成功写入文件 ${params.path}`,
200
+ success: true,
201
+ };
202
+ } else {
203
+ const errorMessage = result.error;
204
+
205
+ content = { message: errorMessage || '写入文件失败', success: false };
206
+ }
207
+ return { content };
208
+ });
209
+ },
186
210
  });
@@ -0,0 +1,7 @@
1
+ import { ElectronState } from '@/store/electron/initialState';
2
+
3
+ const usePath = (s: ElectronState) => s.appState.userPath;
4
+
5
+ export const desktopStateSelectors = {
6
+ usePath,
7
+ };
@@ -1 +1,2 @@
1
+ export * from './desktopState';
1
2
  export * from './sync';
@@ -70,7 +70,7 @@ export const createPluginStoreSlice: StateCreator<
70
70
  loadPluginStore: async () => {
71
71
  const pluginMarketIndex = await toolService.getToolList();
72
72
 
73
- set({ pluginStoreList: pluginMarketIndex }, false, n('loadPluginList'));
73
+ set({ pluginStoreList: pluginMarketIndex || [] }, false, n('loadPluginList'));
74
74
 
75
75
  return pluginMarketIndex;
76
76
  },
@@ -1,42 +1,12 @@
1
1
  import { ListLocalFileParams } from '@lobechat/electron-client-ipc';
2
- import { Typography } from 'antd';
3
- import { createStyles } from 'antd-style';
4
2
  import React, { memo } from 'react';
5
- import { Flexbox } from 'react-layout-kit';
6
3
 
7
- import FileIcon from '@/components/FileIcon';
8
- import { localFileService } from '@/services/electron/localFileService';
4
+ import { LocalFolder } from '@/features/LocalFile';
9
5
  import { LocalFileListState } from '@/tools/local-files/type';
10
6
  import { ChatMessagePluginError } from '@/types/message';
11
7
 
12
8
  import SearchResult from './Result';
13
9
 
14
- const useStyles = createStyles(({ css, token, cx }) => ({
15
- actions: cx(css`
16
- cursor: pointer;
17
- color: ${token.colorTextTertiary};
18
- opacity: 1;
19
- transition: opacity 0.2s ${token.motionEaseInOut};
20
- `),
21
- container: css`
22
- cursor: pointer;
23
-
24
- padding-block: 2px;
25
- padding-inline: 4px;
26
- border-radius: 4px;
27
-
28
- color: ${token.colorTextSecondary};
29
-
30
- :hover {
31
- color: ${token.colorText};
32
- background: ${token.colorFillTertiary};
33
- }
34
- `,
35
- path: css`
36
- color: ${token.colorTextSecondary};
37
- `,
38
- }));
39
-
40
10
  interface ListFilesProps {
41
11
  args: ListLocalFileParams;
42
12
  messageId: string;
@@ -45,22 +15,9 @@ interface ListFilesProps {
45
15
  }
46
16
 
47
17
  const ListFiles = memo<ListFilesProps>(({ messageId, pluginError, args, pluginState }) => {
48
- const { styles } = useStyles();
49
18
  return (
50
19
  <>
51
- <Flexbox
52
- className={styles.container}
53
- gap={8}
54
- horizontal
55
- onClick={() => {
56
- localFileService.openLocalFolder({ isDirectory: true, path: args.path });
57
- }}
58
- >
59
- <FileIcon fileName={args.path} isDirectory size={22} variant={'raw'} />
60
- <Typography.Text className={styles.path} ellipsis>
61
- {args.path}
62
- </Typography.Text>
63
- </Flexbox>
20
+ <LocalFolder path={args.path} />
64
21
  <SearchResult
65
22
  listResults={pluginState?.listResults}
66
23
  messageId={messageId}
@@ -0,0 +1,45 @@
1
+ import { RenameLocalFileParams } from '@lobechat/electron-client-ipc';
2
+ import { Icon } from '@lobehub/ui';
3
+ import { createStyles } from 'antd-style';
4
+ import { ArrowRightIcon } from 'lucide-react';
5
+ import path from 'path-browserify-esm';
6
+ import React, { memo } from 'react';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { LocalFile } from '@/features/LocalFile';
10
+ import { LocalReadFileState } from '@/tools/local-files/type';
11
+ import { ChatMessagePluginError } from '@/types/message';
12
+
13
+ const useStyles = createStyles(({ css, token }) => ({
14
+ container: css`
15
+ color: ${token.colorTextQuaternary};
16
+ `,
17
+ new: css`
18
+ color: ${token.colorTextSecondary};
19
+ `,
20
+ }));
21
+
22
+ interface RenameLocalFileProps {
23
+ args: RenameLocalFileParams;
24
+ messageId: string;
25
+ pluginError: ChatMessagePluginError;
26
+ pluginState: LocalReadFileState;
27
+ }
28
+
29
+ const RenameLocalFile = memo<RenameLocalFileProps>(({ args }) => {
30
+ const { styles } = useStyles();
31
+
32
+ const { base: oldFileName, dir } = path.parse(args.path);
33
+
34
+ return (
35
+ <Flexbox align={'center'} className={styles.container} gap={8} horizontal paddingInline={12}>
36
+ <Flexbox>{oldFileName}</Flexbox>
37
+ <Flexbox>
38
+ <Icon icon={ArrowRightIcon} />
39
+ </Flexbox>
40
+ <LocalFile name={args.newName} path={path.join(dir, args.newName)} />
41
+ </Flexbox>
42
+ );
43
+ });
44
+
45
+ export default RenameLocalFile;
@@ -0,0 +1,35 @@
1
+ import { RunCommandParams } from '@lobechat/electron-client-ipc';
2
+ import { Terminal } from '@xterm/xterm';
3
+ import '@xterm/xterm/css/xterm.css';
4
+ import { memo, useEffect, useRef } from 'react';
5
+
6
+ import { LocalReadFileState } from '@/tools/local-files/type';
7
+ import { ChatMessagePluginError } from '@/types/message';
8
+
9
+ interface RunCommandProps {
10
+ args: RunCommandParams;
11
+ messageId: string;
12
+ pluginError: ChatMessagePluginError;
13
+ pluginState: LocalReadFileState;
14
+ }
15
+
16
+ const RunCommand = memo<RunCommandProps>(({ args }) => {
17
+ const terminalRef = useRef(null);
18
+
19
+ useEffect(() => {
20
+ if (!terminalRef.current) return;
21
+
22
+ const term = new Terminal({ cols: 80, cursorBlink: true, rows: 30 });
23
+
24
+ term.open(terminalRef.current);
25
+ term.write(args.command);
26
+
27
+ return () => {
28
+ term.dispose();
29
+ };
30
+ }, []);
31
+
32
+ return <div ref={terminalRef} />;
33
+ });
34
+
35
+ export default RunCommand;
@@ -0,0 +1,32 @@
1
+ import { WriteLocalFileParams } from '@lobechat/electron-client-ipc';
2
+ import { Icon } from '@lobehub/ui';
3
+ import { Skeleton } from 'antd';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import path from 'path-browserify-esm';
6
+ import { memo } from 'react';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { LocalFile, LocalFolder } from '@/features/LocalFile';
10
+ import { ChatMessagePluginError } from '@/types/message';
11
+
12
+ interface WriteFileProps {
13
+ args: WriteLocalFileParams;
14
+ messageId: string;
15
+ pluginError: ChatMessagePluginError;
16
+ }
17
+
18
+ const WriteFile = memo<WriteFileProps>(({ args }) => {
19
+ if (!args) return <Skeleton active />;
20
+
21
+ const { base, dir } = path.parse(args.path);
22
+
23
+ return (
24
+ <Flexbox horizontal>
25
+ <LocalFolder path={dir} />
26
+ <Icon icon={ChevronRight} />
27
+ <LocalFile name={base} path={args.path} />
28
+ </Flexbox>
29
+ );
30
+ });
31
+
32
+ export default WriteFile;
@@ -6,12 +6,16 @@ import { BuiltinRenderProps } from '@/types/tool';
6
6
 
7
7
  import ListFiles from './ListFiles';
8
8
  import ReadLocalFile from './ReadLocalFile';
9
+ import RenameLocalFile from './RenameLocalFile';
9
10
  import SearchFiles from './SearchFiles';
11
+ import WriteFile from './WriteFile';
10
12
 
11
13
  const RenderMap = {
12
14
  [LocalFilesApiName.searchLocalFiles]: SearchFiles,
13
15
  [LocalFilesApiName.listLocalFiles]: ListFiles,
14
16
  [LocalFilesApiName.readLocalFile]: ReadLocalFile,
17
+ [LocalFilesApiName.renameLocalFile]: RenameLocalFile,
18
+ [LocalFilesApiName.writeLocalFile]: WriteFile,
15
19
  };
16
20
 
17
21
  const LocalFilesRender = memo<BuiltinRenderProps<LocalFileItem[]>>(
@@ -8,7 +8,7 @@ export const LocalFilesApiName = {
8
8
  readLocalFile: 'readLocalFile',
9
9
  renameLocalFile: 'renameLocalFile',
10
10
  searchLocalFiles: 'searchLocalFiles',
11
- writeFile: 'writeFile',
11
+ writeLocalFile: 'writeLocalFile',
12
12
  };
13
13
 
14
14
  export const LocalFilesManifest: BuiltinToolManifest = {
@@ -176,26 +176,25 @@ export const LocalFilesManifest: BuiltinToolManifest = {
176
176
  type: 'object',
177
177
  },
178
178
  },
179
- // TODO: Add writeFile API definition later
180
- // {
181
- // description:
182
- // 'Write content to a specific file. Input should be the file path and content. Overwrites existing file or creates a new one.',
183
- // name: LocalFilesApiName.writeFile,
184
- // parameters: {
185
- // properties: {
186
- // path: {
187
- // description: 'The file path to write to',
188
- // type: 'string',
189
- // },
190
- // content: {
191
- // description: 'The content to write',
192
- // type: 'string',
193
- // },
194
- // },
195
- // required: ['path', 'content'],
196
- // type: 'object',
197
- // },
198
- // },
179
+ {
180
+ description:
181
+ 'Write content to a specific file. Input should be the file path and content. Overwrites existing file or creates a new one.',
182
+ name: LocalFilesApiName.writeLocalFile,
183
+ parameters: {
184
+ properties: {
185
+ content: {
186
+ description: 'The content to write',
187
+ type: 'string',
188
+ },
189
+ path: {
190
+ description: 'The file path to write to',
191
+ type: 'string',
192
+ },
193
+ },
194
+ required: ['path', 'content'],
195
+ type: 'object',
196
+ },
197
+ },
199
198
  ],
200
199
  identifier: 'lobe-local-files',
201
200
  meta: {
@@ -1,4 +1,4 @@
1
- export const systemPrompt = `You have a Local Files tool with capabilities to interact with the user's local file system. You can list directories, read file contents, search for files, move, and rename files/directories.
1
+ export const systemPrompt = `You have a Local System tool with capabilities to interact with the user's local file system. You can list directories, read file contents, search for files, move, and rename files/directories.
2
2
 
3
3
  <user_context>
4
4
  Here are some known locations and system details on the user's system. User is using the Operating System: {{platform}}({{arch}}). Use these paths when the user refers to these common locations by name (e.g., "my desktop", "downloads folder").
@@ -12,26 +12,20 @@ Here are some known locations and system details on the user's system. User is u
12
12
  - App Data: {{userDataPath}} (Use this primarily for plugin-related data or configurations if needed, less for general user files)
13
13
  </user_context>
14
14
 
15
+ <core_capabilities>
15
16
  You have access to a set of tools to interact with the user's local file system:
16
17
 
17
18
  1. **listLocalFiles**: Lists files and directories in a specified path.
18
19
  2. **readLocalFile**: Reads the content of a specified file, optionally within a line range.
19
- 3. **searchLocalFiles**: Searches for files based on keywords and other criteria. Use this tool to find files if the user is unsure about the exact path.
20
- 4. **renameLocalFile**: Renames a single file or directory in its current location.
21
- 5. **moveLocalFiles**: Moves multiple files or directories. Can be used for renaming during the move.
22
-
23
- <core_capabilities>
24
- 1. List files and folders in a directory (listFiles)
25
- 2. Read the content of a specific file (readFile)
26
- 3. Search for files based on a query and various filter options (searchFiles)
27
- 4. Rename a file or folder within its current directory (renameFile)
28
- 5. Move a file or folder to a new location, potentially renaming it (moveFile)
29
- 6. Write content to a specific file (writeFile) - // TODO: Implement later
20
+ 3. **writeFile**: Write content to a specific file, only support plain text file like \`.text\` or \`.md\`
21
+ 4. **searchLocalFiles**: Searches for files based on keywords and other criteria. Use this tool to find files if the user is unsure about the exact path.
22
+ 5. **renameLocalFile**: Renames a single file or directory in its current location.
23
+ 6. **moveLocalFiles**: Moves multiple files or directories. Can be used for renaming during the move.
30
24
  </core_capabilities>
31
25
 
32
26
  <workflow>
33
27
  1. Understand the user's request regarding local files (listing, reading, searching, renaming, moving, writing).
34
- 2. Select the appropriate tool (listFiles, readFile, searchFiles, renameFile, moveFile, writeFile).
28
+ 2. Select the appropriate tool (listFiles, readFile, searchFiles, renameFile, moveLocalFiles, writeFile).
35
29
  3. Execute the file operation. **If the user mentions a common location (like Desktop, Documents, Downloads, etc.) without providing a full path, use the corresponding path from the <user_context> section.**
36
30
  4. Present the results (directory listing, file content, search results) or confirmation of the rename or move operation.
37
31
  </workflow>