@lobehub/lobehub 2.0.0-next.375 → 2.0.0-next.376

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 (95) hide show
  1. package/.github/workflows/claude-migration-support.yml +1 -1
  2. package/.github/workflows/revalidate-docs.yml +25 -0
  3. package/CHANGELOG.md +41 -0
  4. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +7 -1
  5. package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +23 -10
  6. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +261 -0
  7. package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +36 -0
  8. package/apps/desktop/src/main/core/infrastructure/BackendProxyProtocolManager.ts +2 -1
  9. package/changelog/v1.json +12 -0
  10. package/docs/development/database-schema.dbml +89 -0
  11. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +6 -0
  12. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +5 -0
  13. package/docs/usage/features/auth.mdx +54 -33
  14. package/docs/usage/features/auth.zh-CN.mdx +56 -33
  15. package/locales/en-US/common.json +24 -0
  16. package/locales/en-US/setting.json +19 -0
  17. package/locales/zh-CN/authError.json +2 -0
  18. package/locales/zh-CN/common.json +24 -0
  19. package/locales/zh-CN/setting.json +19 -0
  20. package/package.json +2 -1
  21. package/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx +1 -11
  22. package/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx +6 -11
  23. package/packages/database/migrations/0074_add_fk_indexes_for_cascade_delete.sql +56 -0
  24. package/packages/database/migrations/meta/0074_snapshot.json +10901 -0
  25. package/packages/database/migrations/meta/_journal.json +7 -0
  26. package/packages/database/src/models/message.ts +84 -81
  27. package/packages/database/src/repositories/search/index.ts +330 -570
  28. package/packages/database/src/schemas/agent.ts +4 -0
  29. package/packages/database/src/schemas/chatGroup.ts +2 -0
  30. package/packages/database/src/schemas/file.ts +4 -0
  31. package/packages/database/src/schemas/generation.ts +1 -0
  32. package/packages/database/src/schemas/message.ts +16 -0
  33. package/packages/database/src/schemas/nextauth.ts +15 -8
  34. package/packages/database/src/schemas/oidc.ts +104 -68
  35. package/packages/database/src/schemas/rag.ts +4 -0
  36. package/packages/database/src/schemas/ragEvals.ts +97 -73
  37. package/packages/database/src/schemas/relations.ts +7 -0
  38. package/packages/database/src/schemas/session.ts +1 -0
  39. package/packages/database/src/schemas/topic.ts +8 -1
  40. package/packages/desktop-bridge/src/index.ts +17 -0
  41. package/scripts/_shared/checkDeprecatedAuth.js +26 -0
  42. package/src/app/(backend)/trpc/async/[trpc]/route.ts +2 -0
  43. package/src/app/(backend)/trpc/lambda/[trpc]/route.ts +2 -5
  44. package/src/app/(backend)/trpc/mobile/[trpc]/route.ts +2 -5
  45. package/src/app/(backend)/trpc/tools/[trpc]/route.ts +2 -0
  46. package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +7 -4
  47. package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/index.tsx +3 -23
  48. package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/index.tsx +3 -23
  49. package/src/app/[variants]/(main)/resource/(home)/index.tsx +4 -1
  50. package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +3 -0
  51. package/src/app/[variants]/(main)/resource/library/_layout/Header/index.tsx +2 -1
  52. package/src/app/[variants]/(main)/settings/storage/features/Advanced.tsx +19 -12
  53. package/src/app/[variants]/(main)/settings/storage/index.tsx +19 -1
  54. package/src/business/client/features/AccountDeletion/index.tsx +3 -0
  55. package/src/business/server/lambda-routers/accountDeletion.ts +3 -0
  56. package/src/components/DragUpload/index.tsx +1 -1
  57. package/src/config/routes/index.ts +9 -1
  58. package/src/features/CommandMenu/ContextCommands.tsx +10 -2
  59. package/src/features/CommandMenu/MainMenu.tsx +29 -22
  60. package/src/features/CommandMenu/SearchResults.tsx +19 -0
  61. package/src/features/CommandMenu/components/CommandInput.tsx +1 -0
  62. package/src/features/CommandMenu/index.tsx +25 -3
  63. package/src/features/CommandMenu/styles.ts +1 -0
  64. package/src/features/CommandMenu/useCommandMenu.ts +8 -2
  65. package/src/features/CommandMenu/utils/contextCommands.ts +17 -2
  66. package/src/features/CommandMenu/utils/queryParser.ts +1 -0
  67. package/src/features/Conversation/Messages/AssistantGroup/index.tsx +6 -1
  68. package/src/features/Electron/connection/Connection.tsx +20 -15
  69. package/src/features/FileViewer/index.tsx +28 -5
  70. package/src/features/ModelSwitchPanel/components/List/ListItemRenderer.tsx +10 -9
  71. package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +1 -0
  72. package/src/features/ModelSwitchPanel/components/Toolbar.tsx +1 -1
  73. package/src/features/ResourceManager/components/Editor/index.tsx +7 -4
  74. package/src/features/ResourceManager/components/Explorer/Header/index.tsx +1 -1
  75. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +12 -1
  76. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +23 -43
  77. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/ImageFileItem.tsx +1 -0
  78. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +24 -44
  79. package/src/features/ResourceManager/components/Explorer/hooks/useFileItemClick.ts +78 -0
  80. package/src/features/ResourceManager/components/Header/AddButton.tsx +0 -5
  81. package/src/features/ResourceManager/index.tsx +38 -24
  82. package/src/libs/better-auth/define-config.ts +17 -0
  83. package/src/libs/better-auth/plugins/email-whitelist.ts +4 -1
  84. package/src/libs/redis/manager.ts +5 -9
  85. package/src/libs/trpc/utils/responseMeta.test.ts +82 -0
  86. package/src/libs/trpc/utils/responseMeta.ts +41 -0
  87. package/src/locales/default/authError.ts +3 -0
  88. package/src/locales/default/common.ts +82 -4
  89. package/src/locales/default/setting.ts +23 -1
  90. package/src/server/modules/AgentRuntime/redis.ts +3 -0
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/search.ts +3 -2
  93. package/src/utils/errorResponse.ts +2 -1
  94. package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/AddButton.tsx +0 -37
  95. package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/AddButton.tsx +0 -32
@@ -1,5 +1,4 @@
1
1
  name: Claude Migration Support
2
- description: Automatically respond to migration feedback issues using Claude Code
3
2
 
4
3
  on:
5
4
  issue_comment:
@@ -76,6 +75,7 @@ jobs:
76
75
 
77
76
  3. Read additional reference files:
78
77
  - Main auth documentation: `cat docs/self-hosting/advanced/auth.mdx`
78
+ - Migration internals: `cat docs/self-hosting/advanced/auth/migration-internals.mdx`
79
79
  - Deprecated env vars checker: `cat scripts/_shared/checkDeprecatedAuth.js`
80
80
 
81
81
  4. Analyze the user's comment and determine:
@@ -0,0 +1,25 @@
1
+ name: Revalidate Docs
2
+ permissions:
3
+ contents: read
4
+
5
+ on:
6
+ push:
7
+ branches:
8
+ - main
9
+ - next
10
+ paths:
11
+ - 'docs/**'
12
+
13
+ jobs:
14
+ revalidate:
15
+ name: Revalidate Docs
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Trigger docs revalidation
19
+ run: |
20
+ response=$(curl "${{ secrets.DOCS_REVALIDATE_URL }}" --silent --show-error)
21
+ echo "Response: $response"
22
+ if [ "$response" != '{"success":true}' ]; then
23
+ echo "Error: Unexpected response"
24
+ exit 1
25
+ fi
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.376](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.375...v2.0.0-next.376)
6
+
7
+ <sup>Released on **2026-01-25**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor search model implement.
12
+
13
+ #### ✨ Features
14
+
15
+ - **trpc**: Add response metadata and auth header handling.
16
+
17
+ #### 🐛 Bug Fixes
18
+
19
+ - **misc**: Fix add message and improve local system tool.
20
+
21
+ <br/>
22
+
23
+ <details>
24
+ <summary><kbd>Improvements and Fixes</kbd></summary>
25
+
26
+ #### Code refactoring
27
+
28
+ - **misc**: Refactor search model implement, closes [#11825](https://github.com/lobehub/lobe-chat/issues/11825) ([3cf0bfa](https://github.com/lobehub/lobe-chat/commit/3cf0bfa))
29
+
30
+ #### What's improved
31
+
32
+ - **trpc**: Add response metadata and auth header handling, closes [#11816](https://github.com/lobehub/lobe-chat/issues/11816) ([1276a87](https://github.com/lobehub/lobe-chat/commit/1276a87))
33
+
34
+ #### What's fixed
35
+
36
+ - **misc**: Fix add message and improve local system tool, closes [#11815](https://github.com/lobehub/lobe-chat/issues/11815) ([3b41009](https://github.com/lobehub/lobe-chat/commit/3b41009))
37
+
38
+ </details>
39
+
40
+ <div align="right">
41
+
42
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
43
+
44
+ </div>
45
+
5
46
  ## [Version 2.0.0-next.375](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.374...v2.0.0-next.375)
6
47
 
7
48
  <sup>Released on **2026-01-25**</sup>
@@ -548,7 +548,13 @@ export default class LocalFileCtr extends ControllerModule {
548
548
  filesToSearch = [searchPath];
549
549
  } else {
550
550
  // Use glob pattern if provided, otherwise search all files
551
- const globPattern = params.glob || '**/*';
551
+ // If glob doesn't contain directory separator and doesn't start with **,
552
+ // auto-prefix with **/ to make it recursive
553
+ let globPattern = params.glob || '**/*';
554
+ if (params.glob && !params.glob.includes('/') && !params.glob.startsWith('**')) {
555
+ globPattern = `**/${params.glob}`;
556
+ }
557
+
552
558
  filesToSearch = await fg(globPattern, {
553
559
  absolute: true,
554
560
  cwd: searchPath,
@@ -15,6 +15,19 @@ import { ControllerModule, IpcMethod } from './index';
15
15
 
16
16
  const logger = createLogger('controllers:ShellCommandCtr');
17
17
 
18
+ // Maximum output length to prevent context explosion
19
+ const MAX_OUTPUT_LENGTH = 10_000;
20
+
21
+ /**
22
+ * Truncate string to max length with ellipsis indicator
23
+ */
24
+ const truncateOutput = (str: string, maxLength: number = MAX_OUTPUT_LENGTH): string => {
25
+ if (str.length <= maxLength) return str;
26
+ return (
27
+ str.slice(0, maxLength) + '\n... [truncated, ' + (str.length - maxLength) + ' more characters]'
28
+ );
29
+ };
30
+
18
31
  interface ShellProcess {
19
32
  lastReadStderr: number;
20
33
  lastReadStdout: number;
@@ -104,8 +117,8 @@ export default class ShellCommandCtr extends ControllerModule {
104
117
  childProcess.kill();
105
118
  resolve({
106
119
  error: `Command timed out after ${effectiveTimeout}ms`,
107
- stderr,
108
- stdout,
120
+ stderr: truncateOutput(stderr),
121
+ stdout: truncateOutput(stdout),
109
122
  success: false,
110
123
  });
111
124
  }, effectiveTimeout);
@@ -125,9 +138,9 @@ export default class ShellCommandCtr extends ControllerModule {
125
138
  logger.info(`${logPrefix} Command completed`, { code, success });
126
139
  resolve({
127
140
  exit_code: code || 0,
128
- output: stdout + stderr,
129
- stderr,
130
- stdout,
141
+ output: truncateOutput(stdout + stderr),
142
+ stderr: truncateOutput(stderr),
143
+ stdout: truncateOutput(stdout),
131
144
  success,
132
145
  });
133
146
  }
@@ -138,8 +151,8 @@ export default class ShellCommandCtr extends ControllerModule {
138
151
  logger.error(`${logPrefix} Command failed:`, error);
139
152
  resolve({
140
153
  error: error.message,
141
- stderr,
142
- stdout,
154
+ stderr: truncateOutput(stderr),
155
+ stdout: truncateOutput(stdout),
143
156
  success: false,
144
157
  });
145
158
  });
@@ -205,10 +218,10 @@ export default class ShellCommandCtr extends ControllerModule {
205
218
  });
206
219
 
207
220
  return {
208
- output,
221
+ output: truncateOutput(output),
209
222
  running,
210
- stderr: newStderr,
211
- stdout: newStdout,
223
+ stderr: truncateOutput(newStderr),
224
+ stdout: truncateOutput(newStdout),
212
225
  success: true,
213
226
  };
214
227
  }
@@ -552,4 +552,265 @@ describe('LocalFileCtr', () => {
552
552
  expect(result.diffText).toContain('+modified line 2');
553
553
  });
554
554
  });
555
+
556
+ describe('handleGrepContent', () => {
557
+ it('should search content in a single file', async () => {
558
+ vi.mocked(mockFsPromises.stat).mockResolvedValue({
559
+ isFile: () => true,
560
+ isDirectory: () => false,
561
+ } as any);
562
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('Hello world\nTest line\nAnother test');
563
+
564
+ const result = await localFileCtr.handleGrepContent({
565
+ 'pattern': 'test',
566
+ 'path': '/test/file.txt',
567
+ '-i': true,
568
+ });
569
+
570
+ expect(result.success).toBe(true);
571
+ expect(result.matches).toContain('/test/file.txt');
572
+ expect(result.total_matches).toBe(1);
573
+ });
574
+
575
+ it('should search content in directory with default glob pattern', async () => {
576
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
577
+ if (filePath === '/test') {
578
+ return { isFile: () => false, isDirectory: () => true } as any;
579
+ }
580
+ return { isFile: () => true, isDirectory: () => false } as any;
581
+ });
582
+ vi.mocked(mockFg).mockResolvedValue(['/test/file1.txt', '/test/file2.txt']);
583
+ vi.mocked(mockFsPromises.readFile).mockImplementation(async (filePath) => {
584
+ if (filePath === '/test/file1.txt') return 'Hello world';
585
+ if (filePath === '/test/file2.txt') return 'Test content';
586
+ return '';
587
+ });
588
+
589
+ const result = await localFileCtr.handleGrepContent({
590
+ pattern: 'Hello',
591
+ path: '/test',
592
+ });
593
+
594
+ expect(result.success).toBe(true);
595
+ expect(result.matches).toContain('/test/file1.txt');
596
+ expect(result.total_matches).toBe(1);
597
+ expect(mockFg).toHaveBeenCalledWith('**/*', expect.objectContaining({ cwd: '/test' }));
598
+ });
599
+
600
+ it('should auto-prefix glob pattern with **/ for non-recursive patterns', async () => {
601
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
602
+ if (filePath === '/test') {
603
+ return { isFile: () => false, isDirectory: () => true } as any;
604
+ }
605
+ return { isFile: () => true, isDirectory: () => false } as any;
606
+ });
607
+ vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts', '/test/lib/file2.tsx']);
608
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
609
+
610
+ const result = await localFileCtr.handleGrepContent({
611
+ pattern: 'test',
612
+ path: '/test',
613
+ glob: '*.{ts,tsx}',
614
+ });
615
+
616
+ expect(result.success).toBe(true);
617
+ // Should auto-prefix *.{ts,tsx} with **/ to make it recursive
618
+ expect(mockFg).toHaveBeenCalledWith(
619
+ '**/*.{ts,tsx}',
620
+ expect.objectContaining({ cwd: '/test' }),
621
+ );
622
+ });
623
+
624
+ it('should not modify glob pattern that already contains path separator', async () => {
625
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
626
+ if (filePath === '/test') {
627
+ return { isFile: () => false, isDirectory: () => true } as any;
628
+ }
629
+ return { isFile: () => true, isDirectory: () => false } as any;
630
+ });
631
+ vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts']);
632
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
633
+
634
+ const result = await localFileCtr.handleGrepContent({
635
+ pattern: 'test',
636
+ path: '/test',
637
+ glob: 'src/*.ts',
638
+ });
639
+
640
+ expect(result.success).toBe(true);
641
+ // Should not modify glob pattern that already contains /
642
+ expect(mockFg).toHaveBeenCalledWith('src/*.ts', expect.objectContaining({ cwd: '/test' }));
643
+ });
644
+
645
+ it('should not modify glob pattern that starts with **', async () => {
646
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
647
+ if (filePath === '/test') {
648
+ return { isFile: () => false, isDirectory: () => true } as any;
649
+ }
650
+ return { isFile: () => true, isDirectory: () => false } as any;
651
+ });
652
+ vi.mocked(mockFg).mockResolvedValue(['/test/src/file1.ts']);
653
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('const test = "hello";');
654
+
655
+ const result = await localFileCtr.handleGrepContent({
656
+ pattern: 'test',
657
+ path: '/test',
658
+ glob: '**/components/*.tsx',
659
+ });
660
+
661
+ expect(result.success).toBe(true);
662
+ // Should not modify glob pattern that already starts with **
663
+ expect(mockFg).toHaveBeenCalledWith(
664
+ '**/components/*.tsx',
665
+ expect.objectContaining({ cwd: '/test' }),
666
+ );
667
+ });
668
+
669
+ it('should filter by type when provided', async () => {
670
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
671
+ if (filePath === '/test') {
672
+ return { isFile: () => false, isDirectory: () => true } as any;
673
+ }
674
+ return { isFile: () => true, isDirectory: () => false } as any;
675
+ });
676
+ // fast-glob returns all files, then type filter is applied
677
+ vi.mocked(mockFg).mockResolvedValue(['/test/file1.ts', '/test/file2.js', '/test/file3.ts']);
678
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('unique_pattern');
679
+
680
+ const result = await localFileCtr.handleGrepContent({
681
+ pattern: 'unique_pattern',
682
+ path: '/test',
683
+ type: 'ts',
684
+ });
685
+
686
+ expect(result.success).toBe(true);
687
+ // Type filter should exclude .js files from being searched
688
+ // Only .ts files should be in the results
689
+ expect(result.matches).not.toContain('/test/file2.js');
690
+ // At least one .ts file should match
691
+ expect(result.matches.length).toBeGreaterThan(0);
692
+ expect(result.matches.every((m) => m.endsWith('.ts'))).toBe(true);
693
+ });
694
+
695
+ it('should return content mode with line numbers', async () => {
696
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
697
+ if (filePath === '/test') {
698
+ return { isFile: () => false, isDirectory: () => true } as any;
699
+ }
700
+ return { isFile: () => true, isDirectory: () => false } as any;
701
+ });
702
+ vi.mocked(mockFg).mockResolvedValue(['/test/file.txt']);
703
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('line 1\ntest line\nline 3');
704
+
705
+ const result = await localFileCtr.handleGrepContent({
706
+ 'pattern': 'test',
707
+ 'path': '/test',
708
+ 'output_mode': 'content',
709
+ '-n': true,
710
+ });
711
+
712
+ expect(result.success).toBe(true);
713
+ expect(result.matches.some((m) => m.includes('2:'))).toBe(true);
714
+ });
715
+
716
+ it('should return count mode', async () => {
717
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
718
+ if (filePath === '/test') {
719
+ return { isFile: () => false, isDirectory: () => true } as any;
720
+ }
721
+ return { isFile: () => true, isDirectory: () => false } as any;
722
+ });
723
+ vi.mocked(mockFg).mockResolvedValue(['/test/file.txt']);
724
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('test one\ntest two\ntest three');
725
+
726
+ const result = await localFileCtr.handleGrepContent({
727
+ pattern: 'test',
728
+ path: '/test',
729
+ output_mode: 'count',
730
+ });
731
+
732
+ expect(result.success).toBe(true);
733
+ expect(result.matches).toContain('/test/file.txt:3');
734
+ expect(result.total_matches).toBe(3);
735
+ });
736
+
737
+ it('should respect head_limit', async () => {
738
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
739
+ if (filePath === '/test') {
740
+ return { isFile: () => false, isDirectory: () => true } as any;
741
+ }
742
+ return { isFile: () => true, isDirectory: () => false } as any;
743
+ });
744
+ vi.mocked(mockFg).mockResolvedValue([
745
+ '/test/file1.txt',
746
+ '/test/file2.txt',
747
+ '/test/file3.txt',
748
+ '/test/file4.txt',
749
+ '/test/file5.txt',
750
+ ]);
751
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('test content');
752
+
753
+ const result = await localFileCtr.handleGrepContent({
754
+ pattern: 'test',
755
+ path: '/test',
756
+ head_limit: 2,
757
+ });
758
+
759
+ expect(result.success).toBe(true);
760
+ expect(result.matches.length).toBe(2);
761
+ });
762
+
763
+ it('should handle case insensitive search', async () => {
764
+ vi.mocked(mockFsPromises.stat).mockResolvedValue({
765
+ isFile: () => true,
766
+ isDirectory: () => false,
767
+ } as any);
768
+ vi.mocked(mockFsPromises.readFile).mockResolvedValue('Hello World\nHELLO world\nhello WORLD');
769
+
770
+ const result = await localFileCtr.handleGrepContent({
771
+ 'pattern': 'hello',
772
+ 'path': '/test/file.txt',
773
+ '-i': true,
774
+ });
775
+
776
+ expect(result.success).toBe(true);
777
+ expect(result.matches).toContain('/test/file.txt');
778
+ });
779
+
780
+ it('should handle grep error gracefully', async () => {
781
+ vi.mocked(mockFsPromises.stat).mockRejectedValue(new Error('Path not found'));
782
+
783
+ const result = await localFileCtr.handleGrepContent({
784
+ pattern: 'test',
785
+ path: '/nonexistent',
786
+ });
787
+
788
+ expect(result.success).toBe(false);
789
+ expect(result.matches).toEqual([]);
790
+ expect(result.total_matches).toBe(0);
791
+ });
792
+
793
+ it('should skip unreadable files gracefully', async () => {
794
+ vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
795
+ if (filePath === '/test') {
796
+ return { isFile: () => false, isDirectory: () => true } as any;
797
+ }
798
+ return { isFile: () => true, isDirectory: () => false } as any;
799
+ });
800
+ vi.mocked(mockFg).mockResolvedValue(['/test/file1.txt', '/test/file2.txt']);
801
+ vi.mocked(mockFsPromises.readFile).mockImplementation(async (filePath) => {
802
+ if (filePath === '/test/file1.txt') throw new Error('Permission denied');
803
+ return 'test content';
804
+ });
805
+
806
+ const result = await localFileCtr.handleGrepContent({
807
+ pattern: 'test',
808
+ path: '/test',
809
+ });
810
+
811
+ expect(result.success).toBe(true);
812
+ // Should still find match in file2.txt despite file1.txt error
813
+ expect(result.matches).toContain('/test/file2.txt');
814
+ });
815
+ });
555
816
  });
@@ -193,6 +193,42 @@ describe('ShellCommandCtr', () => {
193
193
  expect(result.stderr).toBe('error message\n');
194
194
  });
195
195
 
196
+ it('should truncate long output to prevent context explosion', async () => {
197
+ let exitCallback: (code: number) => void;
198
+ let stdoutCallback: (data: Buffer) => void;
199
+
200
+ mockChildProcess.on.mockImplementation((event: string, callback: any) => {
201
+ if (event === 'exit') {
202
+ exitCallback = callback;
203
+ setTimeout(() => exitCallback(0), 10);
204
+ }
205
+ return mockChildProcess;
206
+ });
207
+
208
+ mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
209
+ if (event === 'data') {
210
+ stdoutCallback = callback;
211
+ // Simulate very long output (15k characters)
212
+ const longOutput = 'x'.repeat(15_000);
213
+ setTimeout(() => stdoutCallback(Buffer.from(longOutput)), 5);
214
+ }
215
+ return mockChildProcess.stdout;
216
+ });
217
+
218
+ mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
219
+
220
+ const result = await shellCommandCtr.handleRunCommand({
221
+ command: 'command-with-long-output',
222
+ description: 'long output command',
223
+ });
224
+
225
+ expect(result.success).toBe(true);
226
+ // Output should be truncated to ~10k + truncation message
227
+ expect(result.stdout!.length).toBeLessThan(15_000);
228
+ expect(result.stdout).toContain('truncated');
229
+ expect(result.stdout).toContain('more characters');
230
+ });
231
+
196
232
  it('should enforce timeout limits', async () => {
197
233
  mockChildProcess.on.mockImplementation(() => mockChildProcess);
198
234
  mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
@@ -1,3 +1,4 @@
1
+ import { AUTH_REQUIRED_HEADER } from '@lobechat/desktop-bridge';
1
2
  import { BrowserWindow, type Session } from 'electron';
2
3
 
3
4
  import { isDev } from '@/const/env';
@@ -167,7 +168,7 @@ export class BackendProxyProtocolManager {
167
168
  // The server sets X-Auth-Required header for real authentication failures (e.g., token expired)
168
169
  // Other 401 errors (e.g., invalid API keys) should not trigger re-authentication
169
170
  if (upstreamResponse.status === 401) {
170
- const authRequired = upstreamResponse.headers.get('X-Auth-Required') === 'true';
171
+ const authRequired = upstreamResponse.headers.get(AUTH_REQUIRED_HEADER) === 'true';
171
172
  if (authRequired) {
172
173
  this.notifyAuthorizationRequired();
173
174
  }
package/changelog/v1.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Refactor search model implement."
6
+ ],
7
+ "fixes": [
8
+ "Fix add message and improve local system tool."
9
+ ]
10
+ },
11
+ "date": "2026-01-25",
12
+ "version": "2.0.0-next.376"
13
+ },
2
14
  {
3
15
  "children": {
4
16
  "fixes": [