@lobehub/chat 1.81.8 → 1.82.0

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 (66) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/plugin.json +33 -2
  4. package/locales/bg-BG/plugin.json +33 -2
  5. package/locales/de-DE/plugin.json +33 -2
  6. package/locales/en-US/plugin.json +33 -2
  7. package/locales/es-ES/plugin.json +33 -2
  8. package/locales/fa-IR/plugin.json +33 -2
  9. package/locales/fr-FR/plugin.json +33 -2
  10. package/locales/it-IT/plugin.json +33 -2
  11. package/locales/ja-JP/plugin.json +33 -2
  12. package/locales/ko-KR/plugin.json +33 -2
  13. package/locales/nl-NL/plugin.json +33 -2
  14. package/locales/pl-PL/plugin.json +33 -2
  15. package/locales/pt-BR/plugin.json +33 -2
  16. package/locales/ru-RU/plugin.json +33 -2
  17. package/locales/tr-TR/plugin.json +33 -2
  18. package/locales/vi-VN/plugin.json +33 -2
  19. package/locales/zh-CN/plugin.json +33 -2
  20. package/locales/zh-TW/plugin.json +33 -2
  21. package/package.json +1 -1
  22. package/packages/electron-client-ipc/src/events/localFile.ts +8 -2
  23. package/packages/electron-client-ipc/src/events/system.ts +3 -0
  24. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  25. package/packages/electron-client-ipc/src/types/localFile.ts +46 -0
  26. package/packages/electron-client-ipc/src/types/system.ts +24 -0
  27. package/packages/file-loaders/src/blackList.ts +9 -0
  28. package/packages/file-loaders/src/index.ts +1 -0
  29. package/packages/file-loaders/src/loaders/pdf/index.test.ts +1 -0
  30. package/packages/file-loaders/src/loaders/pdf/index.ts +1 -7
  31. package/src/components/FileIcon/index.tsx +7 -3
  32. package/src/components/ManifestPreviewer/index.tsx +4 -1
  33. package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
  34. package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
  35. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
  36. package/src/features/PluginAvatar/index.tsx +2 -1
  37. package/src/features/PluginDevModal/MCPManifestForm.tsx +164 -0
  38. package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
  39. package/src/features/PluginDevModal/index.tsx +43 -34
  40. package/src/features/PluginStore/AddPluginButton.tsx +3 -1
  41. package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
  42. package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
  43. package/src/features/PluginStore/PluginItem/index.tsx +4 -3
  44. package/src/features/PluginTag/index.tsx +8 -2
  45. package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
  46. package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
  47. package/src/libs/mcp/index.ts +2 -0
  48. package/src/libs/mcp/types.ts +27 -0
  49. package/src/locales/default/plugin.ts +34 -3
  50. package/src/server/routers/tools/index.ts +2 -0
  51. package/src/server/routers/tools/mcp.ts +79 -0
  52. package/src/server/services/mcp/index.ts +157 -0
  53. package/src/services/electron/localFileService.ts +19 -0
  54. package/src/services/electron/system.ts +21 -0
  55. package/src/services/mcp.ts +25 -0
  56. package/src/store/chat/slices/builtinTool/actions/search.ts +0 -3
  57. package/src/store/chat/slices/plugin/action.ts +46 -2
  58. package/src/tools/local-files/Render/ListFiles/index.tsx +24 -17
  59. package/src/tools/local-files/Render/ReadLocalFile/ReadFileView.tsx +28 -28
  60. package/src/tools/local-files/components/FileItem.tsx +9 -11
  61. package/src/tools/local-files/index.ts +60 -2
  62. package/src/tools/local-files/systemRole.ts +53 -13
  63. package/src/tools/local-files/type.ts +19 -1
  64. package/src/tools/web-browsing/systemRole.ts +40 -38
  65. package/src/types/tool/plugin.ts +9 -0
  66. /package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -0
@@ -0,0 +1,79 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { z } from 'zod';
3
+
4
+ import { isDesktop, isServerMode } from '@/const/version';
5
+ import { passwordProcedure } from '@/libs/trpc/edge';
6
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
7
+ import { mcpService } from '@/server/services/mcp';
8
+
9
+ // Define Zod schemas for MCP Client parameters
10
+ const httpParamsSchema = z.object({
11
+ name: z.string().min(1),
12
+ type: z.literal('http'),
13
+ url: z.string().url(),
14
+ });
15
+
16
+ const stdioParamsSchema = z.object({
17
+ args: z.array(z.string()).optional().default([]),
18
+ command: z.string().min(1),
19
+ name: z.string().min(1),
20
+ type: z.literal('stdio'),
21
+ });
22
+
23
+ // Union schema for MCPClientParams
24
+ const mcpClientParamsSchema = z.union([httpParamsSchema, stdioParamsSchema]);
25
+
26
+ const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) => {
27
+ if (params.type === 'stdio' && !isDesktop) {
28
+ throw new TRPCError({
29
+ code: 'BAD_REQUEST',
30
+ message: 'Stdio MCP type is not supported in web environment.',
31
+ });
32
+ }
33
+ };
34
+
35
+ const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
36
+
37
+ export const mcpRouter = router({
38
+ getStreamableMcpServerManifest: mcpProcedure
39
+ .input(
40
+ z.object({
41
+ identifier: z.string(),
42
+ url: z.string().url(),
43
+ }),
44
+ )
45
+ .query(async ({ input }) => {
46
+ return await mcpService.getStreamableMcpServerManifest(input.identifier, input.url);
47
+ }),
48
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
49
+ // --- MCP Interaction ---
50
+ // listTools now accepts MCPClientParams directly
51
+ listTools: mcpProcedure
52
+ .input(mcpClientParamsSchema) // Use the unified schema
53
+ .query(async ({ input }) => {
54
+ // Stdio check can be done here or rely on the service/client layer
55
+ checkStdioEnvironment(input);
56
+
57
+ // Pass the validated MCPClientParams to the service
58
+ return await mcpService.listTools(input);
59
+ }),
60
+
61
+ // callTool now accepts MCPClientParams, toolName, and args
62
+ callTool: mcpProcedure
63
+ .input(
64
+ z.object({
65
+ params: mcpClientParamsSchema, // Use the unified schema for client params
66
+ args: z.any(), // Arguments for the tool call
67
+ toolName: z.string(),
68
+ }),
69
+ )
70
+ .mutation(async ({ input }) => {
71
+ // Stdio check can be done here or rely on the service/client layer
72
+ checkStdioEnvironment(input.params);
73
+
74
+ // Pass the validated params, toolName, and args to the service
75
+ const data = await mcpService.callTool(input.params, input.toolName, input.args);
76
+
77
+ return JSON.stringify(data);
78
+ }),
79
+ });
@@ -0,0 +1,157 @@
1
+ import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
2
+ import { TRPCError } from '@trpc/server';
3
+ import debug from 'debug';
4
+
5
+ import { MCPClient, MCPClientParams } from '@/libs/mcp';
6
+ import { safeParseJSON } from '@/utils/safeParseJSON';
7
+
8
+ const log = debug('lobe-mcp:service');
9
+
10
+ // Removed MCPConnection interface as it's no longer needed
11
+
12
+ class MCPService {
13
+ // Store instances of the custom MCPClient, keyed by serialized MCPClientParams
14
+ private clients: Map<string, MCPClient> = new Map();
15
+
16
+ constructor() {
17
+ log('MCPService initialized');
18
+ }
19
+
20
+ // --- MCP Interaction ---
21
+
22
+ // listTools now accepts MCPClientParams
23
+ async listTools(params: MCPClientParams): Promise<LobeChatPluginApi[]> {
24
+ const client = await this.getClient(params); // Get client using params
25
+ log(`Listing tools using client for params: %O`, params);
26
+
27
+ try {
28
+ const result = await client.listTools();
29
+ log(`Tools listed successfully for params: %O, result count: %d`, params, result.length);
30
+ return result.map<LobeChatPluginApi>((item) => ({
31
+ // Assuming identifier is the unique name/id
32
+ description: item.description,
33
+ name: item.name,
34
+ parameters: item.inputSchema as PluginSchema,
35
+ }));
36
+ } catch (error) {
37
+ console.error(`Error listing tools for params %O:`, params, error);
38
+ // Propagate a TRPCError for better handling upstream
39
+ throw new TRPCError({
40
+ cause: error,
41
+ code: 'INTERNAL_SERVER_ERROR',
42
+ message: `Error listing tools from MCP server: ${(error as Error).message}`,
43
+ });
44
+ }
45
+ }
46
+
47
+ // callTool now accepts MCPClientParams, toolName, and args
48
+ async callTool(params: MCPClientParams, toolName: string, argsStr: any): Promise<any> {
49
+ const client = await this.getClient(params); // Get client using params
50
+
51
+ const args = safeParseJSON(argsStr);
52
+
53
+ log(`Calling tool "${toolName}" using client for params: %O with args: %O`, params, args);
54
+
55
+ try {
56
+ // Delegate the call to the MCPClient instance
57
+ const result = await client.callTool(toolName, args); // Pass args directly
58
+ log(`Tool "${toolName}" called successfully for params: %O, result: %O`, params, result);
59
+ const { content, isError } = result;
60
+ if (!isError) return content;
61
+
62
+ return result;
63
+ } catch (error) {
64
+ console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
65
+ // Propagate a TRPCError
66
+ throw new TRPCError({
67
+ cause: error,
68
+ code: 'INTERNAL_SERVER_ERROR',
69
+ message: `Error calling tool "${toolName}" on MCP server: ${(error as Error).message}`,
70
+ });
71
+ }
72
+ }
73
+
74
+ // TODO: Consider adding methods for managing the client lifecycle if needed,
75
+ // e.g., explicitly closing clients on shutdown or after inactivity,
76
+ // although for serverless, on-demand creation/retrieval might be sufficient.
77
+
78
+ // TODO: Implement methods like listResources, getResource, listPrompts, getPrompt if needed,
79
+ // following the pattern of accepting MCPClientParams.
80
+
81
+ // --- Client Management (Replaces Connection Management) ---
82
+
83
+ // Private method to get or initialize a client based on parameters
84
+ private async getClient(params: MCPClientParams): Promise<MCPClient> {
85
+ const key = this.serializeParams(params); // Use custom serialization
86
+ log(`Attempting to get client for key: ${key} (params: %O)`, params);
87
+
88
+ if (this.clients.has(key)) {
89
+ log(`Returning cached client for key: ${key}`);
90
+ return this.clients.get(key)!;
91
+ }
92
+
93
+ log(`No cached client found for key: ${key}. Initializing new client.`);
94
+ try {
95
+ // Ensure stdio is only attempted in desktop/server environments within the client itself
96
+ // or add a check here if MCPClient doesn't handle it.
97
+ // Example check (adjust based on where environment check is best handled):
98
+ // if (params.type === 'stdio' && typeof window !== 'undefined') {
99
+ // throw new Error('Stdio MCP type is not supported in browser environment.');
100
+ // }
101
+
102
+ const client = new MCPClient(params);
103
+ await client.initialize(); // Initialization logic should be within MCPClient
104
+ this.clients.set(key, client);
105
+ log(`New client initialized and cached for key: ${key}`);
106
+ return client;
107
+ } catch (error) {
108
+ console.error(`Failed to initialize MCP client for key ${key}:`, error);
109
+ // Do not cache failed initializations
110
+ throw new TRPCError({
111
+ cause: error,
112
+ code: 'INTERNAL_SERVER_ERROR',
113
+ message: `Failed to initialize MCP client: ${(error as Error).message}`,
114
+ });
115
+ }
116
+ }
117
+
118
+ // Custom serialization function to ensure consistent keys
119
+ private serializeParams(params: MCPClientParams): string {
120
+ const sortedKeys = Object.keys(params).sort();
121
+ const sortedParams: Record<string, any> = {};
122
+
123
+ for (const key of sortedKeys) {
124
+ const value = (params as any)[key];
125
+ // Sort the 'args' array if it exists
126
+ if (key === 'args' && Array.isArray(value)) {
127
+ sortedParams[key] = JSON.stringify(key);
128
+ } else {
129
+ sortedParams[key] = value;
130
+ }
131
+ }
132
+
133
+ return JSON.stringify(sortedParams);
134
+ }
135
+
136
+ async getStreamableMcpServerManifest(
137
+ identifier: string,
138
+ url: string,
139
+ ): Promise<LobeChatPluginManifest> {
140
+ const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
141
+
142
+ return {
143
+ api: tools,
144
+ identifier,
145
+ meta: {
146
+ avatar: 'MCP_AVATAR',
147
+ description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
148
+ title: identifier,
149
+ },
150
+ // TODO: temporary
151
+ type: 'mcp' as any,
152
+ };
153
+ }
154
+ }
155
+
156
+ // Export a singleton instance
157
+ export const mcpService = new MCPService();
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  ListLocalFileParams,
3
3
  LocalFileItem,
4
+ LocalMoveFilesResultItem,
4
5
  LocalReadFileParams,
5
6
  LocalReadFileResult,
6
7
  LocalReadFilesParams,
7
8
  LocalSearchFilesParams,
9
+ MoveLocalFilesParams,
8
10
  OpenLocalFileParams,
9
11
  OpenLocalFolderParams,
12
+ RenameLocalFileParams,
10
13
  dispatch,
11
14
  } from '@lobechat/electron-client-ipc';
12
15
 
@@ -34,6 +37,22 @@ class LocalFileService {
34
37
  async openLocalFolder(params: OpenLocalFolderParams) {
35
38
  return dispatch('openLocalFolder', params);
36
39
  }
40
+
41
+ async moveLocalFiles(params: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
42
+ return dispatch('moveLocalFiles', params);
43
+ }
44
+
45
+ async renameLocalFile(params: RenameLocalFileParams) {
46
+ return dispatch('renameLocalFile', params);
47
+ }
48
+
49
+ async openLocalFileOrFolder(path: string, isDirectory: boolean) {
50
+ if (isDirectory) {
51
+ return this.openLocalFolder({ isDirectory, path });
52
+ } else {
53
+ return this.openLocalFile({ path });
54
+ }
55
+ }
37
56
  }
38
57
 
39
58
  export const localFileService = new LocalFileService();
@@ -0,0 +1,21 @@
1
+ import { ElectronAppState, dispatch } from '@lobechat/electron-client-ipc';
2
+
3
+ /**
4
+ * Service class for interacting with Electron's system-level information and actions.
5
+ */
6
+ class ElectronSystemService {
7
+ /**
8
+ * Fetches the application state from the Electron main process.
9
+ * This includes system information (platform, arch) and user-specific paths.
10
+ * @returns {Promise<DesktopAppState>} A promise that resolves with the desktop app state.
11
+ */
12
+ async getAppState(): Promise<ElectronAppState> {
13
+ // Calls the underlying IPC function to get data from the main process
14
+ return dispatch('getDesktopAppState');
15
+ }
16
+
17
+ // Add other system-related service methods here if needed in the future
18
+ }
19
+
20
+ // Export a singleton instance of the service
21
+ export const electronSystemService = new ElectronSystemService();
@@ -0,0 +1,25 @@
1
+ import { toolsClient } from '@/libs/trpc/client';
2
+ import { getToolStoreState } from '@/store/tool';
3
+ import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
4
+ import { ChatToolPayload } from '@/types/message';
5
+
6
+ class MCPService {
7
+ async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
8
+ const s = getToolStoreState();
9
+ const { identifier, arguments: args, apiName } = payload;
10
+ const plugin = pluginSelectors.getCustomPluginById(identifier)(s);
11
+
12
+ if (!plugin) return;
13
+
14
+ return toolsClient.mcp.callTool.mutate(
15
+ { args, params: { ...plugin.customParams?.mcp, name: identifier } as any, toolName: apiName },
16
+ { signal },
17
+ );
18
+ }
19
+
20
+ async getStreamableMcpServerManifest(identifier: string, url: string) {
21
+ return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
22
+ }
23
+ }
24
+
25
+ export const mcpService = new MCPService();
@@ -190,9 +190,6 @@ export const searchSlice: StateCreator<
190
190
 
191
191
  await get().internal_updateMessageContent(id, JSON.stringify(searchContent));
192
192
 
193
- // 如果没搜索到结果,那么不触发 ai 总结
194
- if (searchContent.length === 0) return;
195
-
196
193
  // 如果 aiSummary 为 true,则会自动触发总结
197
194
  return aiSummary;
198
195
  },
@@ -8,6 +8,7 @@ import { StateCreator } from 'zustand/vanilla';
8
8
  import { LOADING_FLAT } from '@/const/message';
9
9
  import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
10
10
  import { chatService } from '@/services/chat';
11
+ import { mcpService } from '@/services/mcp';
11
12
  import { messageService } from '@/services/message';
12
13
  import { ChatStore } from '@/store/chat/store';
13
14
  import { useToolStore } from '@/store/tool';
@@ -41,6 +42,7 @@ export interface ChatPluginAction {
41
42
  invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
42
43
  invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
43
44
  invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
45
+ invokeMCPTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
44
46
 
45
47
  invokeStandaloneTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
46
48
 
@@ -271,7 +273,7 @@ export const chatPlugin: StateCreator<
271
273
  // trigger the plugin call
272
274
  const data = await get().internal_invokeDifferentTypePlugin(id, payload);
273
275
 
274
- if ((payload.type === 'default' || payload.type === 'builtin') && data) {
276
+ if (data && !['markdown', 'standalone'].includes(payload.type)) {
275
277
  shouldCreateMessage = true;
276
278
  latestToolId = id;
277
279
  }
@@ -328,7 +330,9 @@ export const chatPlugin: StateCreator<
328
330
 
329
331
  const updateAssistantMessage = async () => {
330
332
  if (!assistantMessage) return;
331
- await messageService.updateMessage(assistantMessage!.id, { tools: assistantMessage?.tools });
333
+ await messageService.updateMessage(assistantMessage!.id, {
334
+ tools: assistantMessage?.tools,
335
+ });
332
336
  };
333
337
 
334
338
  await Promise.all([
@@ -438,11 +442,51 @@ export const chatPlugin: StateCreator<
438
442
  return await get().invokeBuiltinTool(id, payload);
439
443
  }
440
444
 
445
+ // @ts-ignore
446
+ case 'mcp': {
447
+ return await get().invokeMCPTypePlugin(id, payload);
448
+ }
449
+
441
450
  default: {
442
451
  return await get().invokeDefaultTypePlugin(id, payload);
443
452
  }
444
453
  }
445
454
  },
455
+ invokeMCPTypePlugin: async (id, payload) => {
456
+ const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
457
+ get();
458
+ let data: string = '';
459
+
460
+ try {
461
+ const abortController = internal_togglePluginApiCalling(
462
+ true,
463
+ id,
464
+ n('fetchPlugin/start') as string,
465
+ );
466
+
467
+ const result = await mcpService.invokeMcpToolCall(payload, {
468
+ signal: abortController?.signal,
469
+ });
470
+ if (!!result) data = result;
471
+ } catch (error) {
472
+ console.log(error);
473
+ const err = error as Error;
474
+
475
+ // ignore the aborted request error
476
+ if (!err.message.includes('The user aborted a request.')) {
477
+ await messageService.updateMessageError(id, error as any);
478
+ await refreshMessages();
479
+ }
480
+ }
481
+
482
+ internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
483
+ // 如果报错则结束了
484
+ if (!data) return;
485
+
486
+ await internal_updateMessageContent(id, data);
487
+
488
+ return data;
489
+ },
446
490
 
447
491
  internal_togglePluginApiCalling: (loading, id, action) => {
448
492
  return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
@@ -1,12 +1,10 @@
1
1
  import { ListLocalFileParams } from '@lobechat/electron-client-ipc';
2
- import { ActionIcon } from '@lobehub/ui';
3
2
  import { Typography } from 'antd';
4
3
  import { createStyles } from 'antd-style';
5
- import { FolderOpen } from 'lucide-react';
6
4
  import React, { memo } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
5
  import { Flexbox } from 'react-layout-kit';
9
6
 
7
+ import FileIcon from '@/components/FileIcon';
10
8
  import { localFileService } from '@/services/electron/localFileService';
11
9
  import { LocalFileListState } from '@/tools/local-files/type';
12
10
  import { ChatMessagePluginError } from '@/types/message';
@@ -20,8 +18,21 @@ const useStyles = createStyles(({ css, token, cx }) => ({
20
18
  opacity: 1;
21
19
  transition: opacity 0.2s ${token.motionEaseInOut};
22
20
  `),
21
+ container: css`
22
+ cursor: pointer;
23
+
24
+ padding-block: 2px;
25
+ padding-inline: 4px;
26
+ border-radius: 4px;
27
+
28
+ color: ${token.colorTextSecondary};
29
+
30
+ :hover {
31
+ color: ${token.colorText};
32
+ background: ${token.colorFillTertiary};
33
+ }
34
+ `,
23
35
  path: css`
24
- padding-inline-start: 8px;
25
36
  color: ${token.colorTextSecondary};
26
37
  `,
27
38
  }));
@@ -34,25 +45,21 @@ interface ListFilesProps {
34
45
  }
35
46
 
36
47
  const ListFiles = memo<ListFilesProps>(({ messageId, pluginError, args, pluginState }) => {
37
- const { t } = useTranslation('tool');
38
-
39
48
  const { styles } = useStyles();
40
49
  return (
41
50
  <>
42
- <Flexbox gap={8} horizontal>
51
+ <Flexbox
52
+ className={styles.container}
53
+ gap={8}
54
+ horizontal
55
+ onClick={() => {
56
+ localFileService.openLocalFolder({ isDirectory: true, path: args.path });
57
+ }}
58
+ >
59
+ <FileIcon fileName={args.path} isDirectory size={22} variant={'pure'} />
43
60
  <Typography.Text className={styles.path} ellipsis>
44
61
  {args.path}
45
62
  </Typography.Text>
46
- <Flexbox className={styles.actions} gap={8} horizontal style={{ marginLeft: 8 }}>
47
- <ActionIcon
48
- icon={FolderOpen}
49
- onClick={() => {
50
- localFileService.openLocalFolder({ isDirectory: true, path: args.path });
51
- }}
52
- size="small"
53
- title={t('localFiles.openFolder')}
54
- />
55
- </Flexbox>
56
63
  </Flexbox>
57
64
  <SearchResult
58
65
  listResults={pluginState?.listResults}
@@ -46,6 +46,9 @@ const useStyles = createStyles(({ css, token, cx }) => ({
46
46
  header: css`
47
47
  cursor: pointer;
48
48
  `,
49
+ lineCount: css`
50
+ color: ${token.colorTextQuaternary};
51
+ `,
49
52
  meta: css`
50
53
  font-size: 12px;
51
54
  color: ${token.colorTextTertiary};
@@ -70,7 +73,6 @@ const useStyles = createStyles(({ css, token, cx }) => ({
70
73
  background: ${token.colorFillQuaternary};
71
74
  `,
72
75
  previewText: css`
73
- font-family: ${token.fontFamilyCode};
74
76
  font-size: 12px;
75
77
  line-height: 1.6;
76
78
  word-break: break-all;
@@ -84,14 +86,7 @@ interface ReadFileViewProps extends LocalReadFileResult {
84
86
  }
85
87
 
86
88
  const ReadFileView = memo<ReadFileViewProps>(
87
- ({
88
- filename,
89
- path,
90
- fileType,
91
- charCount,
92
- lineCount, // Assuming the 250 is total lines?
93
- content, // The actual content preview
94
- }) => {
89
+ ({ filename, path, fileType, charCount, content, totalLineCount, totalCharCount, loc }) => {
95
90
  const { t } = useTranslation('tool');
96
91
  const { styles } = useStyles();
97
92
  const [isExpanded, setIsExpanded] = useState(false);
@@ -115,6 +110,7 @@ const ReadFileView = memo<ReadFileViewProps>(
115
110
  <Flexbox
116
111
  align={'center'}
117
112
  className={styles.header}
113
+ gap={12}
118
114
  horizontal
119
115
  justify={'space-between'}
120
116
  onClick={handleToggleExpand}
@@ -126,7 +122,7 @@ const ReadFileView = memo<ReadFileViewProps>(
126
122
  {filename}
127
123
  </Typography.Text>
128
124
  {/* Actions on Hover */}
129
- <Flexbox className={styles.actions} gap={8} horizontal style={{ marginLeft: 8 }}>
125
+ <Flexbox className={styles.actions} gap={2} horizontal style={{ marginLeft: 8 }}>
130
126
  <ActionIcon
131
127
  icon={ExternalLink}
132
128
  onClick={handleOpenFile}
@@ -143,16 +139,26 @@ const ReadFileView = memo<ReadFileViewProps>(
143
139
  </Flexbox>
144
140
  </Flexbox>
145
141
  <Flexbox align={'center'} className={styles.meta} gap={8} horizontal>
146
- <Flexbox align={'center'} gap={4} horizontal>
147
- <Icon icon={Asterisk} size={'small'} />
148
- <span>{charCount}</span>
149
- </Flexbox>
142
+ {isExpanded && (
143
+ <Flexbox align={'center'} gap={4} horizontal>
144
+ <Icon icon={Asterisk} size={'small'} />
145
+ <span>
146
+ {charCount} / <span className={styles.lineCount}>{totalCharCount}</span>
147
+ </span>
148
+ </Flexbox>
149
+ )}
150
150
  <Flexbox align={'center'} gap={4} horizontal>
151
151
  <Icon icon={AlignLeft} size={'small'} />
152
- <span>
153
- {content?.split('\n').length || 0} / {lineCount}
154
- </span>
155
- {/* Display preview lines / total lines */}
152
+ {isExpanded ? (
153
+ <span>
154
+ L{loc?.[0]}-{loc?.[1]} /{' '}
155
+ <span className={styles.lineCount}>{totalLineCount}</span>
156
+ </span>
157
+ ) : (
158
+ <span>
159
+ L{loc?.[0]}-{loc?.[1]}
160
+ </span>
161
+ )}
156
162
  </Flexbox>
157
163
  <ActionIcon
158
164
  active={isExpanded}
@@ -160,7 +166,6 @@ const ReadFileView = memo<ReadFileViewProps>(
160
166
  onClick={handleToggleExpand}
161
167
  size="small"
162
168
  style={{
163
- marginLeft: 8,
164
169
  transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
165
170
  transition: 'transform 0.2s',
166
171
  }}
@@ -173,19 +178,14 @@ const ReadFileView = memo<ReadFileViewProps>(
173
178
  {path}
174
179
  </Typography.Text>
175
180
 
176
- {/* Content Preview (Collapsible) */}
177
181
  {isExpanded && (
178
- <Flexbox className={styles.previewBox}>
182
+ <Flexbox className={styles.previewBox} style={{ maxHeight: 240, overflow: 'auto' }}>
179
183
  {fileType === 'md' ? (
180
- <Markdown style={{ maxHeight: 240, overflow: 'auto' }}>{content}</Markdown>
184
+ <Markdown>{content}</Markdown>
181
185
  ) : (
182
- <Typography.Paragraph
183
- className={styles.previewText}
184
- ellipsis={{ expandable: true, rows: 10, symbol: t('localFiles.read.more') }}
185
- style={{ maxHeight: 240, overflow: 'auto' }}
186
- >
186
+ <div className={styles.previewText} style={{ width: '100%' }}>
187
187
  {content}
188
- </Typography.Paragraph>
188
+ </div>
189
189
  )}
190
190
  </Flexbox>
191
191
  )}
@@ -1,5 +1,5 @@
1
1
  import { LocalFileItem } from '@lobechat/electron-client-ipc';
2
- import { ActionIcon, FileTypeIcon } from '@lobehub/ui';
2
+ import { ActionIcon } from '@lobehub/ui';
3
3
  import { createStyles } from 'antd-style';
4
4
  import dayjs from 'dayjs';
5
5
  import { FolderOpen } from 'lucide-react';
@@ -64,22 +64,20 @@ const FileItem = memo<FileItemProps>(
64
64
  gap={12}
65
65
  horizontal
66
66
  onClick={() => {
67
- if (isDirectory) {
68
- localFileService.openLocalFolder({ isDirectory, path });
69
- } else {
70
- localFileService.openLocalFile({ path });
71
- }
67
+ localFileService.openLocalFileOrFolder(path, isDirectory);
72
68
  }}
73
69
  onMouseEnter={() => setIsHovering(true)}
74
70
  onMouseLeave={() => setIsHovering(false)}
75
71
  padding={'2px 8px'}
76
72
  style={{ cursor: 'pointer', fontSize: 12, width: '100%' }}
77
73
  >
78
- {isDirectory ? (
79
- <FileTypeIcon size={16} type={'folder'} variant={'mono'} />
80
- ) : (
81
- <FileIcon fileName={name} fileType={type} size={16} variant={'pure'} />
82
- )}
74
+ <FileIcon
75
+ fileName={name}
76
+ fileType={type}
77
+ isDirectory={isDirectory}
78
+ size={16}
79
+ variant={'pure'}
80
+ />
83
81
  <Flexbox
84
82
  align={'baseline'}
85
83
  gap={4}