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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.github/workflows/desktop-pr-build.yml +7 -3
  2. package/CHANGELOG.md +50 -0
  3. package/apps/desktop/package.json +1 -0
  4. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +55 -11
  5. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +153 -0
  6. package/changelog/v1.json +18 -0
  7. package/locales/ar/chat.json +5 -0
  8. package/locales/ar/models.json +15 -0
  9. package/locales/ar/tool.json +12 -1
  10. package/locales/bg-BG/chat.json +5 -0
  11. package/locales/bg-BG/models.json +15 -0
  12. package/locales/bg-BG/tool.json +12 -1
  13. package/locales/de-DE/chat.json +5 -0
  14. package/locales/de-DE/models.json +15 -0
  15. package/locales/de-DE/tool.json +12 -1
  16. package/locales/en-US/models.json +15 -0
  17. package/locales/en-US/tool.json +12 -1
  18. package/locales/es-ES/chat.json +5 -0
  19. package/locales/es-ES/models.json +15 -0
  20. package/locales/es-ES/tool.json +12 -1
  21. package/locales/fa-IR/chat.json +5 -0
  22. package/locales/fa-IR/models.json +15 -0
  23. package/locales/fa-IR/tool.json +12 -1
  24. package/locales/fr-FR/chat.json +5 -0
  25. package/locales/fr-FR/models.json +15 -0
  26. package/locales/fr-FR/tool.json +12 -1
  27. package/locales/it-IT/chat.json +5 -0
  28. package/locales/it-IT/models.json +15 -0
  29. package/locales/it-IT/tool.json +12 -1
  30. package/locales/ja-JP/chat.json +5 -0
  31. package/locales/ja-JP/models.json +15 -0
  32. package/locales/ja-JP/tool.json +12 -1
  33. package/locales/ko-KR/chat.json +5 -0
  34. package/locales/ko-KR/models.json +15 -0
  35. package/locales/ko-KR/tool.json +12 -1
  36. package/locales/nl-NL/chat.json +5 -0
  37. package/locales/nl-NL/models.json +15 -0
  38. package/locales/nl-NL/tool.json +12 -1
  39. package/locales/pl-PL/chat.json +5 -0
  40. package/locales/pl-PL/models.json +15 -0
  41. package/locales/pl-PL/tool.json +12 -1
  42. package/locales/pt-BR/chat.json +5 -0
  43. package/locales/pt-BR/models.json +15 -0
  44. package/locales/pt-BR/tool.json +12 -1
  45. package/locales/ru-RU/chat.json +5 -0
  46. package/locales/ru-RU/models.json +15 -0
  47. package/locales/ru-RU/tool.json +12 -1
  48. package/locales/tr-TR/chat.json +5 -0
  49. package/locales/tr-TR/models.json +15 -0
  50. package/locales/tr-TR/tool.json +12 -1
  51. package/locales/vi-VN/chat.json +5 -0
  52. package/locales/vi-VN/models.json +15 -0
  53. package/locales/vi-VN/tool.json +12 -1
  54. package/locales/zh-CN/models.json +15 -0
  55. package/locales/zh-CN/tool.json +12 -1
  56. package/locales/zh-TW/chat.json +5 -0
  57. package/locales/zh-TW/models.json +15 -0
  58. package/locales/zh-TW/tool.json +12 -1
  59. package/package.json +2 -1
  60. package/packages/electron-client-ipc/src/types/localSystem.ts +4 -0
  61. package/scripts/prebuild.mts +15 -5
  62. package/src/app/[variants]/desktopRouter.config.tsx +0 -17
  63. package/src/app/[variants]/mobileRouter.config.tsx +0 -16
  64. package/src/app/[variants]/page.tsx +5 -4
  65. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +23 -4
  66. package/src/locales/default/tool.ts +11 -0
  67. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +5 -6
  68. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +45 -182
  69. package/src/tools/executionRuntimes.ts +3 -0
  70. package/src/tools/local-system/ExecutionRuntime/index.ts +407 -0
  71. package/src/tools/local-system/Intervention/EditLocalFile/index.tsx +89 -0
  72. package/src/tools/local-system/Intervention/WriteFile/index.tsx +72 -0
  73. package/src/tools/local-system/Intervention/index.ts +4 -0
  74. package/src/tools/local-system/Render/EditLocalFile/index.tsx +67 -0
  75. package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +53 -78
  76. package/src/tools/local-system/Render/index.ts +2 -0
  77. package/src/tools/local-system/index.ts +1 -0
  78. package/src/tools/local-system/type.ts +4 -3
@@ -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: '创建新的搜索记录',
@@ -50,18 +50,17 @@ describe('localFileSlice', () => {
50
50
 
51
51
  describe('internal_triggerLocalFileToolCalling', () => {
52
52
  it('should handle successful calling', async () => {
53
- const mockContent = { foo: 'bar' };
53
+ const mockContent = 'result content';
54
54
  const mockState = { state: 'test' };
55
- const mockService = vi.fn().mockResolvedValue({ content: mockContent, state: mockState });
55
+ const mockService = vi
56
+ .fn()
57
+ .mockResolvedValue({ content: mockContent, state: mockState, success: true });
56
58
 
57
59
  await store.internal_triggerLocalFileToolCalling('test-id', mockService);
58
60
 
59
61
  expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
60
62
  expect(mockStore.optimisticUpdatePluginState).toBeCalledWith('test-id', mockState);
61
- expect(mockStore.optimisticUpdateMessageContent).toBeCalledWith(
62
- 'test-id',
63
- JSON.stringify(mockContent),
64
- );
63
+ expect(mockStore.optimisticUpdateMessageContent).toBeCalledWith('test-id', mockContent);
65
64
  expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', false);
66
65
  });
67
66
 
@@ -5,7 +5,6 @@ import {
5
5
  GrepContentParams,
6
6
  KillCommandParams,
7
7
  ListLocalFileParams,
8
- LocalMoveFilesResultItem,
9
8
  LocalReadFileParams,
10
9
  LocalReadFilesParams,
11
10
  LocalSearchFilesParams,
@@ -16,28 +15,14 @@ import {
16
15
  } from '@lobechat/electron-client-ipc';
17
16
  import { StateCreator } from 'zustand/vanilla';
18
17
 
19
- import { localFileService } from '@/services/electron/localFileService';
20
18
  import { ChatStore } from '@/store/chat/store';
21
- import {
22
- EditLocalFileState,
23
- GetCommandOutputState,
24
- GlobFilesState,
25
- GrepContentState,
26
- KillCommandState,
27
- LocalFileListState,
28
- LocalFileSearchState,
29
- LocalMoveFilesState,
30
- LocalReadFileState,
31
- LocalReadFilesState,
32
- LocalRenameFileState,
33
- RunCommandState,
34
- } from '@/tools/local-system/type';
19
+ import { LocalSystemExecutionRuntime } from '@/tools/local-system/ExecutionRuntime';
35
20
 
36
21
  /* eslint-disable typescript-sort-keys/interface */
37
22
  export interface LocalFileAction {
38
- internal_triggerLocalFileToolCalling: <T = any>(
23
+ internal_triggerLocalFileToolCalling: (
39
24
  id: string,
40
- callingService: () => Promise<{ content: any; state?: T }>,
25
+ callingService: () => Promise<{ content: string; error?: any; state?: any; success: boolean }>,
41
26
  ) => Promise<boolean>;
42
27
 
43
28
  // File Operations
@@ -63,6 +48,8 @@ export interface LocalFileAction {
63
48
  }
64
49
  /* eslint-enable typescript-sort-keys/interface */
65
50
 
51
+ const runtime = new LocalSystemExecutionRuntime();
52
+
66
53
  /* eslint-disable sort-keys-fix/sort-keys-fix */
67
54
  export const localSystemSlice: StateCreator<
68
55
  ChatStore,
@@ -72,148 +59,49 @@ export const localSystemSlice: StateCreator<
72
59
  > = (set, get) => ({
73
60
  // ==================== File Editing ====================
74
61
  editLocalFile: async (id, params) => {
75
- return get().internal_triggerLocalFileToolCalling<EditLocalFileState>(id, async () => {
76
- const result = await localFileService.editLocalFile(params);
77
-
78
- const message = result.success
79
- ? `Successfully replaced ${result.replacements} occurrence(s) in ${params.file_path}`
80
- : `Edit failed: ${result.error}`;
81
-
82
- const state: EditLocalFileState = { message, result };
83
-
84
- return { content: result, state };
62
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
63
+ return await runtime.editLocalFile(params);
85
64
  });
86
65
  },
87
66
 
88
67
  writeLocalFile: async (id, params) => {
89
68
  return get().internal_triggerLocalFileToolCalling(id, async () => {
90
- const result = await localFileService.writeFile(params);
91
-
92
- let content: { message: string; success: boolean };
93
-
94
- if (result.success) {
95
- content = {
96
- message: `成功写入文件 ${params.path}`,
97
- success: true,
98
- };
99
- } else {
100
- const errorMessage = result.error;
101
-
102
- content = { message: errorMessage || '写入文件失败', success: false };
103
- }
104
- return { content };
69
+ return await runtime.writeLocalFile(params);
105
70
  });
106
71
  },
107
72
  moveLocalFiles: async (id, params) => {
108
- return get().internal_triggerLocalFileToolCalling<LocalMoveFilesState>(id, async () => {
109
- const results: LocalMoveFilesResultItem[] = await localFileService.moveLocalFiles(params);
110
-
111
- // 检查所有文件是否成功移动以更新消息内容
112
- const allSucceeded = results.every((r) => r.success);
113
- const someFailed = results.some((r) => !r.success);
114
- const successCount = results.filter((r) => r.success).length;
115
- const failedCount = results.length - successCount;
116
-
117
- let message = '';
118
-
119
- if (allSucceeded) {
120
- message = `Successfully moved ${results.length} item(s).`;
121
- } else if (someFailed) {
122
- message = `Moved ${successCount} item(s) successfully. Failed to move ${failedCount} item(s).`;
123
- } else {
124
- // 所有都失败了?
125
- message = `Failed to move all ${results.length} item(s).`;
126
- }
127
-
128
- const state: LocalMoveFilesState = { results, successCount, totalCount: results.length };
129
-
130
- return { content: { message, results }, state };
73
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
74
+ return await runtime.moveLocalFiles(params);
131
75
  });
132
76
  },
133
77
  renameLocalFile: async (id, params) => {
134
- return get().internal_triggerLocalFileToolCalling<LocalRenameFileState>(id, async () => {
135
- const { path: currentPath, newName } = params;
136
-
137
- // Basic validation for newName (can be done here or backend, maybe better in backend)
138
- if (
139
- !newName ||
140
- newName.includes('/') ||
141
- newName.includes('\\') ||
142
- newName === '.' ||
143
- newName === '..' ||
144
- /["*/:<>?\\|]/.test(newName)
145
- ) {
146
- throw new Error(
147
- 'Invalid new name provided. It cannot be empty, contain path separators, or invalid characters.',
148
- );
149
- }
150
-
151
- const result = await localFileService.renameLocalFile({ newName, path: currentPath }); // Call the specific service
152
-
153
- let state: LocalRenameFileState;
154
- let content: { message: string; success: boolean };
155
-
156
- if (result.success) {
157
- state = { newPath: result.newPath!, oldPath: currentPath, success: true };
158
- // Simplified message
159
- content = {
160
- message: `Successfully renamed file ${currentPath} to ${newName}.`,
161
- success: true,
162
- };
163
- } else {
164
- const errorMessage = result.error;
165
- state = {
166
- error: errorMessage,
167
- newPath: '',
168
- oldPath: params.path,
169
- success: false,
170
- };
171
- content = { message: errorMessage, success: false };
172
- }
173
- return { content, state };
78
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
79
+ return await runtime.renameLocalFile(params);
174
80
  });
175
81
  },
176
82
 
177
83
  // ==================== Search & Find ====================
178
84
  grepContent: async (id, params) => {
179
- return get().internal_triggerLocalFileToolCalling<GrepContentState>(id, async () => {
180
- const result = await localFileService.grepContent(params);
181
-
182
- const message = result.success
183
- ? `Found ${result.total_matches} matches in ${result.matches.length} locations`
184
- : 'Search failed';
185
-
186
- const state: GrepContentState = { message, result };
187
-
188
- return { content: result, state };
85
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
86
+ return await runtime.grepContent(params);
189
87
  });
190
88
  },
191
89
 
192
90
  globLocalFiles: async (id, params) => {
193
- return get().internal_triggerLocalFileToolCalling<GlobFilesState>(id, async () => {
194
- const result = await localFileService.globFiles(params);
195
-
196
- const message = result.success ? `Found ${result.total_files} files` : 'Glob search failed';
197
-
198
- const state: GlobFilesState = { message, result };
199
-
200
- return { content: result, state };
91
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
92
+ return await runtime.globLocalFiles(params);
201
93
  });
202
94
  },
203
95
 
204
96
  searchLocalFiles: async (id, params) => {
205
- return get().internal_triggerLocalFileToolCalling<LocalFileSearchState>(id, async () => {
206
- const result = await localFileService.searchLocalFiles(params);
207
- const state: LocalFileSearchState = { searchResults: result };
208
- return { content: result, state };
97
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
98
+ return await runtime.searchLocalFiles(params);
209
99
  });
210
100
  },
211
101
 
212
102
  listLocalFiles: async (id, params) => {
213
- return get().internal_triggerLocalFileToolCalling<LocalFileListState>(id, async () => {
214
- const result = await localFileService.listLocalFiles(params);
215
- const state: LocalFileListState = { listResults: result };
216
- return { content: result, state };
103
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
104
+ return await runtime.listLocalFiles(params);
217
105
  });
218
106
  },
219
107
 
@@ -226,67 +114,31 @@ export const localSystemSlice: StateCreator<
226
114
  },
227
115
 
228
116
  readLocalFile: async (id, params) => {
229
- return get().internal_triggerLocalFileToolCalling<LocalReadFileState>(id, async () => {
230
- const result = await localFileService.readLocalFile(params);
231
- const state: LocalReadFileState = { fileContent: result };
232
- return { content: result, state };
117
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
118
+ return await runtime.readLocalFile(params);
233
119
  });
234
120
  },
235
121
 
236
122
  readLocalFiles: async (id, params) => {
237
- return get().internal_triggerLocalFileToolCalling<LocalReadFilesState>(id, async () => {
238
- const results = await localFileService.readLocalFiles(params);
239
- const state: LocalReadFilesState = { filesContent: results };
240
- return { content: results, state };
123
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
124
+ return await runtime.readLocalFiles(params);
241
125
  });
242
126
  },
243
127
 
244
128
  // ==================== Shell Commands ====================
245
129
  runCommand: async (id, params) => {
246
- return get().internal_triggerLocalFileToolCalling<RunCommandState>(id, async () => {
247
- const result = await localFileService.runCommand(params);
248
-
249
- let message: string;
250
-
251
- if (result.success) {
252
- if (result.shell_id) {
253
- message = `Command started in background with shell_id: ${result.shell_id}`;
254
- } else {
255
- message = `Command completed successfully.`;
256
- }
257
- } else {
258
- message = `Command failed: ${result.error}`;
259
- }
260
-
261
- const state: RunCommandState = { message, result };
262
-
263
- return { content: result, state };
130
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
131
+ return await runtime.runCommand(params);
264
132
  });
265
133
  },
266
134
  killCommand: async (id, params) => {
267
- return get().internal_triggerLocalFileToolCalling<KillCommandState>(id, async () => {
268
- const result = await localFileService.killCommand(params);
269
-
270
- const message = result.success
271
- ? `Successfully killed shell: ${params.shell_id}`
272
- : `Failed to kill shell: ${result.error}`;
273
-
274
- const state: KillCommandState = { message, result };
275
-
276
- return { content: result, state };
135
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
136
+ return await runtime.killCommand(params);
277
137
  });
278
138
  },
279
139
  getCommandOutput: async (id, params) => {
280
- return get().internal_triggerLocalFileToolCalling<GetCommandOutputState>(id, async () => {
281
- const result = await localFileService.getCommandOutput(params);
282
-
283
- const message = result.success
284
- ? `Output retrieved. Running: ${result.running}`
285
- : `Failed: ${result.error}`;
286
-
287
- const state: GetCommandOutputState = { message, result };
288
-
289
- return { content: result, state };
140
+ return get().internal_triggerLocalFileToolCalling(id, async () => {
141
+ return await runtime.getCommandOutput(params);
290
142
  });
291
143
  },
292
144
 
@@ -305,11 +157,22 @@ export const localSystemSlice: StateCreator<
305
157
  internal_triggerLocalFileToolCalling: async (id, callingService) => {
306
158
  get().toggleLocalFileLoading(id, true);
307
159
  try {
308
- const { state, content } = await callingService();
309
- if (state) {
310
- await get().optimisticUpdatePluginState(id, state as any);
160
+ const { state, content, success, error } = await callingService();
161
+
162
+ if (success) {
163
+ if (state) {
164
+ await get().optimisticUpdatePluginState(id, state);
165
+ }
166
+ await get().optimisticUpdateMessageContent(id, content);
167
+ } else {
168
+ await get().optimisticUpdateMessagePluginError(id, {
169
+ body: error,
170
+ message: error?.message || 'Operation failed',
171
+ type: 'PluginServerError',
172
+ });
173
+ // Still update content even if failed, to show error message
174
+ await get().optimisticUpdateMessageContent(id, content);
311
175
  }
312
- await get().optimisticUpdateMessageContent(id, JSON.stringify(content));
313
176
  } catch (error) {
314
177
  await get().optimisticUpdateMessagePluginError(id, {
315
178
  body: error,
@@ -1,6 +1,9 @@
1
+ import { LocalSystemManifest } from './local-system';
2
+ import { LocalSystemExecutionRuntime } from './local-system/ExecutionRuntime';
1
3
  import { WebBrowsingManifest } from './web-browsing';
2
4
  import { WebBrowsingExecutionRuntime } from './web-browsing/ExecutionRuntime';
3
5
 
4
6
  export const BuiltinToolServerRuntimes: Record<string, any> = {
7
+ [LocalSystemManifest.identifier]: LocalSystemExecutionRuntime,
5
8
  [WebBrowsingManifest.identifier]: WebBrowsingExecutionRuntime,
6
9
  };