@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.
- package/.github/workflows/desktop-pr-build.yml +7 -3
- package/CHANGELOG.md +50 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +55 -11
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +153 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +5 -0
- package/locales/ar/models.json +15 -0
- package/locales/ar/tool.json +12 -1
- package/locales/bg-BG/chat.json +5 -0
- package/locales/bg-BG/models.json +15 -0
- package/locales/bg-BG/tool.json +12 -1
- package/locales/de-DE/chat.json +5 -0
- package/locales/de-DE/models.json +15 -0
- package/locales/de-DE/tool.json +12 -1
- package/locales/en-US/models.json +15 -0
- package/locales/en-US/tool.json +12 -1
- package/locales/es-ES/chat.json +5 -0
- package/locales/es-ES/models.json +15 -0
- package/locales/es-ES/tool.json +12 -1
- package/locales/fa-IR/chat.json +5 -0
- package/locales/fa-IR/models.json +15 -0
- package/locales/fa-IR/tool.json +12 -1
- package/locales/fr-FR/chat.json +5 -0
- package/locales/fr-FR/models.json +15 -0
- package/locales/fr-FR/tool.json +12 -1
- package/locales/it-IT/chat.json +5 -0
- package/locales/it-IT/models.json +15 -0
- package/locales/it-IT/tool.json +12 -1
- package/locales/ja-JP/chat.json +5 -0
- package/locales/ja-JP/models.json +15 -0
- package/locales/ja-JP/tool.json +12 -1
- package/locales/ko-KR/chat.json +5 -0
- package/locales/ko-KR/models.json +15 -0
- package/locales/ko-KR/tool.json +12 -1
- package/locales/nl-NL/chat.json +5 -0
- package/locales/nl-NL/models.json +15 -0
- package/locales/nl-NL/tool.json +12 -1
- package/locales/pl-PL/chat.json +5 -0
- package/locales/pl-PL/models.json +15 -0
- package/locales/pl-PL/tool.json +12 -1
- package/locales/pt-BR/chat.json +5 -0
- package/locales/pt-BR/models.json +15 -0
- package/locales/pt-BR/tool.json +12 -1
- package/locales/ru-RU/chat.json +5 -0
- package/locales/ru-RU/models.json +15 -0
- package/locales/ru-RU/tool.json +12 -1
- package/locales/tr-TR/chat.json +5 -0
- package/locales/tr-TR/models.json +15 -0
- package/locales/tr-TR/tool.json +12 -1
- package/locales/vi-VN/chat.json +5 -0
- package/locales/vi-VN/models.json +15 -0
- package/locales/vi-VN/tool.json +12 -1
- package/locales/zh-CN/models.json +15 -0
- package/locales/zh-CN/tool.json +12 -1
- package/locales/zh-TW/chat.json +5 -0
- package/locales/zh-TW/models.json +15 -0
- package/locales/zh-TW/tool.json +12 -1
- package/package.json +2 -1
- package/packages/electron-client-ipc/src/types/localSystem.ts +4 -0
- package/scripts/prebuild.mts +15 -5
- package/src/app/[variants]/desktopRouter.config.tsx +0 -17
- package/src/app/[variants]/mobileRouter.config.tsx +0 -16
- package/src/app/[variants]/page.tsx +5 -4
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +23 -4
- package/src/locales/default/tool.ts +11 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +5 -6
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +45 -182
- package/src/tools/executionRuntimes.ts +3 -0
- package/src/tools/local-system/ExecutionRuntime/index.ts +407 -0
- package/src/tools/local-system/Intervention/EditLocalFile/index.tsx +89 -0
- package/src/tools/local-system/Intervention/WriteFile/index.tsx +72 -0
- package/src/tools/local-system/Intervention/index.ts +4 -0
- package/src/tools/local-system/Render/EditLocalFile/index.tsx +67 -0
- package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +53 -78
- package/src/tools/local-system/Render/index.ts +2 -0
- package/src/tools/local-system/index.ts +1 -0
- 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;
|