@lobehub/lobehub 2.0.0-next.73 → 2.0.0-next.75

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 (78) hide show
  1. package/.github/workflows/desktop-pr-build.yml +7 -3
  2. package/CHANGELOG.md +50 -0
  3. package/apps/desktop/package.json +1 -0
  4. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +55 -11
  5. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +153 -0
  6. package/changelog/v1.json +18 -0
  7. package/locales/ar/chat.json +5 -0
  8. package/locales/ar/models.json +15 -0
  9. package/locales/ar/tool.json +12 -1
  10. package/locales/bg-BG/chat.json +5 -0
  11. package/locales/bg-BG/models.json +15 -0
  12. package/locales/bg-BG/tool.json +12 -1
  13. package/locales/de-DE/chat.json +5 -0
  14. package/locales/de-DE/models.json +15 -0
  15. package/locales/de-DE/tool.json +12 -1
  16. package/locales/en-US/models.json +15 -0
  17. package/locales/en-US/tool.json +12 -1
  18. package/locales/es-ES/chat.json +5 -0
  19. package/locales/es-ES/models.json +15 -0
  20. package/locales/es-ES/tool.json +12 -1
  21. package/locales/fa-IR/chat.json +5 -0
  22. package/locales/fa-IR/models.json +15 -0
  23. package/locales/fa-IR/tool.json +12 -1
  24. package/locales/fr-FR/chat.json +5 -0
  25. package/locales/fr-FR/models.json +15 -0
  26. package/locales/fr-FR/tool.json +12 -1
  27. package/locales/it-IT/chat.json +5 -0
  28. package/locales/it-IT/models.json +15 -0
  29. package/locales/it-IT/tool.json +12 -1
  30. package/locales/ja-JP/chat.json +5 -0
  31. package/locales/ja-JP/models.json +15 -0
  32. package/locales/ja-JP/tool.json +12 -1
  33. package/locales/ko-KR/chat.json +5 -0
  34. package/locales/ko-KR/models.json +15 -0
  35. package/locales/ko-KR/tool.json +12 -1
  36. package/locales/nl-NL/chat.json +5 -0
  37. package/locales/nl-NL/models.json +15 -0
  38. package/locales/nl-NL/tool.json +12 -1
  39. package/locales/pl-PL/chat.json +5 -0
  40. package/locales/pl-PL/models.json +15 -0
  41. package/locales/pl-PL/tool.json +12 -1
  42. package/locales/pt-BR/chat.json +5 -0
  43. package/locales/pt-BR/models.json +15 -0
  44. package/locales/pt-BR/tool.json +12 -1
  45. package/locales/ru-RU/chat.json +5 -0
  46. package/locales/ru-RU/models.json +15 -0
  47. package/locales/ru-RU/tool.json +12 -1
  48. package/locales/tr-TR/chat.json +5 -0
  49. package/locales/tr-TR/models.json +15 -0
  50. package/locales/tr-TR/tool.json +12 -1
  51. package/locales/vi-VN/chat.json +5 -0
  52. package/locales/vi-VN/models.json +15 -0
  53. package/locales/vi-VN/tool.json +12 -1
  54. package/locales/zh-CN/models.json +15 -0
  55. package/locales/zh-CN/tool.json +12 -1
  56. package/locales/zh-TW/chat.json +5 -0
  57. package/locales/zh-TW/models.json +15 -0
  58. package/locales/zh-TW/tool.json +12 -1
  59. package/package.json +2 -1
  60. package/packages/electron-client-ipc/src/types/localSystem.ts +4 -0
  61. package/scripts/prebuild.mts +15 -5
  62. package/src/app/[variants]/desktopRouter.config.tsx +0 -17
  63. package/src/app/[variants]/mobileRouter.config.tsx +0 -16
  64. package/src/app/[variants]/page.tsx +5 -4
  65. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +23 -4
  66. package/src/locales/default/tool.ts +11 -0
  67. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +5 -6
  68. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +45 -182
  69. package/src/tools/executionRuntimes.ts +3 -0
  70. package/src/tools/local-system/ExecutionRuntime/index.ts +407 -0
  71. package/src/tools/local-system/Intervention/EditLocalFile/index.tsx +89 -0
  72. package/src/tools/local-system/Intervention/WriteFile/index.tsx +72 -0
  73. package/src/tools/local-system/Intervention/index.ts +4 -0
  74. package/src/tools/local-system/Render/EditLocalFile/index.tsx +67 -0
  75. package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +53 -78
  76. package/src/tools/local-system/Render/index.ts +2 -0
  77. package/src/tools/local-system/index.ts +1 -0
  78. package/src/tools/local-system/type.ts +4 -3
@@ -0,0 +1,407 @@
1
+ import {
2
+ EditLocalFileParams,
3
+ EditLocalFileResult,
4
+ GetCommandOutputParams,
5
+ GetCommandOutputResult,
6
+ GlobFilesParams,
7
+ GlobFilesResult,
8
+ GrepContentParams,
9
+ GrepContentResult,
10
+ KillCommandParams,
11
+ KillCommandResult,
12
+ ListLocalFileParams,
13
+ LocalFileItem,
14
+ LocalMoveFilesResultItem,
15
+ LocalReadFileParams,
16
+ LocalReadFileResult,
17
+ LocalReadFilesParams,
18
+ LocalSearchFilesParams,
19
+ MoveLocalFilesParams,
20
+ RenameLocalFileParams,
21
+ RenameLocalFileResult,
22
+ RunCommandParams,
23
+ RunCommandResult,
24
+ WriteLocalFileParams,
25
+ } from '@lobechat/electron-client-ipc';
26
+ import { BuiltinServerRuntimeOutput } from '@lobechat/types';
27
+
28
+ import { localFileService } from '@/services/electron/localFileService';
29
+
30
+ import {
31
+ EditLocalFileState,
32
+ GetCommandOutputState,
33
+ GlobFilesState,
34
+ GrepContentState,
35
+ KillCommandState,
36
+ LocalFileListState,
37
+ LocalFileSearchState,
38
+ LocalMoveFilesState,
39
+ LocalReadFileState,
40
+ LocalReadFilesState,
41
+ LocalRenameFileState,
42
+ RunCommandState,
43
+ } from '../type';
44
+
45
+ export class LocalSystemExecutionRuntime {
46
+ // ==================== File Operations ====================
47
+
48
+ async listLocalFiles(args: ListLocalFileParams): Promise<BuiltinServerRuntimeOutput> {
49
+ try {
50
+ const result: LocalFileItem[] = await localFileService.listLocalFiles(args);
51
+
52
+ const state: LocalFileListState = { listResults: result };
53
+
54
+ return {
55
+ content: JSON.stringify(result),
56
+ state,
57
+ success: true,
58
+ };
59
+ } catch (error) {
60
+ return {
61
+ content: (error as Error).message,
62
+ error,
63
+ success: false,
64
+ };
65
+ }
66
+ }
67
+
68
+ async readLocalFile(args: LocalReadFileParams): Promise<BuiltinServerRuntimeOutput> {
69
+ try {
70
+ const result: LocalReadFileResult = await localFileService.readLocalFile(args);
71
+
72
+ const state: LocalReadFileState = { fileContent: result };
73
+
74
+ return {
75
+ content: JSON.stringify(result),
76
+ state,
77
+ success: true,
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ content: (error as Error).message,
82
+ error,
83
+ success: false,
84
+ };
85
+ }
86
+ }
87
+
88
+ async readLocalFiles(args: LocalReadFilesParams): Promise<BuiltinServerRuntimeOutput> {
89
+ try {
90
+ const results: LocalReadFileResult[] = await localFileService.readLocalFiles(args);
91
+
92
+ const state: LocalReadFilesState = { filesContent: results };
93
+
94
+ return {
95
+ content: JSON.stringify(results),
96
+ state,
97
+ success: true,
98
+ };
99
+ } catch (error) {
100
+ return {
101
+ content: (error as Error).message,
102
+ error,
103
+ success: false,
104
+ };
105
+ }
106
+ }
107
+
108
+ async searchLocalFiles(args: LocalSearchFilesParams): Promise<BuiltinServerRuntimeOutput> {
109
+ try {
110
+ const result: LocalFileItem[] = await localFileService.searchLocalFiles(args);
111
+
112
+ const state: LocalFileSearchState = { searchResults: result };
113
+
114
+ return {
115
+ content: JSON.stringify(result),
116
+ state,
117
+ success: true,
118
+ };
119
+ } catch (error) {
120
+ return {
121
+ content: (error as Error).message,
122
+ error,
123
+ success: false,
124
+ };
125
+ }
126
+ }
127
+
128
+ async moveLocalFiles(args: MoveLocalFilesParams): Promise<BuiltinServerRuntimeOutput> {
129
+ try {
130
+ const results: LocalMoveFilesResultItem[] = await localFileService.moveLocalFiles(args);
131
+
132
+ const allSucceeded = results.every((r) => r.success);
133
+ const someFailed = results.some((r) => !r.success);
134
+ const successCount = results.filter((r) => r.success).length;
135
+ const failedCount = results.length - successCount;
136
+
137
+ let message = '';
138
+
139
+ if (allSucceeded) {
140
+ message = `Successfully moved ${results.length} item(s).`;
141
+ } else if (someFailed) {
142
+ message = `Moved ${successCount} item(s) successfully. Failed to move ${failedCount} item(s).`;
143
+ } else {
144
+ message = `Failed to move all ${results.length} item(s).`;
145
+ }
146
+
147
+ const state: LocalMoveFilesState = {
148
+ results,
149
+ successCount,
150
+ totalCount: results.length,
151
+ };
152
+
153
+ return {
154
+ content: JSON.stringify({ message, results }),
155
+ state,
156
+ success: true,
157
+ };
158
+ } catch (error) {
159
+ return {
160
+ content: (error as Error).message,
161
+ error,
162
+ success: false,
163
+ };
164
+ }
165
+ }
166
+
167
+ async renameLocalFile(args: RenameLocalFileParams): Promise<BuiltinServerRuntimeOutput> {
168
+ try {
169
+ const result: RenameLocalFileResult = await localFileService.renameLocalFile(args);
170
+
171
+ if (!result.success) {
172
+ const state: LocalRenameFileState = {
173
+ error: result.error,
174
+ newPath: '',
175
+ oldPath: args.path,
176
+ success: false,
177
+ };
178
+
179
+ return {
180
+ content: JSON.stringify({ message: result.error, success: false }),
181
+ state,
182
+ success: false,
183
+ };
184
+ }
185
+
186
+ const state: LocalRenameFileState = {
187
+ newPath: result.newPath!,
188
+ oldPath: args.path,
189
+ success: true,
190
+ };
191
+
192
+ return {
193
+ content: JSON.stringify({
194
+ message: `Successfully renamed file ${args.path} to ${args.newName}.`,
195
+ success: true,
196
+ }),
197
+ state,
198
+ success: true,
199
+ };
200
+ } catch (error) {
201
+ return {
202
+ content: (error as Error).message,
203
+ error,
204
+ success: false,
205
+ };
206
+ }
207
+ }
208
+
209
+ async writeLocalFile(args: WriteLocalFileParams): Promise<BuiltinServerRuntimeOutput> {
210
+ try {
211
+ const result = await localFileService.writeFile(args);
212
+
213
+ if (!result.success) {
214
+ return {
215
+ content: JSON.stringify({
216
+ message: result.error || '写入文件失败',
217
+ success: false,
218
+ }),
219
+ error: result.error,
220
+ success: false,
221
+ };
222
+ }
223
+
224
+ return {
225
+ content: JSON.stringify({
226
+ message: `成功写入文件 ${args.path}`,
227
+ success: true,
228
+ }),
229
+ success: true,
230
+ };
231
+ } catch (error) {
232
+ return {
233
+ content: (error as Error).message,
234
+ error,
235
+ success: false,
236
+ };
237
+ }
238
+ }
239
+
240
+ async editLocalFile(args: EditLocalFileParams): Promise<BuiltinServerRuntimeOutput> {
241
+ try {
242
+ const result: EditLocalFileResult = await localFileService.editLocalFile(args);
243
+
244
+ if (!result.success) {
245
+ return {
246
+ content: `Edit failed: ${result.error}`,
247
+ success: false,
248
+ };
249
+ }
250
+
251
+ const statsText =
252
+ result.linesAdded || result.linesDeleted
253
+ ? ` (+${result.linesAdded || 0} -${result.linesDeleted || 0})`
254
+ : '';
255
+ const message = `Successfully replaced ${result.replacements} occurrence(s) in ${args.file_path}${statsText}`;
256
+
257
+ const state: EditLocalFileState = {
258
+ diffText: result.diffText,
259
+ linesAdded: result.linesAdded,
260
+ linesDeleted: result.linesDeleted,
261
+ replacements: result.replacements,
262
+ };
263
+
264
+ return {
265
+ content: message,
266
+ state,
267
+ success: true,
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ content: (error as Error).message,
272
+ error,
273
+ success: false,
274
+ };
275
+ }
276
+ }
277
+
278
+ // ==================== Shell Commands ====================
279
+
280
+ async runCommand(args: RunCommandParams): Promise<BuiltinServerRuntimeOutput> {
281
+ try {
282
+ const result: RunCommandResult = await localFileService.runCommand(args);
283
+
284
+ let message: string;
285
+
286
+ if (result.success) {
287
+ if (result.shell_id) {
288
+ message = `Command started in background with shell_id: ${result.shell_id}`;
289
+ } else {
290
+ message = `Command completed successfully.`;
291
+ }
292
+ } else {
293
+ message = `Command failed: ${result.error}`;
294
+ }
295
+
296
+ const state: RunCommandState = { message, result };
297
+
298
+ return {
299
+ content: JSON.stringify(result),
300
+ state,
301
+ success: result.success,
302
+ };
303
+ } catch (error) {
304
+ return {
305
+ content: (error as Error).message,
306
+ error,
307
+ success: false,
308
+ };
309
+ }
310
+ }
311
+
312
+ async getCommandOutput(args: GetCommandOutputParams): Promise<BuiltinServerRuntimeOutput> {
313
+ try {
314
+ const result: GetCommandOutputResult = await localFileService.getCommandOutput(args);
315
+
316
+ const message = result.success
317
+ ? `Output retrieved. Running: ${result.running}`
318
+ : `Failed: ${result.error}`;
319
+
320
+ const state: GetCommandOutputState = { message, result };
321
+
322
+ return {
323
+ content: JSON.stringify(result),
324
+ state,
325
+ success: result.success,
326
+ };
327
+ } catch (error) {
328
+ return {
329
+ content: (error as Error).message,
330
+ error,
331
+ success: false,
332
+ };
333
+ }
334
+ }
335
+
336
+ async killCommand(args: KillCommandParams): Promise<BuiltinServerRuntimeOutput> {
337
+ try {
338
+ const result: KillCommandResult = await localFileService.killCommand(args);
339
+
340
+ const message = result.success
341
+ ? `Successfully killed shell: ${args.shell_id}`
342
+ : `Failed to kill shell: ${result.error}`;
343
+
344
+ const state: KillCommandState = { message, result };
345
+
346
+ return {
347
+ content: JSON.stringify(result),
348
+ state,
349
+ success: result.success,
350
+ };
351
+ } catch (error) {
352
+ return {
353
+ content: (error as Error).message,
354
+ error,
355
+ success: false,
356
+ };
357
+ }
358
+ }
359
+
360
+ // ==================== Search & Find ====================
361
+
362
+ async grepContent(args: GrepContentParams): Promise<BuiltinServerRuntimeOutput> {
363
+ try {
364
+ const result: GrepContentResult = await localFileService.grepContent(args);
365
+
366
+ const message = result.success
367
+ ? `Found ${result.total_matches} matches in ${result.matches.length} locations`
368
+ : 'Search failed';
369
+
370
+ const state: GrepContentState = { message, result };
371
+
372
+ return {
373
+ content: JSON.stringify(result),
374
+ state,
375
+ success: result.success,
376
+ };
377
+ } catch (error) {
378
+ return {
379
+ content: (error as Error).message,
380
+ error,
381
+ success: false,
382
+ };
383
+ }
384
+ }
385
+
386
+ async globLocalFiles(args: GlobFilesParams): Promise<BuiltinServerRuntimeOutput> {
387
+ try {
388
+ const result: GlobFilesResult = await localFileService.globFiles(args);
389
+
390
+ const message = result.success ? `Found ${result.total_files} files` : 'Glob search failed';
391
+
392
+ const state: GlobFilesState = { message, result };
393
+
394
+ return {
395
+ content: JSON.stringify(result),
396
+ state,
397
+ success: result.success,
398
+ };
399
+ } catch (error) {
400
+ return {
401
+ content: (error as Error).message,
402
+ error,
403
+ success: false,
404
+ };
405
+ }
406
+ }
407
+ }
@@ -0,0 +1,89 @@
1
+ import { EditLocalFileParams } from '@lobechat/electron-client-ipc';
2
+ import { BuiltinInterventionProps } from '@lobechat/types';
3
+ import { Icon, Text } from '@lobehub/ui';
4
+ import { Skeleton } from 'antd';
5
+ import { createPatch } from 'diff';
6
+ import { ChevronRight } from 'lucide-react';
7
+ import path from 'path-browserify-esm';
8
+ import React, { memo, useMemo } from 'react';
9
+ import { Diff, Hunk, parseDiff } from 'react-diff-view';
10
+ import 'react-diff-view/style/index.css';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { Flexbox } from 'react-layout-kit';
13
+ import useSWR from 'swr';
14
+
15
+ import { LocalFile, LocalFolder } from '@/features/LocalFile';
16
+ import { localFileService } from '@/services/electron/localFileService';
17
+
18
+ const EditLocalFile = memo<BuiltinInterventionProps<EditLocalFileParams>>(({ args }) => {
19
+ const { t } = useTranslation('tool');
20
+ const { base, dir } = path.parse(args.file_path);
21
+
22
+ // Fetch full file content
23
+ const { data: fileData, isLoading } = useSWR(
24
+ ['readLocalFile', args.file_path],
25
+ () => localFileService.readLocalFile({ fullContent: true, path: args.file_path }),
26
+ {
27
+ revalidateOnFocus: false,
28
+ revalidateOnReconnect: false,
29
+ },
30
+ );
31
+
32
+ // Generate diff from full file content
33
+ const files = useMemo(() => {
34
+ if (!fileData?.content) return [];
35
+
36
+ try {
37
+ const oldContent = fileData.content;
38
+
39
+ // Generate new content by applying the replacement
40
+ const newContent = args.replace_all
41
+ ? oldContent.replaceAll(args.old_string, args.new_string)
42
+ : oldContent.replace(args.old_string, args.new_string);
43
+
44
+ // Use createPatch to generate unified diff with full file content
45
+ const patch = createPatch(args.file_path, oldContent, newContent, '', '');
46
+
47
+ // Add git diff header for parseDiff compatibility
48
+ const diffText = `diff --git a${args.file_path} b${args.file_path}\n${patch}`;
49
+
50
+ return parseDiff(diffText);
51
+ } catch (error) {
52
+ console.error('Failed to generate diff:', error);
53
+ return [];
54
+ }
55
+ }, [fileData?.content, args.file_path, args.old_string, args.new_string, args.replace_all]);
56
+
57
+ return (
58
+ <Flexbox gap={12}>
59
+ <Flexbox horizontal>
60
+ <LocalFolder path={dir} />
61
+ <Icon icon={ChevronRight} />
62
+ <LocalFile name={base} path={args.file_path} />
63
+ </Flexbox>
64
+
65
+ {isLoading ? (
66
+ <Skeleton active paragraph={{ rows: 3 }} />
67
+ ) : (
68
+ <Flexbox gap={8}>
69
+ <Text type="secondary">
70
+ {args.replace_all
71
+ ? t('localFiles.editFile.replaceAll')
72
+ : t('localFiles.editFile.replaceFirst')}
73
+ </Text>
74
+ {files.map((file, index) => (
75
+ <div key={`${file.oldPath}-${index}`} style={{ fontSize: '12px' }}>
76
+ <Diff diffType={file.type} hunks={file.hunks} viewType="split">
77
+ {(hunks) => hunks.map((hunk) => <Hunk hunk={hunk} key={hunk.content} />)}
78
+ </Diff>
79
+ </div>
80
+ ))}
81
+ </Flexbox>
82
+ )}
83
+ </Flexbox>
84
+ );
85
+ });
86
+
87
+ EditLocalFile.displayName = 'EditLocalFileIntervention';
88
+
89
+ export default EditLocalFile;
@@ -0,0 +1,72 @@
1
+ import { WriteLocalFileParams } from '@lobechat/electron-client-ipc';
2
+ import { BuiltinInterventionProps } from '@lobechat/types';
3
+ import { Highlighter, Icon, Text } from '@lobehub/ui';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import path from 'path-browserify-esm';
6
+ import React, { memo, useMemo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { LocalFile, LocalFolder } from '@/features/LocalFile';
11
+
12
+ const WriteFile = memo<BuiltinInterventionProps<WriteLocalFileParams>>(({ args }) => {
13
+ const { t } = useTranslation('tool');
14
+ const { base, dir, ext } = path.parse(args.path);
15
+
16
+ // Detect language from file extension
17
+ const language = useMemo(() => {
18
+ const extMap: Record<string, string> = {
19
+ css: 'css',
20
+ html: 'html',
21
+ js: 'javascript',
22
+ json: 'json',
23
+ jsx: 'jsx',
24
+ md: 'markdown',
25
+ py: 'python',
26
+ sh: 'bash',
27
+ ts: 'typescript',
28
+ tsx: 'tsx',
29
+ txt: 'text',
30
+ xml: 'xml',
31
+ yaml: 'yaml',
32
+ yml: 'yaml',
33
+ };
34
+ return extMap[ext.replace('.', '')] || 'text';
35
+ }, [ext]);
36
+
37
+ const contentLength = args.content?.length || 0;
38
+
39
+ return (
40
+ <Flexbox gap={12}>
41
+ <Flexbox horizontal>
42
+ <LocalFolder path={dir} />
43
+ <Icon icon={ChevronRight} />
44
+ <LocalFile name={base} path={args.path} />
45
+ </Flexbox>
46
+
47
+ <Flexbox gap={4}>
48
+ <Flexbox horizontal justify={'space-between'}>
49
+ <Text type="secondary">{t('localFiles.writeFile.preview')}</Text>
50
+ <Text style={{ fontSize: 12 }} type={'secondary'}>
51
+ {contentLength.toLocaleString()} {t('localFiles.writeFile.characters')}
52
+ </Text>
53
+ </Flexbox>
54
+
55
+ {args.content && (
56
+ <Highlighter
57
+ language={language}
58
+ showLanguage={false}
59
+ style={{ maxHeight: 400, overflow: 'auto', padding: '8px' }}
60
+ variant={'outlined'}
61
+ >
62
+ {args.content}
63
+ </Highlighter>
64
+ )}
65
+ </Flexbox>
66
+ </Flexbox>
67
+ );
68
+ });
69
+
70
+ WriteFile.displayName = 'WriteFileIntervention';
71
+
72
+ export default WriteFile;
@@ -1,11 +1,15 @@
1
1
  import { LocalSystemApiName } from '../index';
2
+ import EditLocalFile from './EditLocalFile';
2
3
  import MoveLocalFiles from './MoveLocalFiles';
3
4
  import RunCommand from './RunCommand';
5
+ import WriteFile from './WriteFile';
4
6
 
5
7
  /**
6
8
  * Local System Intervention Components Registry
7
9
  */
8
10
  export const LocalSystemInterventions = {
11
+ [LocalSystemApiName.editLocalFile]: EditLocalFile,
9
12
  [LocalSystemApiName.moveLocalFiles]: MoveLocalFiles,
10
13
  [LocalSystemApiName.runCommand]: RunCommand,
14
+ [LocalSystemApiName.writeLocalFile]: WriteFile,
11
15
  };
@@ -0,0 +1,67 @@
1
+ import { EditLocalFileParams } from '@lobechat/electron-client-ipc';
2
+ import { BuiltinRenderProps } from '@lobechat/types';
3
+ import { Alert, Icon } from '@lobehub/ui';
4
+ import { Skeleton } from 'antd';
5
+ import { ChevronRight } from 'lucide-react';
6
+ import path from 'path-browserify-esm';
7
+ import React, { memo, useMemo } from 'react';
8
+ import { Diff, Hunk, parseDiff } from 'react-diff-view';
9
+ import 'react-diff-view/style/index.css';
10
+ import { Flexbox } from 'react-layout-kit';
11
+
12
+ import { LocalFile, LocalFolder } from '@/features/LocalFile';
13
+
14
+ import { EditLocalFileState } from '../../type';
15
+
16
+ const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFileState>>(
17
+ ({ args, pluginState, pluginError }) => {
18
+ const { base, dir } = path.parse(args.file_path);
19
+
20
+ // Parse diff for react-diff-view
21
+ const files = useMemo(() => {
22
+ const diffText = pluginState?.diffText;
23
+ if (!diffText) return [];
24
+
25
+ try {
26
+ return parseDiff(diffText);
27
+ } catch (error) {
28
+ console.error('Failed to parse diff:', error);
29
+ return [];
30
+ }
31
+ }, [pluginState?.diffText]);
32
+
33
+ if (!args) return <Skeleton active />;
34
+
35
+ return (
36
+ <Flexbox gap={12}>
37
+ <Flexbox horizontal>
38
+ <LocalFolder path={dir} />
39
+ <Icon icon={ChevronRight} />
40
+ <LocalFile name={base} path={args.file_path} />
41
+ </Flexbox>
42
+ {pluginError ? (
43
+ <Alert
44
+ description={pluginError.message || 'Unknown error occurred'}
45
+ message="Edit Failed"
46
+ showIcon
47
+ type="error"
48
+ />
49
+ ) : (
50
+ <Flexbox gap={12}>
51
+ {files.map((file, index) => (
52
+ <div key={`${file.oldPath}-${index}`} style={{ fontSize: '12px' }}>
53
+ <Diff diffType={file.type} gutterType="default" hunks={file.hunks} viewType="split">
54
+ {(hunks) => hunks.map((hunk) => <Hunk hunk={hunk} key={hunk.content} />)}
55
+ </Diff>
56
+ </div>
57
+ ))}
58
+ </Flexbox>
59
+ )}
60
+ </Flexbox>
61
+ );
62
+ },
63
+ );
64
+
65
+ EditLocalFile.displayName = 'EditLocalFile';
66
+
67
+ export default EditLocalFile;