@lobehub/lobehub 2.0.0-next.73 → 2.0.0-next.74
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 +25 -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 +9 -0
- package/locales/en-US/tool.json +12 -1
- package/locales/zh-CN/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: Desktop PR Build
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
4
|
+
pull_request:
|
|
5
5
|
types: [synchronize, labeled, unlabeled] # PR 更新或标签变化时触发
|
|
6
6
|
|
|
7
7
|
# 确保同一 PR 同一时间只运行一个相同的 workflow,取消正在进行的旧的运行
|
|
@@ -126,6 +126,7 @@ jobs:
|
|
|
126
126
|
run: npm run workflow:set-desktop-version ${{ needs.version.outputs.version }} nightly
|
|
127
127
|
|
|
128
128
|
# macOS 构建处理
|
|
129
|
+
# 注意:fork 的 PR 无法访问 secrets,会构建未签名版本
|
|
129
130
|
- name: Build artifact on macOS
|
|
130
131
|
if: runner.os == 'macOS'
|
|
131
132
|
run: npm run desktop:build
|
|
@@ -136,7 +137,7 @@ jobs:
|
|
|
136
137
|
DATABASE_URL: "postgresql://postgres@localhost:5432/postgres"
|
|
137
138
|
# 默认添加一个加密 SECRET
|
|
138
139
|
KEY_VAULTS_SECRET: "oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE="
|
|
139
|
-
# macOS
|
|
140
|
+
# macOS 签名和公证配置(fork 的 PR 访问不到 secrets,会跳过签名)
|
|
140
141
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
|
141
142
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
142
143
|
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
|
|
@@ -148,7 +149,8 @@ jobs:
|
|
|
148
149
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
149
150
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
150
151
|
|
|
151
|
-
#
|
|
152
|
+
# Windows 平台构建处理
|
|
153
|
+
# 注意:fork 的 PR 无法访问 secrets,会构建未签名版本
|
|
152
154
|
- name: Build artifact on Windows
|
|
153
155
|
if: runner.os == 'Windows'
|
|
154
156
|
run: npm run desktop:build
|
|
@@ -275,6 +277,8 @@ jobs:
|
|
|
275
277
|
publish-pr:
|
|
276
278
|
needs: [merge-mac-files, version]
|
|
277
279
|
name: Publish PR Build
|
|
280
|
+
# 只为非 fork 的 PR 发布(fork 的 PR 没有写权限)
|
|
281
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
278
282
|
runs-on: ubuntu-latest
|
|
279
283
|
# Grant write permissions for creating release and commenting on PR
|
|
280
284
|
permissions:
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.74](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.73...v2.0.0-next.74)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-11-17**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: Edit local file render & intervention.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: Edit local file render & intervention, closes [#10269](https://github.com/lobehub/lobe-chat/issues/10269) ([3785a71](https://github.com/lobehub/lobe-chat/commit/3785a71))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.73](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.72...v2.0.0-next.73)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-11-17**</sup>
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
WriteLocalFileParams,
|
|
19
19
|
} from '@lobechat/electron-client-ipc';
|
|
20
20
|
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
|
|
21
|
+
import { createPatch } from 'diff';
|
|
21
22
|
import { shell } from 'electron';
|
|
22
23
|
import fg from 'fast-glob';
|
|
23
24
|
import { Stats, constants } from 'node:fs';
|
|
@@ -94,26 +95,45 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
@ipcClientEvent('readLocalFile')
|
|
97
|
-
async readFile({
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
async readFile({
|
|
99
|
+
path: filePath,
|
|
100
|
+
loc,
|
|
101
|
+
fullContent,
|
|
102
|
+
}: LocalReadFileParams): Promise<LocalReadFileResult> {
|
|
103
|
+
const effectiveLoc = fullContent ? undefined : (loc ?? [0, 200]);
|
|
104
|
+
logger.debug('Starting to read file:', { filePath, fullContent, loc: effectiveLoc });
|
|
100
105
|
|
|
101
106
|
try {
|
|
102
107
|
const fileDocument = await loadFile(filePath);
|
|
103
108
|
|
|
104
|
-
const [startLine, endLine] = effectiveLoc;
|
|
105
109
|
const lines = fileDocument.content.split('\n');
|
|
106
110
|
const totalLineCount = lines.length;
|
|
107
111
|
const totalCharCount = fileDocument.content.length;
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
let content: string;
|
|
114
|
+
let charCount: number;
|
|
115
|
+
let lineCount: number;
|
|
116
|
+
let actualLoc: [number, number];
|
|
117
|
+
|
|
118
|
+
if (effectiveLoc === undefined) {
|
|
119
|
+
// Return full content
|
|
120
|
+
content = fileDocument.content;
|
|
121
|
+
charCount = totalCharCount;
|
|
122
|
+
lineCount = totalLineCount;
|
|
123
|
+
actualLoc = [0, totalLineCount];
|
|
124
|
+
} else {
|
|
125
|
+
// Return specified range
|
|
126
|
+
const [startLine, endLine] = effectiveLoc;
|
|
127
|
+
const selectedLines = lines.slice(startLine, endLine);
|
|
128
|
+
content = selectedLines.join('\n');
|
|
129
|
+
charCount = content.length;
|
|
130
|
+
lineCount = selectedLines.length;
|
|
131
|
+
actualLoc = effectiveLoc;
|
|
132
|
+
}
|
|
114
133
|
|
|
115
134
|
logger.debug('File read successfully:', {
|
|
116
135
|
filePath,
|
|
136
|
+
fullContent,
|
|
117
137
|
selectedLineCount: lineCount,
|
|
118
138
|
totalCharCount,
|
|
119
139
|
totalLineCount,
|
|
@@ -128,7 +148,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
128
148
|
fileType: fileDocument.fileType,
|
|
129
149
|
filename: fileDocument.filename,
|
|
130
150
|
lineCount,
|
|
131
|
-
loc:
|
|
151
|
+
loc: actualLoc,
|
|
132
152
|
// Line count for the selected range
|
|
133
153
|
modifiedTime: fileDocument.modifiedTime,
|
|
134
154
|
|
|
@@ -711,8 +731,32 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
711
731
|
// Write back to file
|
|
712
732
|
await writeFile(filePath, newContent, 'utf8');
|
|
713
733
|
|
|
714
|
-
|
|
734
|
+
// Generate diff for UI display
|
|
735
|
+
const patch = createPatch(filePath, content, newContent, '', '');
|
|
736
|
+
const diffText = `diff --git a${filePath} b${filePath}\n${patch}`;
|
|
737
|
+
|
|
738
|
+
// Calculate lines added and deleted from patch
|
|
739
|
+
const patchLines = patch.split('\n');
|
|
740
|
+
let linesAdded = 0;
|
|
741
|
+
let linesDeleted = 0;
|
|
742
|
+
|
|
743
|
+
for (const line of patchLines) {
|
|
744
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
745
|
+
linesAdded++;
|
|
746
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
747
|
+
linesDeleted++;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
logger.info(`${logPrefix} File edited successfully`, {
|
|
752
|
+
linesAdded,
|
|
753
|
+
linesDeleted,
|
|
754
|
+
replacements,
|
|
755
|
+
});
|
|
715
756
|
return {
|
|
757
|
+
diffText,
|
|
758
|
+
linesAdded,
|
|
759
|
+
linesDeleted,
|
|
716
760
|
replacements,
|
|
717
761
|
success: true,
|
|
718
762
|
};
|
|
@@ -183,6 +183,26 @@ describe('LocalFileCtr', () => {
|
|
|
183
183
|
expect(result.totalLineCount).toBe(5);
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
it('should read full file content when fullContent is true', async () => {
|
|
187
|
+
const mockFileContent = 'line1\nline2\nline3\nline4\nline5';
|
|
188
|
+
vi.mocked(mockLoadFile).mockResolvedValue({
|
|
189
|
+
content: mockFileContent,
|
|
190
|
+
filename: 'test.txt',
|
|
191
|
+
fileType: 'txt',
|
|
192
|
+
createdTime: new Date('2024-01-01'),
|
|
193
|
+
modifiedTime: new Date('2024-01-02'),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await localFileCtr.readFile({ path: '/test/file.txt', fullContent: true });
|
|
197
|
+
|
|
198
|
+
expect(result.content).toBe(mockFileContent);
|
|
199
|
+
expect(result.lineCount).toBe(5);
|
|
200
|
+
expect(result.charCount).toBe(mockFileContent.length);
|
|
201
|
+
expect(result.totalLineCount).toBe(5);
|
|
202
|
+
expect(result.totalCharCount).toBe(mockFileContent.length);
|
|
203
|
+
expect(result.loc).toEqual([0, 5]);
|
|
204
|
+
});
|
|
205
|
+
|
|
186
206
|
it('should handle file read error', async () => {
|
|
187
207
|
vi.mocked(mockLoadFile).mockRejectedValue(new Error('File not found'));
|
|
188
208
|
|
|
@@ -392,4 +412,137 @@ describe('LocalFileCtr', () => {
|
|
|
392
412
|
});
|
|
393
413
|
});
|
|
394
414
|
});
|
|
415
|
+
|
|
416
|
+
describe('handleEditFile', () => {
|
|
417
|
+
it('should replace first occurrence successfully', async () => {
|
|
418
|
+
const originalContent = 'Hello world\nHello again\nGoodbye world';
|
|
419
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue(originalContent);
|
|
420
|
+
vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined);
|
|
421
|
+
|
|
422
|
+
const result = await localFileCtr.handleEditFile({
|
|
423
|
+
file_path: '/test/file.txt',
|
|
424
|
+
old_string: 'Hello',
|
|
425
|
+
new_string: 'Hi',
|
|
426
|
+
replace_all: false,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
expect(result.success).toBe(true);
|
|
430
|
+
expect(result.replacements).toBe(1);
|
|
431
|
+
expect(result.linesAdded).toBe(1);
|
|
432
|
+
expect(result.linesDeleted).toBe(1);
|
|
433
|
+
expect(result.diffText).toContain('diff --git a/test/file.txt b/test/file.txt');
|
|
434
|
+
expect(mockFsPromises.writeFile).toHaveBeenCalledWith(
|
|
435
|
+
'/test/file.txt',
|
|
436
|
+
'Hi world\nHello again\nGoodbye world',
|
|
437
|
+
'utf8',
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should replace all occurrences when replace_all is true', async () => {
|
|
442
|
+
const originalContent = 'Hello world\nHello again\nHello there';
|
|
443
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue(originalContent);
|
|
444
|
+
vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined);
|
|
445
|
+
|
|
446
|
+
const result = await localFileCtr.handleEditFile({
|
|
447
|
+
file_path: '/test/file.txt',
|
|
448
|
+
old_string: 'Hello',
|
|
449
|
+
new_string: 'Hi',
|
|
450
|
+
replace_all: true,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(result.success).toBe(true);
|
|
454
|
+
expect(result.replacements).toBe(3);
|
|
455
|
+
expect(result.linesAdded).toBe(3);
|
|
456
|
+
expect(result.linesDeleted).toBe(3);
|
|
457
|
+
expect(mockFsPromises.writeFile).toHaveBeenCalledWith(
|
|
458
|
+
'/test/file.txt',
|
|
459
|
+
'Hi world\nHi again\nHi there',
|
|
460
|
+
'utf8',
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should handle multiline replacement correctly', async () => {
|
|
465
|
+
const originalContent = 'function test() {\n console.log("old");\n}';
|
|
466
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue(originalContent);
|
|
467
|
+
vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined);
|
|
468
|
+
|
|
469
|
+
const result = await localFileCtr.handleEditFile({
|
|
470
|
+
file_path: '/test/file.js',
|
|
471
|
+
old_string: 'console.log("old");',
|
|
472
|
+
new_string: 'console.log("new");\n console.log("added");',
|
|
473
|
+
replace_all: false,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(result.success).toBe(true);
|
|
477
|
+
expect(result.replacements).toBe(1);
|
|
478
|
+
expect(result.linesAdded).toBe(2);
|
|
479
|
+
expect(result.linesDeleted).toBe(1);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should return error when old_string is not found', async () => {
|
|
483
|
+
const originalContent = 'Hello world';
|
|
484
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue(originalContent);
|
|
485
|
+
|
|
486
|
+
const result = await localFileCtr.handleEditFile({
|
|
487
|
+
file_path: '/test/file.txt',
|
|
488
|
+
old_string: 'NonExistent',
|
|
489
|
+
new_string: 'New',
|
|
490
|
+
replace_all: false,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(result.success).toBe(false);
|
|
494
|
+
expect(result.error).toBe('The specified old_string was not found in the file');
|
|
495
|
+
expect(result.replacements).toBe(0);
|
|
496
|
+
expect(mockFsPromises.writeFile).not.toHaveBeenCalled();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should handle file read error', async () => {
|
|
500
|
+
vi.mocked(mockFsPromises.readFile).mockRejectedValue(new Error('Permission denied'));
|
|
501
|
+
|
|
502
|
+
const result = await localFileCtr.handleEditFile({
|
|
503
|
+
file_path: '/test/file.txt',
|
|
504
|
+
old_string: 'Hello',
|
|
505
|
+
new_string: 'Hi',
|
|
506
|
+
replace_all: false,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(result.success).toBe(false);
|
|
510
|
+
expect(result.error).toBe('Permission denied');
|
|
511
|
+
expect(result.replacements).toBe(0);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should handle file write error', async () => {
|
|
515
|
+
const originalContent = 'Hello world';
|
|
516
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue(originalContent);
|
|
517
|
+
vi.mocked(mockFsPromises.writeFile).mockRejectedValue(new Error('Disk full'));
|
|
518
|
+
|
|
519
|
+
const result = await localFileCtr.handleEditFile({
|
|
520
|
+
file_path: '/test/file.txt',
|
|
521
|
+
old_string: 'Hello',
|
|
522
|
+
new_string: 'Hi',
|
|
523
|
+
replace_all: false,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(result.success).toBe(false);
|
|
527
|
+
expect(result.error).toBe('Disk full');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('should generate correct diff format', async () => {
|
|
531
|
+
const originalContent = 'line 1\nline 2\nline 3';
|
|
532
|
+
vi.mocked(mockFsPromises.readFile).mockResolvedValue(originalContent);
|
|
533
|
+
vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined);
|
|
534
|
+
|
|
535
|
+
const result = await localFileCtr.handleEditFile({
|
|
536
|
+
file_path: '/test/file.txt',
|
|
537
|
+
old_string: 'line 2',
|
|
538
|
+
new_string: 'modified line 2',
|
|
539
|
+
replace_all: false,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(result.success).toBe(true);
|
|
543
|
+
expect(result.diffText).toContain('diff --git a/test/file.txt b/test/file.txt');
|
|
544
|
+
expect(result.diffText).toContain('-line 2');
|
|
545
|
+
expect(result.diffText).toContain('+modified line 2');
|
|
546
|
+
});
|
|
547
|
+
});
|
|
395
548
|
});
|
package/changelog/v1.json
CHANGED
package/locales/en-US/tool.json
CHANGED
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
"prompt": "Prompt"
|
|
16
16
|
},
|
|
17
17
|
"localFiles": {
|
|
18
|
+
"editFile": {
|
|
19
|
+
"newString": "Replace with",
|
|
20
|
+
"oldString": "Find",
|
|
21
|
+
"replaceAll": "Replace all occurrences",
|
|
22
|
+
"replaceFirst": "Replace first occurrence only"
|
|
23
|
+
},
|
|
18
24
|
"file": "File",
|
|
19
25
|
"folder": "Folder",
|
|
20
26
|
"moveFiles": {
|
|
@@ -34,7 +40,12 @@
|
|
|
34
40
|
"readFile": "Read File",
|
|
35
41
|
"readFileError": "Failed to read file, please check if the file path is correct",
|
|
36
42
|
"readFiles": "Read Files",
|
|
37
|
-
"readFilesError": "Failed to read files, please check if the file path is correct"
|
|
43
|
+
"readFilesError": "Failed to read files, please check if the file path is correct",
|
|
44
|
+
"writeFile": {
|
|
45
|
+
"characters": "characters",
|
|
46
|
+
"preview": "Content Preview",
|
|
47
|
+
"truncated": "truncated"
|
|
48
|
+
}
|
|
38
49
|
},
|
|
39
50
|
"search": {
|
|
40
51
|
"createNewSearch": "Create a new search record",
|
package/locales/zh-CN/tool.json
CHANGED
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
"prompt": "提示词"
|
|
16
16
|
},
|
|
17
17
|
"localFiles": {
|
|
18
|
+
"editFile": {
|
|
19
|
+
"newString": "替换为",
|
|
20
|
+
"oldString": "查找内容",
|
|
21
|
+
"replaceAll": "替换全部匹配项",
|
|
22
|
+
"replaceFirst": "仅替换第一个匹配项"
|
|
23
|
+
},
|
|
18
24
|
"file": "文件",
|
|
19
25
|
"folder": "文件夹",
|
|
20
26
|
"moveFiles": {
|
|
@@ -34,7 +40,12 @@
|
|
|
34
40
|
"readFile": "读取文件",
|
|
35
41
|
"readFileError": "读取文件失败,请检查文件路径是否正确",
|
|
36
42
|
"readFiles": "读取文件",
|
|
37
|
-
"readFilesError": "读取文件失败,请检查文件路径是否正确"
|
|
43
|
+
"readFilesError": "读取文件失败,请检查文件路径是否正确",
|
|
44
|
+
"writeFile": {
|
|
45
|
+
"characters": "字符",
|
|
46
|
+
"preview": "内容预览",
|
|
47
|
+
"truncated": "已截断"
|
|
48
|
+
}
|
|
38
49
|
},
|
|
39
50
|
"search": {
|
|
40
51
|
"createNewSearch": "创建新的搜索记录",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.74",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -258,6 +258,7 @@
|
|
|
258
258
|
"random-words": "^2.0.1",
|
|
259
259
|
"react": "19.2.0",
|
|
260
260
|
"react-confetti": "^6.4.0",
|
|
261
|
+
"react-diff-view": "^3.3.2",
|
|
261
262
|
"react-dom": "19.2.0",
|
|
262
263
|
"react-fast-marquee": "^1.6.5",
|
|
263
264
|
"react-hotkeys-hook": "^5.2.1",
|
|
@@ -48,6 +48,7 @@ export interface RenameLocalFileResult {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface LocalReadFileParams {
|
|
51
|
+
fullContent?: boolean;
|
|
51
52
|
loc?: [number, number];
|
|
52
53
|
path: string;
|
|
53
54
|
}
|
|
@@ -217,7 +218,10 @@ export interface EditLocalFileParams {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
export interface EditLocalFileResult {
|
|
221
|
+
diffText?: string;
|
|
220
222
|
error?: string;
|
|
223
|
+
linesAdded?: number;
|
|
224
|
+
linesDeleted?: number;
|
|
221
225
|
replacements: number;
|
|
222
226
|
success: boolean;
|
|
223
227
|
}
|
package/scripts/prebuild.mts
CHANGED
|
@@ -20,11 +20,11 @@ const partialBuildPages = [
|
|
|
20
20
|
disabled: isDesktop,
|
|
21
21
|
paths: ['src/app/[variants]/(auth)'],
|
|
22
22
|
},
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
23
|
+
// {
|
|
24
|
+
// name: 'mobile',
|
|
25
|
+
// disabled: isDesktop,
|
|
26
|
+
// paths: ['src/app/[variants]/(main)/(mobile)'],
|
|
27
|
+
// },
|
|
28
28
|
{
|
|
29
29
|
name: 'oauth',
|
|
30
30
|
disabled: isDesktop,
|
|
@@ -35,6 +35,16 @@ const partialBuildPages = [
|
|
|
35
35
|
disabled: isDesktop,
|
|
36
36
|
paths: ['src/app/(backend)/api/webhooks'],
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
name: 'market-auth',
|
|
40
|
+
disabled: isDesktop,
|
|
41
|
+
paths: ['src/app/market-auth-callback'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'pwa',
|
|
45
|
+
disabled: isDesktop,
|
|
46
|
+
paths: ['src/manifest.ts', 'src/sitemap.tsx', 'src/robots.tsx', 'src/sw'],
|
|
47
|
+
},
|
|
38
48
|
// no need for web
|
|
39
49
|
{
|
|
40
50
|
name: 'desktop-devtools',
|
|
@@ -7,7 +7,6 @@ import Loading from '@/components/Loading/BrandTextLoading';
|
|
|
7
7
|
import { useGlobalStore } from '@/store/global';
|
|
8
8
|
import type { Locales } from '@/types/locale';
|
|
9
9
|
|
|
10
|
-
import DesktopChangelogLayout from './(main)/changelog/_layout/Desktop';
|
|
11
10
|
import DesktopMainLayout from './(main)/layouts/desktop';
|
|
12
11
|
import { idLoader, slugLoader } from './loaders/routeParams';
|
|
13
12
|
|
|
@@ -300,22 +299,6 @@ export const createDesktopRouter = (locale: Locales) =>
|
|
|
300
299
|
path: 'labs',
|
|
301
300
|
},
|
|
302
301
|
|
|
303
|
-
// Changelog routes
|
|
304
|
-
{
|
|
305
|
-
children: [
|
|
306
|
-
{
|
|
307
|
-
index: true,
|
|
308
|
-
lazy: () =>
|
|
309
|
-
import('./(main)/changelog').then((m) => ({
|
|
310
|
-
Component: m.DesktopPage,
|
|
311
|
-
})),
|
|
312
|
-
path: '*',
|
|
313
|
-
},
|
|
314
|
-
],
|
|
315
|
-
element: <DesktopChangelogLayout locale={locale} />,
|
|
316
|
-
path: 'changelog',
|
|
317
|
-
},
|
|
318
|
-
|
|
319
302
|
// Profile routes
|
|
320
303
|
{
|
|
321
304
|
children: [
|
|
@@ -7,7 +7,6 @@ import Loading from '@/components/Loading/BrandTextLoading';
|
|
|
7
7
|
import { useGlobalStore } from '@/store/global';
|
|
8
8
|
import type { Locales } from '@/types/locale';
|
|
9
9
|
|
|
10
|
-
import MobileChangelogLayout from './(main)/changelog/_layout/Mobile';
|
|
11
10
|
import { MobileMainLayout } from './(main)/layouts/mobile';
|
|
12
11
|
import { idLoader, slugLoader } from './loaders/routeParams';
|
|
13
12
|
|
|
@@ -287,21 +286,6 @@ export const createMobileRouter = (locale: Locales) =>
|
|
|
287
286
|
path: 'labs',
|
|
288
287
|
},
|
|
289
288
|
|
|
290
|
-
// Changelog routes
|
|
291
|
-
{
|
|
292
|
-
children: [
|
|
293
|
-
{
|
|
294
|
-
index: true,
|
|
295
|
-
lazy: () =>
|
|
296
|
-
import('./(main)/changelog').then((m) => ({
|
|
297
|
-
Component: m.MobilePage,
|
|
298
|
-
})),
|
|
299
|
-
},
|
|
300
|
-
],
|
|
301
|
-
element: <MobileChangelogLayout locale={locale} />,
|
|
302
|
-
path: 'changelog',
|
|
303
|
-
},
|
|
304
|
-
|
|
305
289
|
// Profile routes
|
|
306
290
|
{
|
|
307
291
|
children: [
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { DynamicLayoutProps } from '@/types/next';
|
|
2
2
|
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import DesktopRouter from './DesktopRouter';
|
|
5
|
+
import MobileRouter from './MobileRouter';
|
|
6
|
+
|
|
7
|
+
export default async (props: DynamicLayoutProps) => {
|
|
5
8
|
// Get isMobile from variants parameter on server side
|
|
6
9
|
const isMobile = await RouteVariants.getIsMobile(props);
|
|
7
10
|
const { locale } = await RouteVariants.getVariantsFromProps(props);
|
|
@@ -10,10 +13,8 @@ export default async function Page(props: DynamicLayoutProps) {
|
|
|
10
13
|
// Using native dynamic import ensures complete code splitting
|
|
11
14
|
// Mobile and Desktop bundles will be completely separate
|
|
12
15
|
if (isMobile) {
|
|
13
|
-
const { default: MobileRouter } = await import('./MobileRouter');
|
|
14
16
|
return <MobileRouter locale={locale} />;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
const { default: DesktopRouter } = await import('./DesktopRouter');
|
|
18
19
|
return <DesktopRouter locale={locale} />;
|
|
19
|
-
}
|
|
20
|
+
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
Trash2,
|
|
12
12
|
X,
|
|
13
13
|
} from 'lucide-react';
|
|
14
|
-
import { CSSProperties, memo, useState } from 'react';
|
|
14
|
+
import { CSSProperties, memo, useEffect, useState } from 'react';
|
|
15
15
|
import { useTranslation } from 'react-i18next';
|
|
16
16
|
import { Flexbox } from 'react-layout-kit';
|
|
17
17
|
|
|
@@ -97,7 +97,6 @@ const Inspectors = memo<InspectorProps>(
|
|
|
97
97
|
apiName,
|
|
98
98
|
id,
|
|
99
99
|
arguments: requestArgs,
|
|
100
|
-
showRender,
|
|
101
100
|
result,
|
|
102
101
|
setShowRender,
|
|
103
102
|
showPluginRender,
|
|
@@ -109,6 +108,8 @@ const Inspectors = memo<InspectorProps>(
|
|
|
109
108
|
const { styles, theme } = useStyles();
|
|
110
109
|
|
|
111
110
|
const [showDebug, setShowDebug] = useState(false);
|
|
111
|
+
const [isPinned, setIsPinned] = useState(false);
|
|
112
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
112
113
|
|
|
113
114
|
const [deleteAssistantMessage] = useChatStore((s) => [s.deleteAssistantMessage]);
|
|
114
115
|
|
|
@@ -121,7 +122,15 @@ const Inspectors = memo<InspectorProps>(
|
|
|
121
122
|
const isReject = intervention?.status === 'rejected';
|
|
122
123
|
const isTitleLoading = !hasResult && !isPending;
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
// Compute actual render state based on pinned or hovered
|
|
126
|
+
const shouldShowRender = isPinned || isHovered;
|
|
127
|
+
|
|
128
|
+
// Sync with parent state
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
setShowRender(shouldShowRender);
|
|
131
|
+
}, [shouldShowRender, setShowRender]);
|
|
132
|
+
|
|
133
|
+
const showCustomPluginRender = shouldShowRender && !isPending && !isReject;
|
|
125
134
|
return (
|
|
126
135
|
<Flexbox className={styles.container} gap={4}>
|
|
127
136
|
<Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
|
|
@@ -131,7 +140,17 @@ const Inspectors = memo<InspectorProps>(
|
|
|
131
140
|
gap={8}
|
|
132
141
|
horizontal
|
|
133
142
|
onClick={() => {
|
|
134
|
-
|
|
143
|
+
setIsPinned(!isPinned);
|
|
144
|
+
}}
|
|
145
|
+
onMouseEnter={() => {
|
|
146
|
+
if (!isPinned) {
|
|
147
|
+
setIsHovered(true);
|
|
148
|
+
}
|
|
149
|
+
}}
|
|
150
|
+
onMouseLeave={() => {
|
|
151
|
+
if (!isPinned) {
|
|
152
|
+
setIsHovered(false);
|
|
153
|
+
}
|
|
135
154
|
}}
|
|
136
155
|
paddingInline={4}
|
|
137
156
|
>
|
|
@@ -15,6 +15,12 @@ export default {
|
|
|
15
15
|
prompt: '提示词',
|
|
16
16
|
},
|
|
17
17
|
localFiles: {
|
|
18
|
+
editFile: {
|
|
19
|
+
newString: '替换为',
|
|
20
|
+
oldString: '查找内容',
|
|
21
|
+
replaceAll: '替换全部匹配项',
|
|
22
|
+
replaceFirst: '仅替换第一个匹配项',
|
|
23
|
+
},
|
|
18
24
|
file: '文件',
|
|
19
25
|
folder: '文件夹',
|
|
20
26
|
moveFiles: {
|
|
@@ -35,6 +41,11 @@ export default {
|
|
|
35
41
|
readFileError: '读取文件失败,请检查文件路径是否正确',
|
|
36
42
|
readFiles: '读取文件',
|
|
37
43
|
readFilesError: '读取文件失败,请检查文件路径是否正确',
|
|
44
|
+
writeFile: {
|
|
45
|
+
characters: '字符',
|
|
46
|
+
preview: '内容预览',
|
|
47
|
+
truncated: '已截断',
|
|
48
|
+
},
|
|
38
49
|
},
|
|
39
50
|
search: {
|
|
40
51
|
createNewSearch: '创建新的搜索记录',
|