@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.
Files changed (28) hide show
  1. package/.github/workflows/desktop-pr-build.yml +7 -3
  2. package/CHANGELOG.md +25 -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 +9 -0
  7. package/locales/en-US/tool.json +12 -1
  8. package/locales/zh-CN/tool.json +12 -1
  9. package/package.json +2 -1
  10. package/packages/electron-client-ipc/src/types/localSystem.ts +4 -0
  11. package/scripts/prebuild.mts +15 -5
  12. package/src/app/[variants]/desktopRouter.config.tsx +0 -17
  13. package/src/app/[variants]/mobileRouter.config.tsx +0 -16
  14. package/src/app/[variants]/page.tsx +5 -4
  15. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +23 -4
  16. package/src/locales/default/tool.ts +11 -0
  17. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +5 -6
  18. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +45 -182
  19. package/src/tools/executionRuntimes.ts +3 -0
  20. package/src/tools/local-system/ExecutionRuntime/index.ts +407 -0
  21. package/src/tools/local-system/Intervention/EditLocalFile/index.tsx +89 -0
  22. package/src/tools/local-system/Intervention/WriteFile/index.tsx +72 -0
  23. package/src/tools/local-system/Intervention/index.ts +4 -0
  24. package/src/tools/local-system/Render/EditLocalFile/index.tsx +67 -0
  25. package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +53 -78
  26. package/src/tools/local-system/Render/index.ts +2 -0
  27. package/src/tools/local-system/index.ts +1 -0
  28. 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
- pull_request_target:
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
- # Windows 平台构建处理
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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>
@@ -52,6 +52,7 @@
52
52
  "@typescript/native-preview": "7.0.0-dev.20250711.1",
53
53
  "consola": "^3.4.2",
54
54
  "cookie": "^1.0.2",
55
+ "diff": "^8.0.2",
55
56
  "electron": "^38.7.0",
56
57
  "electron-builder": "^26.0.12",
57
58
  "electron-is": "^3.0.0",
@@ -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({ path: filePath, loc }: LocalReadFileParams): Promise<LocalReadFileResult> {
98
- const effectiveLoc = loc ?? [0, 200];
99
- logger.debug('Starting to read file:', { filePath, loc: effectiveLoc });
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
- // Adjust slice indices to be 0-based and inclusive/exclusive
110
- const selectedLines = lines.slice(startLine, endLine);
111
- const content = selectedLines.join('\n');
112
- const charCount = content.length;
113
- const lineCount = selectedLines.length;
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: effectiveLoc,
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
- logger.info(`${logPrefix} File edited successfully`, { replacements });
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
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Edit local file render & intervention."
6
+ ]
7
+ },
8
+ "date": "2025-11-17",
9
+ "version": "2.0.0-next.74"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "features": [
@@ -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",
@@ -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.73",
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
  }
@@ -20,11 +20,11 @@ const partialBuildPages = [
20
20
  disabled: isDesktop,
21
21
  paths: ['src/app/[variants]/(auth)'],
22
22
  },
23
- {
24
- name: 'mobile',
25
- disabled: isDesktop,
26
- paths: ['src/app/[variants]/(main)/(mobile)'],
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
- export default async function Page(props: DynamicLayoutProps) {
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
- const showCustomPluginRender = showRender && !isPending && !isReject;
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
- setShowRender(!showRender);
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: '创建新的搜索记录',