From be99aaebd0bfc0726c7bf73badd6ba56cec1d75e Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 2 Apr 2026 19:42:45 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20unify=20tool?= =?UTF-8?q?=20content=20formatting=20with=20ComputerRuntime=20and=20shared?= =?UTF-8?q?=20UI=20(#13470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: unify tool content formatting with ComputerRuntime and shared UI components Introduce `@lobechat/tool-runtime` with `ComputerRuntime` abstract class to ensure consistent content formatting (via `formatCommandResult`, `formatFileContent`, etc.) across local-system, cloud-sandbox, and skills packages. Create `@lobechat/shared-tool-ui` to share Render and Inspector components, eliminating duplicated UI code across tool packages. Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: address review issues — state mapping for renders and IPC param denormalization - Add legacy state field mappings in local-system executor (listResults, fileContent, searchResults) for backward compatibility with existing render components - Add denormalizeParams in LocalSystemExecutionRuntime to map ComputerRuntime params back to IPC-expected field names (file_path, items, shell_id, etc.) - Fix i18n type casting for dynamic translation keys in shared-tool-ui inspectors Co-Authored-By: Claude Opus 4.6 (1M context) * ♻️ refactor: inject render capabilities via context, unify state shape for cross-package render reuse - Add ToolRenderContext with injectable capabilities (openFile, openFolder, isLoading, displayRelativePath) to shared-tool-ui - Update local-system render components (ReadLocalFile, ListFiles, SearchFiles, MoveLocalFiles, FileItem) to use context instead of direct Electron imports - Enrich ReadFileState with render-compatible fields (filename, fileType, charCount, loc, totalCharCount) - Cloud-sandbox now fully reuses local-system renders — renders degrade gracefully when capabilities are not provided (no open file buttons in sandbox) - Remove executor-level state mapping hacks Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: fix sandbox render bugs — SearchFiles, GrepContent, MoveFiles, GlobFiles - SearchFiles: ensure results is always an array (not object passthrough) - GrepContent: update formatGrepResults to support object matches `{path, content, lineNumber}` alongside string matches - MoveFiles: render now handles both IPC format (items/oldPath/newPath) and ComputerRuntime format (operations/source/destination) - GlobFiles: fallback totalCount to files.length when API returns 0 Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: unify SearchLocalFiles inspector with shared factory SearchLocalFiles inspector now supports all keyword field variants (keyword, keywords, query) and reads from unified state (results/totalCount). Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: handle missing path in grep matches to avoid undefined display Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: improve render field compatibility for sandbox - EditLocalFile render: support both file_path (IPC) and path (sandbox) args - SearchFiles render: support keyword/keywords/query arg variants - FileItem: derive name from path when not provided Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: add missing cloud-sandbox i18n key for noResults Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- package.json | 1 + .../builtin-tool-cloud-sandbox/package.json | 5 + .../src/ExecutionRuntime/index.ts | 537 +----------------- .../client/Inspector/EditLocalFile/index.tsx | 95 +--- .../client/Inspector/GlobLocalFiles/index.tsx | 72 +-- .../client/Inspector/GrepContent/index.tsx | 69 +-- .../client/Inspector/ListLocalFiles/index.tsx | 69 +-- .../client/Inspector/ReadLocalFile/index.tsx | 75 +-- .../src/client/Inspector/RunCommand/index.tsx | 64 +-- .../Inspector/SearchLocalFiles/index.tsx | 70 +-- .../client/Inspector/WriteLocalFile/index.tsx | 58 +- .../src/client/Render/index.ts | 27 +- .../src/executor/index.ts | 16 +- .../src/types/state.ts | 119 +--- .../builtin-tool-local-system/package.json | 7 +- .../src/ExecutionRuntime/index.ts | 285 ++++++++++ .../client/Inspector/EditLocalFile/index.tsx | 90 +-- .../client/Inspector/GlobLocalFiles/index.tsx | 76 +-- .../client/Inspector/GrepContent/index.tsx | 75 +-- .../client/Inspector/ListLocalFiles/index.tsx | 68 +-- .../client/Inspector/ReadLocalFile/index.tsx | 66 +-- .../src/client/Inspector/RunCommand/index.tsx | 71 +-- .../Inspector/SearchLocalFiles/index.tsx | 77 +-- .../client/Inspector/WriteLocalFile/index.tsx | 51 +- .../src/client/Render/EditLocalFile/index.tsx | 8 +- .../src/client/Render/ListFiles/Result.tsx | 12 +- .../src/client/Render/ListFiles/index.tsx | 9 +- .../Render/MoveLocalFiles/MoveFileItem.tsx | 9 +- .../client/Render/MoveLocalFiles/index.tsx | 21 +- .../Render/ReadLocalFile/ReadFileView.tsx | 125 ++-- .../src/client/Render/ReadLocalFile/index.tsx | 35 +- .../src/client/Render/RunCommand/index.tsx | 70 --- .../src/client/Render/SearchFiles/Result.tsx | 10 +- .../Render/SearchFiles/SearchQuery/index.tsx | 18 +- .../src/client/Render/SearchFiles/index.tsx | 7 +- .../src/client/Render/index.ts | 5 +- .../src/client/components/FileItem.tsx | 45 +- .../src/executor/index.ts | 455 ++++----------- .../builtin-tool-local-system/src/index.ts | 2 - .../builtin-tool-local-system/src/types.ts | 59 +- packages/builtin-tool-skills/package.json | 5 +- .../src/ExecutionRuntime/index.ts | 90 ++- .../src/client/Inspector/RunCommand/index.tsx | 59 +- .../src/client/Render/RunCommand/index.tsx | 55 -- .../src/client/Render/index.ts | 5 +- .../prompts/fileSystem/formatGrepResults.ts | 16 +- packages/shared-tool-ui/package.json | 24 + .../src/Inspector/EditLocalFile/index.tsx | 109 ++++ .../src/Inspector/GlobLocalFiles/index.tsx | 68 +++ .../src/Inspector/GrepContent/index.tsx | 76 +++ .../src/Inspector/ListLocalFiles/index.tsx | 53 ++ .../src/Inspector/ReadLocalFile/index.tsx | 61 ++ .../src/Inspector/RunCommand/index.tsx | 88 +++ .../src/Inspector/SearchLocalFiles/index.tsx | 86 +++ .../src/Inspector/WriteLocalFile/index.tsx | 61 ++ .../shared-tool-ui/src/Inspector/index.ts | 8 + .../src}/Render/RunCommand/index.tsx | 17 +- packages/shared-tool-ui/src/Render/index.ts | 1 + .../src/components/FilePathDisplay.tsx | 65 +++ packages/shared-tool-ui/src/context.tsx | 26 + packages/shared-tool-ui/src/index.ts | 2 + packages/shared-tool-ui/src/styles.ts | 73 +++ packages/tool-runtime/package.json | 15 + packages/tool-runtime/src/ComputerRuntime.ts | 473 +++++++++++++++ packages/tool-runtime/src/index.ts | 2 + packages/tool-runtime/src/types.ts | 192 +++++++ .../PluginsUI/Render/BuiltinType/index.tsx | 25 +- .../Render/BuiltinType/useToolRenderCaps.ts | 39 ++ src/locales/default/plugin.ts | 1 + 69 files changed, 2305 insertions(+), 2523 deletions(-) create mode 100644 packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts delete mode 100644 packages/builtin-tool-local-system/src/client/Render/RunCommand/index.tsx delete mode 100644 packages/builtin-tool-skills/src/client/Render/RunCommand/index.tsx create mode 100644 packages/shared-tool-ui/package.json create mode 100644 packages/shared-tool-ui/src/Inspector/EditLocalFile/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/GlobLocalFiles/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/GrepContent/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/ListLocalFiles/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/ReadLocalFile/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/SearchLocalFiles/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/WriteLocalFile/index.tsx create mode 100644 packages/shared-tool-ui/src/Inspector/index.ts rename packages/{builtin-tool-cloud-sandbox/src/client => shared-tool-ui/src}/Render/RunCommand/index.tsx (79%) create mode 100644 packages/shared-tool-ui/src/Render/index.ts create mode 100644 packages/shared-tool-ui/src/components/FilePathDisplay.tsx create mode 100644 packages/shared-tool-ui/src/context.tsx create mode 100644 packages/shared-tool-ui/src/index.ts create mode 100644 packages/shared-tool-ui/src/styles.ts create mode 100644 packages/tool-runtime/package.json create mode 100644 packages/tool-runtime/src/ComputerRuntime.ts create mode 100644 packages/tool-runtime/src/index.ts create mode 100644 packages/tool-runtime/src/types.ts create mode 100644 src/features/PluginsUI/Render/BuiltinType/useToolRenderCaps.ts diff --git a/package.json b/package.json index 099e64c2a7..8352b03524 100644 --- a/package.json +++ b/package.json @@ -256,6 +256,7 @@ "@lobechat/openapi": "workspace:*", "@lobechat/prompts": "workspace:*", "@lobechat/python-interpreter": "workspace:*", + "@lobechat/shared-tool-ui": "workspace:*", "@lobechat/ssrf-safe-fetch": "workspace:*", "@lobechat/utils": "workspace:*", "@lobechat/web-crawler": "workspace:*", diff --git a/packages/builtin-tool-cloud-sandbox/package.json b/packages/builtin-tool-cloud-sandbox/package.json index 2c204f508d..619a8172fd 100644 --- a/packages/builtin-tool-cloud-sandbox/package.json +++ b/packages/builtin-tool-cloud-sandbox/package.json @@ -9,6 +9,11 @@ "./executionRuntime": "./src/ExecutionRuntime/index.ts" }, "main": "./src/index.ts", + "dependencies": { + "@lobechat/builtin-tool-local-system": "workspace:*", + "@lobechat/shared-tool-ui": "workspace:*", + "@lobechat/tool-runtime": "workspace:*" + }, "devDependencies": { "@lobechat/types": "workspace:*" }, diff --git a/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts b/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts index 921274cceb..11ffd5429d 100644 --- a/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +++ b/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts @@ -1,338 +1,46 @@ -import { - formatEditResult, - formatFileContent, - formatFileList, - formatFileSearchResults, - formatGlobResults, - formatMoveResults, - formatRenameResult, - formatWriteResult, -} from '@lobechat/prompts'; +import { ComputerRuntime } from '@lobechat/tool-runtime'; import type { BuiltinServerRuntimeOutput } from '@lobechat/types'; import type { - EditLocalFileParams, - EditLocalFileState, ExecuteCodeParams, ExecuteCodeState, ExportFileParams, ExportFileState, - GetCommandOutputParams, - GetCommandOutputState, - GlobFilesState, - GlobLocalFilesParams, - GrepContentParams, - GrepContentState, ISandboxService, - KillCommandParams, - KillCommandState, - ListLocalFilesParams, - ListLocalFilesState, - MoveLocalFilesParams, - MoveLocalFilesState, - ReadLocalFileParams, - ReadLocalFileState, - RenameLocalFileParams, - RenameLocalFileState, - RunCommandParams, - RunCommandState, - SearchLocalFilesParams, - SearchLocalFilesState, - WriteLocalFileParams, - WriteLocalFileState, + SandboxCallToolResult, } from '../types'; /** * Cloud Sandbox Execution Runtime * - * This runtime executes tools via the injected ISandboxService. - * The service handles context (topicId, userId) internally - Runtime doesn't need to know about it. + * Extends ComputerRuntime for standard computer operations (files, shell, search). + * Adds cloud-specific capabilities: code execution and file export. * * Dependency Injection: * - Client: Inject codeInterpreterService (uses tRPC client) * - Server: Inject ServerSandboxService (uses MarketSDK directly) */ -export class CloudSandboxExecutionRuntime { +export class CloudSandboxExecutionRuntime extends ComputerRuntime { private sandboxService: ISandboxService; constructor(sandboxService: ISandboxService) { + super(); this.sandboxService = sandboxService; } - // ==================== File Operations ==================== - - async listLocalFiles(args: ListLocalFilesParams): Promise { - try { - const result = await this.callTool('listLocalFiles', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { files: [] }, - success: true, - }; - } - - const files = result.result?.files || []; - const state: ListLocalFilesState = { files }; - - const content = formatFileList({ - directory: args.directoryPath, - files: files.map((f: { isDirectory: boolean; name: string }) => ({ - isDirectory: f.isDirectory, - name: f.name, - })), - }); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } + protected async callService( + toolName: string, + params: Record, + ): Promise { + return this.sandboxService.callTool(toolName, params); } - async readLocalFile(args: ReadLocalFileParams): Promise { - try { - const result = await this.callTool('readLocalFile', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - content: '', - endLine: args.endLine, - path: args.path, - startLine: args.startLine, - }, - success: true, - }; - } - - const state: ReadLocalFileState = { - content: result.result?.content || '', - endLine: args.endLine, - path: args.path, - startLine: args.startLine, - totalLines: result.result?.totalLines, - }; - - const lineRange: [number, number] | undefined = - args.startLine !== undefined && args.endLine !== undefined - ? [args.startLine, args.endLine] - : undefined; - - const content = formatFileContent({ - content: result.result?.content || '', - lineRange, - path: args.path, - }); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async writeLocalFile(args: WriteLocalFileParams): Promise { - try { - const result = await this.callTool('writeLocalFile', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - path: args.path, - success: false, - }, - success: true, - }; - } - - const state: WriteLocalFileState = { - bytesWritten: result.result?.bytesWritten, - path: args.path, - success: result.success, - }; - - const content = formatWriteResult({ - path: args.path, - success: true, - }); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async editLocalFile(args: EditLocalFileParams): Promise { - try { - const result = await this.callTool('editLocalFile', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - path: args.path, - replacements: 0, - }, - success: true, - }; - } - - const state: EditLocalFileState = { - diffText: result.result?.diffText, - linesAdded: result.result?.linesAdded, - linesDeleted: result.result?.linesDeleted, - path: args.path, - replacements: result.result?.replacements || 0, - }; - - const content = formatEditResult({ - filePath: args.path, - linesAdded: state.linesAdded, - linesDeleted: state.linesDeleted, - replacements: state.replacements, - }); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async searchLocalFiles(args: SearchLocalFilesParams): Promise { - try { - const result = await this.callTool('searchLocalFiles', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - results: [], - totalCount: 0, - }, - success: true, - }; - } - - const results = result.result?.results || []; - const state: SearchLocalFilesState = { - results, - totalCount: result.result?.totalCount || 0, - }; - - const content = formatFileSearchResults( - results.map((r: { path: string }) => ({ path: r.path })), - ); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async moveLocalFiles(args: MoveLocalFilesParams): Promise { - try { - const result = await this.callTool('moveLocalFiles', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - results: [], - successCount: 0, - totalCount: args.operations.length, - }, - success: true, - }; - } - - const results = result.result?.results || []; - const state: MoveLocalFilesState = { - results, - successCount: result.result?.successCount || 0, - totalCount: args.operations.length, - }; - - const content = formatMoveResults(results); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async renameLocalFile(args: RenameLocalFileParams): Promise { - try { - const result = await this.callTool('renameLocalFile', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - error: result.error?.message, - newPath: '', - oldPath: args.oldPath, - success: false, - }, - success: true, - }; - } - - const state: RenameLocalFileState = { - error: result.result?.error, - newPath: result.result?.newPath || '', - oldPath: args.oldPath, - success: result.success, - }; - - const content = formatRenameResult({ - error: result.result?.error, - newName: args.newName, - oldPath: args.oldPath, - success: result.success, - }); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - // ==================== Code Execution ==================== + // ==================== Cloud-Specific: Code Execution ==================== async executeCode(args: ExecuteCodeParams): Promise { try { const language = args.language || 'python'; - const result = await this.callTool('executeCode', { + const result = await this.callService('executeCode', { code: args.code, language, }); @@ -360,207 +68,20 @@ export class CloudSandboxExecutionRuntime { success: true, }; } catch (error) { - console.log('executeCode error', error); + console.error('executeCode error', error); return this.handleError(error); } } - // ==================== Shell Commands ==================== - - async runCommand(args: RunCommandParams): Promise { - try { - const result = await this.callTool('runCommand', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - error: result.error?.message, - isBackground: args.background || false, - success: false, - }, - success: true, - }; - } - - const state: RunCommandState = { - commandId: result.result?.commandId, - error: result.result?.error, - exitCode: result.result?.exitCode, - isBackground: args.background || false, - output: result.result?.output, - stderr: result.result?.stderr, - success: result.success, - }; - - return { - content: JSON.stringify(result.result), - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async getCommandOutput(args: GetCommandOutputParams): Promise { - try { - const result = await this.callTool('getCommandOutput', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - error: result.error?.message, - running: false, - success: false, - }, - success: true, - }; - } - - const state: GetCommandOutputState = { - error: result.result?.error, - newOutput: result.result?.newOutput, - running: result.result?.running ?? false, - success: result.success, - }; - - return { - content: JSON.stringify(result.result), - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async killCommand(args: KillCommandParams): Promise { - try { - const result = await this.callTool('killCommand', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - commandId: args.commandId, - error: result.error?.message, - success: false, - }, - success: true, - }; - } - - const state: KillCommandState = { - commandId: args.commandId, - error: result.result?.error, - success: result.success, - }; - - return { - content: JSON.stringify({ - message: `Successfully killed command: ${args.commandId}`, - success: true, - }), - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - // ==================== Search & Find ==================== - - async grepContent(args: GrepContentParams): Promise { - try { - const result = await this.callTool('grepContent', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - matches: [], - pattern: args.pattern, - totalMatches: 0, - }, - success: true, - }; - } - - const state: GrepContentState = { - matches: result.result?.matches || [], - pattern: args.pattern, - totalMatches: result.result?.totalMatches || 0, - }; - - return { - content: JSON.stringify(result.result), - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - async globLocalFiles(args: GlobLocalFilesParams): Promise { - try { - const result = await this.callTool('globLocalFiles', args); - - if (!result.success) { - return { - content: result.error?.message || JSON.stringify(result.error), - state: { - files: [], - pattern: args.pattern, - totalCount: 0, - }, - success: true, - }; - } - - const files = result.result?.files || []; - const totalCount = result.result?.totalCount || 0; - - const state: GlobFilesState = { - files, - pattern: args.pattern, - totalCount, - }; - - const content = formatGlobResults({ - files, - totalFiles: totalCount, - }); - - return { - content, - state, - success: true, - }; - } catch (error) { - return this.handleError(error); - } - } - - // ==================== Export Operations ==================== + // ==================== Cloud-Specific: File Export ==================== /** * Export a file from the sandbox to cloud storage - * Uses a single call that handles: - * 1. Generate pre-signed upload URL - * 2. Call sandbox to upload file - * 3. Create persistent file record - * 4. Return permanent /f/:id URL */ async exportFile(args: ExportFileParams): Promise { try { - // Extract filename from path const filename = args.path.split('/').pop() || 'exported_file'; - // Single call that handles everything: upload URL generation, sandbox upload, and file record creation const result = await this.sandboxService.exportAndUploadFile(args.path, filename); const state: ExportFileState = { @@ -594,32 +115,4 @@ export class CloudSandboxExecutionRuntime { return this.handleError(error); } } - - // ==================== Helper Methods ==================== - - /** - * Call a tool via the injected sandbox service - */ - private async callTool( - toolName: string, - params: Record, - ): Promise<{ - error?: { message: string; name?: string }; - result: any; - sessionExpiredAndRecreated?: boolean; - success: boolean; - }> { - const result = await this.sandboxService.callTool(toolName, params); - - return result; - } - - private handleError(error: unknown): BuiltinServerRuntimeOutput { - const errorMessage = error instanceof Error ? error.message : String(error); - return { - content: errorMessage, - error, - success: false, - }; - } } diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/EditLocalFile/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/EditLocalFile/index.tsx index b354484d1d..3a201b6647 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/EditLocalFile/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/EditLocalFile/index.tsx @@ -1,94 +1,7 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Icon, Text } from '@lobehub/ui'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Minus, Plus } from 'lucide-react'; -import type { ReactNode } from 'react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createEditLocalFileInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { EditLocalFileState } from '../../../types'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -const styles = createStaticStyles(({ css, cssVar }) => ({ - separator: css` - margin-inline: 2px; - color: ${cssVar.colorTextQuaternary}; - `, -})); - -interface EditLocalFileParams { - file_path: string; - new_string: string; - old_string: string; -} - -export const EditLocalFileInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const filePath = args?.file_path || partialArgs?.file_path || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!filePath) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.editLocalFile')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.editLocalFile')}: - -
- ); - } - - // Build stats parts with colors and icons - const linesAdded = pluginState?.linesAdded ?? 0; - const linesDeleted = pluginState?.linesDeleted ?? 0; - - const statsParts: ReactNode[] = []; - if (linesAdded > 0) { - statsParts.push( - - - {linesAdded} - , - ); - } - if (linesDeleted > 0) { - statsParts.push( - - - {linesDeleted} - , - ); - } - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.editLocalFile')}: - - {!isLoading && statsParts.length > 0 && ( - <> - {' '} - {statsParts.map((part, index) => ( - - {index > 0 && / } - {part} - - ))} - - )} -
- ); -}); - -EditLocalFileInspector.displayName = 'EditLocalFileInspector'; +export const EditLocalFileInspector = createEditLocalFileInspector( + 'builtins.lobe-cloud-sandbox.apiName.editLocalFile', +); diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GlobLocalFiles/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GlobLocalFiles/index.tsx index 9e4913d62d..76a4cc5b87 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GlobLocalFiles/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GlobLocalFiles/index.tsx @@ -1,73 +1,7 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Check, X } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createGlobLocalFilesInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { GlobFilesState } from '../../../types'; - -const styles = createStaticStyles(({ css }) => ({ - statusIcon: css` - margin-block-end: -2px; - margin-inline-start: 4px; - `, -})); - -interface GlobFilesParams { - path?: string; - pattern: string; -} - -export const GlobLocalFilesInspector = memo>( - ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const pattern = args?.pattern || partialArgs?.pattern || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!pattern) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.globLocalFiles')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.globLocalFiles')}: - {pattern} -
- ); - } - - // Check if glob was successful - const totalCount = pluginState?.totalCount ?? 0; - const hasResults = totalCount > 0; - - return ( -
- - {t('builtins.lobe-cloud-sandbox.apiName.globLocalFiles')}: - {pattern && {pattern}} - {isLoading ? null : pluginState ? ( - hasResults ? ( - <> - ({totalCount}) - - - ) : ( - - ) - ) : null} - -
- ); - }, +export const GlobLocalFilesInspector = createGlobLocalFilesInspector( + 'builtins.lobe-cloud-sandbox.apiName.globLocalFiles', ); - -GlobLocalFilesInspector.displayName = 'GlobLocalFilesInspector'; diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GrepContent/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GrepContent/index.tsx index b53f086fac..66d9f1099a 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GrepContent/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GrepContent/index.tsx @@ -1,69 +1,8 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createGrepContentInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { GrepContentState } from '../../../types'; - -interface GrepContentParams { - include?: string; - path?: string; - pattern: string; -} - -export const GrepContentInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const pattern = args?.pattern || partialArgs?.pattern || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!pattern) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.grepContent')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.grepContent')}: - {pattern} -
- ); - } - - // Check result count - const resultCount = pluginState?.totalMatches ?? 0; - const hasResults = resultCount > 0; - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.grepContent')}: - {pattern && {pattern}} - {!isLoading && - pluginState && - (hasResults ? ( - ({resultCount}) - ) : ( - - ({t('builtins.lobe-local-system.inspector.noResults')}) - - ))} -
- ); +export const GrepContentInspector = createGrepContentInspector({ + noResultsKey: 'builtins.lobe-cloud-sandbox.inspector.noResults', + translationKey: 'builtins.lobe-cloud-sandbox.apiName.grepContent', }); - -GrepContentInspector.displayName = 'GrepContentInspector'; diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ListLocalFiles/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ListLocalFiles/index.tsx index 5838394a6b..94397c63ad 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ListLocalFiles/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ListLocalFiles/index.tsx @@ -1,68 +1,7 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createListLocalFilesInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { ListLocalFilesState } from '../../../types'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -interface ListLocalFilesParams { - path: string; -} - -export const ListLocalFilesInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const path = args?.path || partialArgs?.path || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!path) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.listLocalFiles')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.listLocalFiles')}: - -
- ); - } - - // Show result count if available - const resultCount = pluginState?.files?.length ?? 0; - const hasResults = resultCount > 0; - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.listLocalFiles')}: - - {!isLoading && - pluginState?.files && - (hasResults ? ( - ({resultCount}) - ) : ( - - ({t('builtins.lobe-local-system.inspector.noResults')}) - - ))} -
- ); -}); - -ListLocalFilesInspector.displayName = 'ListLocalFilesInspector'; +export const ListLocalFilesInspector = createListLocalFilesInspector( + 'builtins.lobe-cloud-sandbox.apiName.listLocalFiles', +); diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ReadLocalFile/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ReadLocalFile/index.tsx index c0923435b9..9e29e984fc 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ReadLocalFile/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ReadLocalFile/index.tsx @@ -1,74 +1,7 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { createStaticStyles, cx } from 'antd-style'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createReadLocalFileInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { ReadLocalFileState } from '../../../types'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -const styles = createStaticStyles(({ css }) => ({ - lineRange: css` - flex-shrink: 0; - margin-inline-start: 4px; - opacity: 0.7; - `, -})); - -interface ReadLocalFileParams { - end_line?: number; - path: string; - start_line?: number; -} - -export const ReadLocalFileInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, isLoading }) => { - const { t } = useTranslation('plugin'); - - const filePath = args?.path || partialArgs?.path || ''; - const startLine = args?.start_line || partialArgs?.start_line; - const endLine = args?.end_line || partialArgs?.end_line; - - // Format line range display, e.g., "L1-L200" - const lineRangeText = useMemo(() => { - if (startLine === undefined && endLine === undefined) return null; - const start = startLine ?? 1; - const end = endLine; - if (end !== undefined) { - return `L${start}-L${end}`; - } - return `L${start}`; - }, [startLine, endLine]); - - // During argument streaming - if (isArgumentsStreaming) { - if (!filePath) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.readLocalFile')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.readLocalFile')}: - - {lineRangeText && {lineRangeText}} -
- ); - } - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.readLocalFile')}: - - {lineRangeText && {lineRangeText}} -
- ); -}); - -ReadLocalFileInspector.displayName = 'ReadLocalFileInspector'; +export const ReadLocalFileInspector = createReadLocalFileInspector( + 'builtins.lobe-cloud-sandbox.apiName.readLocalFile', +); diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/RunCommand/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/RunCommand/index.tsx index b95e8f66f1..521e14d5ce 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/RunCommand/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/RunCommand/index.tsx @@ -1,65 +1,7 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Check, X } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createRunCommandInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { RunCommandState } from '../../../types'; - -const styles = createStaticStyles(({ css }) => ({ - statusIcon: css` - margin-block-end: -2px; - margin-inline-start: 4px; - `, -})); - -interface RunCommandParams { - background?: boolean; - command: string; - description: string; - timeout?: number; -} - -export const RunCommandInspector = memo>( - ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const description = args?.description || partialArgs?.description; - - if (isArgumentsStreaming) { - if (!description) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.runCommand')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.runCommand')}: - {description} -
- ); - } - - return ( -
- - {t('builtins.lobe-cloud-sandbox.apiName.runCommand')}: - {description && {description}} - {isLoading ? null : pluginState?.success && pluginState?.exitCode === 0 ? ( - - ) : ( - - )} - -
- ); - }, +export const RunCommandInspector = createRunCommandInspector( + 'builtins.lobe-cloud-sandbox.apiName.runCommand', ); - -RunCommandInspector.displayName = 'RunCommandInspector'; diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/SearchLocalFiles/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/SearchLocalFiles/index.tsx index fa662c0b91..398f9051e4 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/SearchLocalFiles/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/SearchLocalFiles/index.tsx @@ -1,70 +1,8 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createSearchLocalFilesInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { SearchLocalFilesState } from '../../../types'; - -interface SearchLocalFilesParams { - path?: string; - query: string; -} - -export const SearchLocalFilesInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const query = args?.query || partialArgs?.query || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!query) - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.searchLocalFiles')} -
- ); - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.searchLocalFiles')}: - {query} -
- ); - } - - // Check if search returned results - const resultCount = pluginState?.results?.length ?? 0; - const hasResults = resultCount > 0; - - return ( -
- - {t('builtins.lobe-cloud-sandbox.apiName.searchLocalFiles')}: - {query && {query}} - {!isLoading && - pluginState?.results && - (hasResults ? ( - ({resultCount}) - ) : ( - - ({t('builtins.lobe-local-system.inspector.noResults')}) - - ))} - -
- ); +export const SearchLocalFilesInspector = createSearchLocalFilesInspector({ + noResultsKey: 'builtins.lobe-cloud-sandbox.inspector.noResults', + translationKey: 'builtins.lobe-cloud-sandbox.apiName.searchLocalFiles', }); - -SearchLocalFilesInspector.displayName = 'SearchLocalFilesInspector'; diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/WriteLocalFile/index.tsx b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/WriteLocalFile/index.tsx index 7641efda1b..403198c893 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Inspector/WriteLocalFile/index.tsx +++ b/packages/builtin-tool-cloud-sandbox/src/client/Inspector/WriteLocalFile/index.tsx @@ -1,57 +1,7 @@ 'use client'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Icon, Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { Plus } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createWriteLocalFileInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { WriteLocalFileState } from '../../../types'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -interface WriteLocalFileParams { - content: string; - path: string; -} - -export const WriteLocalFileInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming }) => { - const { t } = useTranslation('plugin'); - - const filePath = args?.path || partialArgs?.path || ''; - const content = args?.content || partialArgs?.content || ''; - - // Calculate lines from content - const lines = content ? content.split('\n').length : 0; - - // During argument streaming without path - if (isArgumentsStreaming && !filePath) { - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.writeLocalFile')} -
- ); - } - - return ( -
- {t('builtins.lobe-cloud-sandbox.apiName.writeLocalFile')}: - - {lines > 0 && ( - - {' '} - - {lines} - - )} -
- ); -}); - -WriteLocalFileInspector.displayName = 'WriteLocalFileInspector'; +export const WriteLocalFileInspector = createWriteLocalFileInspector( + 'builtins.lobe-cloud-sandbox.apiName.writeLocalFile', +); diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Render/index.ts b/packages/builtin-tool-cloud-sandbox/src/client/Render/index.ts index 6639b44786..7031c9ed71 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Render/index.ts +++ b/packages/builtin-tool-cloud-sandbox/src/client/Render/index.ts @@ -1,27 +1,26 @@ +import { LocalSystemRenders } from '@lobechat/builtin-tool-local-system/client'; +import { RunCommandRender } from '@lobechat/shared-tool-ui/renders'; + import { CloudSandboxApiName } from '../../types'; -import EditLocalFile from './EditLocalFile'; import ExecuteCode from './ExecuteCode'; import ExportFile from './ExportFile'; -import ListFiles from './ListFiles'; -import MoveLocalFiles from './MoveLocalFiles'; -import ReadLocalFile from './ReadLocalFile'; -import RunCommand from './RunCommand'; -import SearchFiles from './SearchFiles'; -import WriteFile from './WriteFile'; /** * Cloud Sandbox Render Components Registry + * + * Reuses local-system renders for shared file/shell operations. + * Only cloud-specific tools (executeCode, exportFile) have their own renders. */ export const CloudSandboxRenders = { - [CloudSandboxApiName.editLocalFile]: EditLocalFile, + [CloudSandboxApiName.editLocalFile]: LocalSystemRenders.editLocalFile, [CloudSandboxApiName.executeCode]: ExecuteCode, [CloudSandboxApiName.exportFile]: ExportFile, - [CloudSandboxApiName.listLocalFiles]: ListFiles, - [CloudSandboxApiName.moveLocalFiles]: MoveLocalFiles, - [CloudSandboxApiName.readLocalFile]: ReadLocalFile, - [CloudSandboxApiName.runCommand]: RunCommand, - [CloudSandboxApiName.searchLocalFiles]: SearchFiles, - [CloudSandboxApiName.writeLocalFile]: WriteFile, + [CloudSandboxApiName.listLocalFiles]: LocalSystemRenders.listLocalFiles, + [CloudSandboxApiName.moveLocalFiles]: LocalSystemRenders.moveLocalFiles, + [CloudSandboxApiName.readLocalFile]: LocalSystemRenders.readLocalFile, + [CloudSandboxApiName.runCommand]: RunCommandRender, + [CloudSandboxApiName.searchLocalFiles]: LocalSystemRenders.searchLocalFiles, + [CloudSandboxApiName.writeLocalFile]: LocalSystemRenders.writeLocalFile, }; // Export API names for use in other modules diff --git a/packages/builtin-tool-cloud-sandbox/src/executor/index.ts b/packages/builtin-tool-cloud-sandbox/src/executor/index.ts index 25d9a826fc..7cdde9f32c 100644 --- a/packages/builtin-tool-cloud-sandbox/src/executor/index.ts +++ b/packages/builtin-tool-cloud-sandbox/src/executor/index.ts @@ -90,7 +90,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.listLocalFiles(params); + const result = await runtime.listFiles(params); return this.toBuiltinResult(result); }; @@ -99,7 +99,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.readLocalFile(params); + const result = await runtime.readFile(params); return this.toBuiltinResult(result); }; @@ -108,7 +108,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.writeLocalFile(params); + const result = await runtime.writeFile(params); return this.toBuiltinResult(result); }; @@ -117,7 +117,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.editLocalFile(params); + const result = await runtime.editFile(params); return this.toBuiltinResult(result); }; @@ -126,7 +126,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.searchLocalFiles(params); + const result = await runtime.searchFiles(params); return this.toBuiltinResult(result); }; @@ -135,7 +135,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.moveLocalFiles(params); + const result = await runtime.moveFiles(params); return this.toBuiltinResult(result); }; @@ -144,7 +144,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.renameLocalFile(params); + const result = await runtime.renameFile(params); return this.toBuiltinResult(result); }; @@ -204,7 +204,7 @@ class CloudSandboxExecutor extends BaseExecutor { ctx: BuiltinToolContext, ): Promise => { const runtime = this.getRuntime(ctx); - const result = await runtime.globLocalFiles(params); + const result = await runtime.globFiles(params); return this.toBuiltinResult(result); }; diff --git a/packages/builtin-tool-cloud-sandbox/src/types/state.ts b/packages/builtin-tool-cloud-sandbox/src/types/state.ts index 33b088a0b1..e07930151f 100644 --- a/packages/builtin-tool-cloud-sandbox/src/types/state.ts +++ b/packages/builtin-tool-cloud-sandbox/src/types/state.ts @@ -1,70 +1,20 @@ -// ==================== File Operations ==================== +// Re-export shared state types from @lobechat/tool-runtime +export type { + EditFileState as EditLocalFileState, + GetCommandOutputState, + GlobFilesState, + GrepContentState, + KillCommandState, + ListFilesState as ListLocalFilesState, + MoveFilesState as MoveLocalFilesState, + ReadFileState as ReadLocalFileState, + RenameFileState as RenameLocalFileState, + RunCommandState, + SearchFilesState as SearchLocalFilesState, + WriteFileState as WriteLocalFileState, +} from '@lobechat/tool-runtime'; -export interface ListLocalFilesState { - files: Array<{ - isDirectory: boolean; - name: string; - path: string; - size?: number; - }>; -} - -export interface ReadLocalFileState { - content: string; - endLine?: number; - path: string; - startLine?: number; - totalLines?: number; -} - -export interface WriteLocalFileState { - bytesWritten?: number; - path: string; - success: boolean; -} - -export interface EditLocalFileState { - diffText?: string; - linesAdded?: number; - linesDeleted?: number; - path: string; - replacements: number; -} - -export interface SearchLocalFilesState { - results: Array<{ - isDirectory: boolean; - modifiedAt?: string; - name: string; - path: string; - size?: number; - }>; - totalCount: number; -} - -export interface MoveLocalFilesState { - results: Array<{ - destination: string; - error?: string; - source: string; - success: boolean; - }>; - successCount: number; - totalCount: number; -} - -export interface RenameLocalFileState { - error?: string; - newPath: string; - oldPath: string; - success: boolean; -} - -export interface GlobFilesState { - files: string[]; - pattern: string; - totalCount: number; -} +// ==================== Cloud-Specific State ==================== export interface ExportFileState { /** The download URL for the exported file (permanent /f/:id URL) */ @@ -83,18 +33,6 @@ export interface ExportFileState { success: boolean; } -export interface GrepContentState { - matches: Array<{ - content?: string; - lineNumber?: number; - path: string; - }>; - pattern: string; - totalMatches: number; -} - -// ==================== Code Execution ==================== - export interface ExecuteCodeState { /** Error message if execution failed */ error?: string; @@ -110,31 +48,6 @@ export interface ExecuteCodeState { success: boolean; } -// ==================== Shell Commands ==================== - -export interface RunCommandState { - commandId?: string; - error?: string; - exitCode?: number; - isBackground: boolean; - output?: string; - stderr?: string; - success: boolean; -} - -export interface GetCommandOutputState { - error?: string; - newOutput?: string; - running: boolean; - success: boolean; -} - -export interface KillCommandState { - commandId: string; - error?: string; - success: boolean; -} - // ==================== Session Info ==================== export interface SessionInfo { diff --git a/packages/builtin-tool-local-system/package.json b/packages/builtin-tool-local-system/package.json index b9a3e305c2..819aa96b5e 100644 --- a/packages/builtin-tool-local-system/package.json +++ b/packages/builtin-tool-local-system/package.json @@ -5,11 +5,14 @@ "exports": { ".": "./src/index.ts", "./client": "./src/client/index.ts", - "./executor": "./src/executor/index.ts" + "./executor": "./src/executor/index.ts", + "./executionRuntime": "./src/ExecutionRuntime/index.ts" }, "main": "./src/index.ts", "dependencies": { - "@lobechat/electron-client-ipc": "workspace:*" + "@lobechat/electron-client-ipc": "workspace:*", + "@lobechat/shared-tool-ui": "workspace:*", + "@lobechat/tool-runtime": "workspace:*" }, "devDependencies": { "@lobechat/types": "workspace:*" diff --git a/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts b/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts new file mode 100644 index 0000000000..589d987c63 --- /dev/null +++ b/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts @@ -0,0 +1,285 @@ +import type { ServiceResult } from '@lobechat/tool-runtime'; +import { ComputerRuntime } from '@lobechat/tool-runtime'; +import type { BuiltinServerRuntimeOutput } from '@lobechat/types'; + +/** + * Service interface for local system operations. + * Abstracts the Electron IPC layer so the runtime is testable and decoupled. + */ +export interface ILocalSystemService { + editLocalFile: (params: any) => Promise; + getCommandOutput: (params: any) => Promise; + globFiles: (params: any) => Promise; + grepContent: (params: any) => Promise; + killCommand: (params: any) => Promise; + listLocalFiles: (params: any) => Promise; + moveLocalFiles: (params: any) => Promise; + readLocalFile: (params: any) => Promise; + readLocalFiles: (params: any) => Promise; + renameLocalFile: (params: any) => Promise; + runCommand: (params: any) => Promise; + searchLocalFiles: (params: any) => Promise; + writeFile: (params: any) => Promise; +} + +/** + * Maps IPC tool names to localFileService method names. + * IPC service uses different method names than the standard tool names. + */ +const SERVICE_METHOD_MAP: Record = { + editLocalFile: 'editLocalFile', + getCommandOutput: 'getCommandOutput', + globLocalFiles: 'globFiles', + grepContent: 'grepContent', + killCommand: 'killCommand', + listLocalFiles: 'listLocalFiles', + moveLocalFiles: 'moveLocalFiles', + readLocalFile: 'readLocalFile', + renameLocalFile: 'renameLocalFile', + runCommand: 'runCommand', + searchLocalFiles: 'searchLocalFiles', + writeLocalFile: 'writeFile', +}; + +/** + * Local System Execution Runtime + * + * Extends ComputerRuntime for standard computer operations via Electron IPC. + * Normalizes snake_case IPC results (exit_code, shell_id, total_matches) + * into the camelCase format expected by ComputerRuntime. + */ +export class LocalSystemExecutionRuntime extends ComputerRuntime { + private service: ILocalSystemService; + + constructor(service: ILocalSystemService) { + super(); + this.service = service; + } + + protected async callService( + toolName: string, + params: Record, + ): Promise { + const methodName = SERVICE_METHOD_MAP[toolName]; + if (!methodName) { + return { error: { message: `Unknown tool: ${toolName}` }, result: null, success: false }; + } + + // Map ComputerRuntime params back to IPC-expected shapes + const ipcParams = this.denormalizeParams(toolName, params); + + const method = this.service[methodName] as (params: any) => Promise; + const result = await method(ipcParams); + + return this.normalizeResult(toolName, result); + } + + /** + * Map ComputerRuntime normalized params back to IPC field names. + */ + private denormalizeParams(toolName: string, params: Record): any { + switch (toolName) { + case 'editLocalFile': { + return { + file_path: params.path, + new_string: params.replace, + old_string: params.search, + replace_all: params.all, + }; + } + + case 'listLocalFiles': { + return { + path: params.directoryPath, + sortBy: params.sortBy, + sortOrder: params.sortOrder, + }; + } + + case 'moveLocalFiles': { + return { + items: params.operations?.map((op: any) => ({ + newPath: op.destination, + oldPath: op.source, + })), + }; + } + + case 'renameLocalFile': { + return { + newName: params.newName, + path: params.oldPath, + }; + } + + case 'getCommandOutput': { + return { shell_id: params.commandId }; + } + + case 'killCommand': { + return { shell_id: params.commandId }; + } + + default: { + return params; + } + } + } + + /** + * Batch read multiple files — unique to local system. + */ + async readFiles(params: any): Promise { + try { + const { formatMultipleFiles } = await import('@lobechat/prompts'); + const results = await this.service.readLocalFiles(params); + + return { + content: formatMultipleFiles(results), + state: { filesContent: results }, + success: true, + }; + } catch (error) { + return this.handleError(error); + } + } + + /** + * Normalize raw IPC results into the ServiceResult format. + * IPC methods return domain objects directly; we wrap them appropriately. + */ + private normalizeResult(toolName: string, raw: any): ServiceResult { + switch (toolName) { + case 'runCommand': { + // RunCommandResult has snake_case fields from local-file-shell + return { + result: { + error: raw.error, + exitCode: raw.exit_code, + output: raw.output, + commandId: raw.shell_id, + stderr: raw.stderr, + stdout: raw.stdout, + success: raw.success, + }, + success: raw.success, + }; + } + + case 'getCommandOutput': { + return { + result: { + error: raw.error, + newOutput: raw.output, + running: raw.running, + success: raw.success, + }, + success: raw.success, + }; + } + + case 'killCommand': { + return { + result: { error: raw.error, success: raw.success }, + success: raw.success, + }; + } + + case 'grepContent': { + return { + result: { + matches: raw.matches, + totalMatches: raw.total_matches, + }, + success: raw.success, + }; + } + + case 'globLocalFiles': { + return { + result: { + files: raw.files, + totalCount: raw.total_files, + }, + success: raw.success, + }; + } + + case 'listLocalFiles': { + return { + result: { files: raw.files, totalCount: raw.totalCount }, + success: true, + }; + } + + case 'readLocalFile': { + // Pass through all IPC fields for render compatibility + return { + result: { + charCount: raw.charCount, + content: raw.content, + fileType: raw.fileType, + filename: raw.filename, + loc: raw.loc, + totalCharCount: raw.totalCharCount, + totalLineCount: raw.totalLineCount, + }, + success: true, + }; + } + + case 'writeLocalFile': { + return { + result: { bytesWritten: raw.bytesWritten, success: raw.success }, + success: raw.success ?? true, + }; + } + + case 'editLocalFile': { + return { + result: { + diffText: raw.diffText, + error: raw.error, + linesAdded: raw.linesAdded, + linesDeleted: raw.linesDeleted, + replacements: raw.replacements, + }, + success: raw.success, + }; + } + + case 'searchLocalFiles': { + // Returns LocalFileItem[] directly + const results = Array.isArray(raw) ? raw : []; + return { + result: { results, totalCount: results.length }, + success: true, + }; + } + + case 'moveLocalFiles': { + // Returns LocalMoveFilesResultItem[] directly + const results = Array.isArray(raw) ? raw : []; + return { + result: { + results, + successCount: results.filter((r: any) => r.success).length, + }, + success: true, + }; + } + + case 'renameLocalFile': { + return { + result: { error: raw.error, newPath: raw.newPath, success: raw.success }, + success: raw.success, + }; + } + + default: { + // Generic passthrough + return { result: raw, success: true }; + } + } + } +} diff --git a/packages/builtin-tool-local-system/src/client/Inspector/EditLocalFile/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/EditLocalFile/index.tsx index adcc684b6e..e2d02a88a9 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/EditLocalFile/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/EditLocalFile/index.tsx @@ -1,89 +1,7 @@ 'use client'; -import type { EditLocalFileParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Icon, Text } from '@lobehub/ui'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Minus, Plus } from 'lucide-react'; -import type { ReactNode } from 'react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createEditLocalFileInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { EditLocalFileState } from '../../../types'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -const styles = createStaticStyles(({ css, cssVar }) => ({ - separator: css` - margin-inline: 2px; - color: ${cssVar.colorTextQuaternary}; - `, -})); - -export const EditLocalFileInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const filePath = args?.file_path || partialArgs?.file_path || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!filePath) - return ( -
- {t('builtins.lobe-local-system.apiName.editLocalFile')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.editLocalFile')}: - -
- ); - } - - // Build stats parts with colors and icons - const linesAdded = pluginState?.linesAdded ?? 0; - const linesDeleted = pluginState?.linesDeleted ?? 0; - - const statsParts: ReactNode[] = []; - if (linesAdded > 0) { - statsParts.push( - - - {linesAdded} - , - ); - } - if (linesDeleted > 0) { - statsParts.push( - - - {linesDeleted} - , - ); - } - - return ( -
- {t('builtins.lobe-local-system.apiName.editLocalFile')}: - - {!isLoading && statsParts.length > 0 && ( - <> - {' '} - {statsParts.map((part, index) => ( - - {index > 0 && / } - {part} - - ))} - - )} -
- ); -}); - -EditLocalFileInspector.displayName = 'EditLocalFileInspector'; +export const EditLocalFileInspector = createEditLocalFileInspector( + 'builtins.lobe-local-system.apiName.editLocalFile', +); diff --git a/packages/builtin-tool-local-system/src/client/Inspector/GlobLocalFiles/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/GlobLocalFiles/index.tsx index af84937ab8..b47b8b413c 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/GlobLocalFiles/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/GlobLocalFiles/index.tsx @@ -1,77 +1,7 @@ 'use client'; -import type { GlobFilesParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Text } from '@lobehub/ui'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Check, X } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createGlobLocalFilesInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { GlobFilesState } from '../../..'; - -const styles = createStaticStyles(({ css }) => ({ - statusIcon: css` - margin-block-end: -2px; - margin-inline-start: 4px; - `, -})); - -export const GlobLocalFilesInspector = memo>( - ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const pattern = args?.pattern || partialArgs?.pattern || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!pattern) - return ( -
- {t('builtins.lobe-local-system.apiName.globLocalFiles')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.globLocalFiles')}: - {pattern} -
- ); - } - - // Check if glob was successful - const isSuccess = pluginState?.result?.success; - const engine = pluginState?.result?.engine; - - return ( -
- - {t('builtins.lobe-local-system.apiName.globLocalFiles')}: - {pattern && {pattern}} - {isLoading ? null : pluginState?.result ? ( - isSuccess ? ( - - ) : ( - - ) - ) : null} - {!isLoading && engine && ( - - [{engine}] - - )} - -
- ); - }, +export const GlobLocalFilesInspector = createGlobLocalFilesInspector( + 'builtins.lobe-local-system.apiName.globLocalFiles', ); - -GlobLocalFilesInspector.displayName = 'GlobLocalFilesInspector'; diff --git a/packages/builtin-tool-local-system/src/client/Inspector/GrepContent/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/GrepContent/index.tsx index 61a9324b74..49eecce562 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/GrepContent/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/GrepContent/index.tsx @@ -1,75 +1,8 @@ 'use client'; -import type { GrepContentParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createGrepContentInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { GrepContentState } from '../../..'; - -export const GrepContentInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const pattern = args?.pattern || partialArgs?.pattern || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!pattern) - return ( -
- {t('builtins.lobe-local-system.apiName.grepContent')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.grepContent')}: - {pattern} -
- ); - } - - // Check result count - const resultCount = pluginState?.result?.total_matches ?? 0; - const hasResults = resultCount > 0; - const engine = pluginState?.result?.engine; - - return ( -
- {t('builtins.lobe-local-system.apiName.grepContent')}: - {pattern && {pattern}} - {!isLoading && - pluginState?.result && - (hasResults ? ( - ({resultCount}) - ) : ( - - ({t('builtins.lobe-local-system.inspector.noResults')}) - - ))} - {!isLoading && engine && ( - - [{engine}] - - )} -
- ); +export const GrepContentInspector = createGrepContentInspector({ + noResultsKey: 'builtins.lobe-local-system.inspector.noResults', + translationKey: 'builtins.lobe-local-system.apiName.grepContent', }); - -GrepContentInspector.displayName = 'GrepContentInspector'; diff --git a/packages/builtin-tool-local-system/src/client/Inspector/ListLocalFiles/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/ListLocalFiles/index.tsx index 5349a5748c..9fbdfcac14 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/ListLocalFiles/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/ListLocalFiles/index.tsx @@ -1,67 +1,7 @@ 'use client'; -import type { ListLocalFileParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Flexbox, Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createListLocalFilesInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { LocalFileListState } from '../../..'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -export const ListLocalFilesInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const path = args?.path || partialArgs?.path || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!path) - return ( -
- {t('builtins.lobe-local-system.apiName.listLocalFiles')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.listLocalFiles')}: - -
- ); - } - - // Show result count if available - const resultCount = pluginState?.listResults?.length ?? 0; - const hasResults = resultCount > 0; - - return ( -
- {t('builtins.lobe-local-system.apiName.listLocalFiles')}: - - - - {!isLoading && - pluginState?.listResults && - (hasResults ? ( - ({resultCount}) - ) : ( - - ({t('builtins.lobe-local-system.inspector.noResults')}) - - ))} -
- ); -}); - -ListLocalFilesInspector.displayName = 'ListLocalFilesInspector'; +export const ListLocalFilesInspector = createListLocalFilesInspector( + 'builtins.lobe-local-system.apiName.listLocalFiles', +); diff --git a/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx index 7c905d3390..24620d58f0 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx @@ -1,65 +1,7 @@ 'use client'; -import type { LocalReadFileParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { createStaticStyles, cx } from 'antd-style'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createReadLocalFileInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { LocalReadFileState } from '../../..'; -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -const styles = createStaticStyles(({ css }) => ({ - lineRange: css` - flex-shrink: 0; - margin-inline-start: 4px; - font-size: 12px; - opacity: 0.7; - `, -})); - -export const ReadLocalFileInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, isLoading }) => { - const { t } = useTranslation('plugin'); - - const filePath = args?.path || partialArgs?.path || ''; - const loc = args?.loc || partialArgs?.loc; - - // Format line range display, e.g., "L1-L200" - const lineRangeText = useMemo(() => { - if (!loc || loc.length !== 2) return null; - const [start, end] = loc; - return `L${start + 1}-L${end}`; - }, [loc]); - - // During argument streaming - if (isArgumentsStreaming) { - if (!filePath) - return ( -
- {t('builtins.lobe-local-system.apiName.readLocalFile')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.readLocalFile')}: - - {lineRangeText && {lineRangeText}} -
- ); - } - - return ( -
- {t('builtins.lobe-local-system.apiName.readLocalFile')}: - - {lineRangeText && {lineRangeText}} -
- ); -}); - -ReadLocalFileInspector.displayName = 'ReadLocalFileInspector'; +export const ReadLocalFileInspector = createReadLocalFileInspector( + 'builtins.lobe-local-system.apiName.readLocalFile', +); diff --git a/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx index 008a358658..8ac0563815 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx @@ -1,72 +1,7 @@ 'use client'; -import type { RunCommandParams, RunCommandResult } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Check, X } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createRunCommandInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -const styles = createStaticStyles(({ css }) => ({ - statusIcon: css` - margin-block-end: -2px; - margin-inline-start: 4px; - `, -})); - -interface RunCommandState { - message: string; - result: RunCommandResult; -} - -export const RunCommandInspector = memo>( - ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - // Show description if available, otherwise show command - const description = args?.description || partialArgs?.description || args?.command || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!description) - return ( -
- {t('builtins.lobe-local-system.apiName.runCommand')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.runCommand')}: - {description} -
- ); - } - - // Get execution result from pluginState - const result = pluginState?.result; - const isSuccess = result?.success || result?.exit_code === 0; - - return ( -
- - {t('builtins.lobe-local-system.apiName.runCommand')}: - {description && {description}} - {isLoading ? null : result?.success !== undefined ? ( - isSuccess ? ( - - ) : ( - - ) - ) : null} - -
- ); - }, +export const RunCommandInspector = createRunCommandInspector( + 'builtins.lobe-local-system.apiName.runCommand', ); - -RunCommandInspector.displayName = 'RunCommandInspector'; - -export default RunCommandInspector; diff --git a/packages/builtin-tool-local-system/src/client/Inspector/SearchLocalFiles/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/SearchLocalFiles/index.tsx index f67fb032d3..07b3b3ddd4 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/SearchLocalFiles/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/SearchLocalFiles/index.tsx @@ -1,77 +1,8 @@ 'use client'; -import type { LocalSearchFilesParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createSearchLocalFilesInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { LocalFileSearchState } from '../../..'; - -export const SearchLocalFilesInspector = memo< - BuiltinInspectorProps ->(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { - const { t } = useTranslation('plugin'); - - const keywords = args?.keywords || partialArgs?.keywords || ''; - - // During argument streaming - if (isArgumentsStreaming) { - if (!keywords) - return ( -
- {t('builtins.lobe-local-system.apiName.searchLocalFiles')} -
- ); - - return ( -
- {t('builtins.lobe-local-system.apiName.searchLocalFiles')}: - {keywords} -
- ); - } - - // Check if search returned results - const resultCount = pluginState?.searchResults?.length ?? 0; - const hasResults = resultCount > 0; - const engine = pluginState?.engine; - - return ( -
- - {t('builtins.lobe-local-system.apiName.searchLocalFiles')}: - {keywords && {keywords}} - {!isLoading && - pluginState?.searchResults && - (hasResults ? ( - ({resultCount}) - ) : ( - - ({t('builtins.lobe-local-system.inspector.noResults')}) - - ))} - {!isLoading && engine && ( - - [{engine}] - - )} - -
- ); +export const SearchLocalFilesInspector = createSearchLocalFilesInspector({ + noResultsKey: 'builtins.lobe-local-system.inspector.noResults', + translationKey: 'builtins.lobe-local-system.apiName.searchLocalFiles', }); - -SearchLocalFilesInspector.displayName = 'SearchLocalFilesInspector'; diff --git a/packages/builtin-tool-local-system/src/client/Inspector/WriteLocalFile/index.tsx b/packages/builtin-tool-local-system/src/client/Inspector/WriteLocalFile/index.tsx index 4fcbbe4a87..ae1beb22b0 100644 --- a/packages/builtin-tool-local-system/src/client/Inspector/WriteLocalFile/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Inspector/WriteLocalFile/index.tsx @@ -1,52 +1,7 @@ 'use client'; -import type { WriteLocalFileParams } from '@lobechat/electron-client-ipc'; -import type { BuiltinInspectorProps } from '@lobechat/types'; -import { Icon, Text } from '@lobehub/ui'; -import { cssVar, cx } from 'antd-style'; -import { Plus } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createWriteLocalFileInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import { FilePathDisplay } from '../../components/FilePathDisplay'; - -export const WriteLocalFileInspector = memo>( - ({ args, partialArgs, isArgumentsStreaming }) => { - const { t } = useTranslation('plugin'); - - const filePath = args?.path || partialArgs?.path || ''; - const content = args?.content || partialArgs?.content || ''; - - // Calculate lines from content - const lines = content ? content.split('\n').length : 0; - - // During argument streaming without path - if (isArgumentsStreaming && !filePath) { - return ( -
- {t('builtins.lobe-local-system.apiName.writeLocalFile')} -
- ); - } - - return ( -
- {t('builtins.lobe-local-system.apiName.writeLocalFile')}: - - {lines > 0 && ( - - {' '} - - {lines} - - )} -
- ); - }, +export const WriteLocalFileInspector = createWriteLocalFileInspector( + 'builtins.lobe-local-system.apiName.writeLocalFile', ); - -WriteLocalFileInspector.displayName = 'WriteLocalFileInspector'; diff --git a/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx b/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx index 657d50163f..ca16f56ab5 100644 --- a/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx @@ -1,13 +1,15 @@ import type { EditLocalFileState } from '@lobechat/builtin-tool-local-system'; -import type { EditLocalFileParams } from '@lobechat/electron-client-ipc'; import type { BuiltinRenderProps } from '@lobechat/types'; import { Alert, Flexbox, PatchDiff, Skeleton } from '@lobehub/ui'; import React, { memo } from 'react'; -const EditLocalFile = memo>( +const EditLocalFile = memo>( ({ args, pluginState, pluginError }) => { if (!args) return ; + // Support both IPC format (file_path) and ComputerRuntime format (path) + const filePath = args.file_path || args.path || ''; + return ( {pluginError ? ( @@ -19,7 +21,7 @@ const EditLocalFile = memo ) : pluginState?.diffText ? ( ; messageId: string; pluginError: ChatMessagePluginError; } const SearchFiles = memo(({ listResults = [], messageId }) => { - const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId)); + const { isLoading } = useToolRenderCapabilities(); + const loading = isLoading?.(messageId); if (loading) { return ( @@ -31,7 +29,7 @@ const SearchFiles = memo(({ listResults = [], messageId }) => return ( {listResults.map((item) => ( - + ))} ); diff --git a/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx b/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx index 569ee0bdf9..96f6a10fec 100644 --- a/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/ListFiles/index.tsx @@ -1,15 +1,14 @@ -import type { LocalFileListState } from '@lobechat/builtin-tool-local-system'; -import type { ListLocalFileParams } from '@lobechat/electron-client-ipc'; +import type { ListFilesState } from '@lobechat/tool-runtime'; import type { BuiltinRenderProps } from '@lobechat/types'; -import React, { memo } from 'react'; +import { memo } from 'react'; import SearchResult from './Result'; -const ListFiles = memo>( +const ListFiles = memo>( ({ messageId, pluginError, pluginState }) => { return ( diff --git a/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/MoveFileItem.tsx b/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/MoveFileItem.tsx index ccc47c34d5..4da1fd88b3 100644 --- a/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/MoveFileItem.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/MoveFileItem.tsx @@ -1,11 +1,9 @@ +import { useToolRenderCapabilities } from '@lobechat/shared-tool-ui'; import { Flexbox, Icon, Text } from '@lobehub/ui'; import { createStaticStyles } from 'antd-style'; import { ArrowRight } from 'lucide-react'; import { memo } from 'react'; -import { useElectronStore } from '@/store/electron'; -import { desktopStateSelectors } from '@/store/electron/selectors'; - const styles = createStaticStyles(({ css, cssVar }) => ({ icon: css` color: ${cssVar.colorTextQuaternary}; @@ -33,8 +31,9 @@ interface MoveFileItemProps { } const MoveFileItem = memo(({ oldPath, newPath }) => { - const displayOldPath = useElectronStore(desktopStateSelectors.displayRelativePath(oldPath)); - const displayNewPath = useElectronStore(desktopStateSelectors.displayRelativePath(newPath)); + const { displayRelativePath } = useToolRenderCapabilities(); + const displayOldPath = displayRelativePath ? displayRelativePath(oldPath) : oldPath; + const displayNewPath = displayRelativePath ? displayRelativePath(newPath) : newPath; return ( diff --git a/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/index.tsx b/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/index.tsx index 4987768d37..9062ef0305 100644 --- a/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/MoveLocalFiles/index.tsx @@ -1,4 +1,3 @@ -import type { MoveLocalFilesParams } from '@lobechat/electron-client-ipc'; import type { BuiltinRenderProps } from '@lobechat/types'; import { Flexbox, Text } from '@lobehub/ui'; import { memo } from 'react'; @@ -6,15 +5,27 @@ import { useTranslation } from 'react-i18next'; import MoveFileItem from './MoveFileItem'; -const MoveLocalFiles = memo>(({ args }) => { - const { items } = args; +interface MoveFilesArgs { + items?: Array<{ newPath: string; oldPath: string }>; + operations?: Array<{ destination: string; source: string }>; +} + +const MoveLocalFiles = memo>(({ args }) => { const { t } = useTranslation('tool'); + // Support both IPC format (items) and ComputerRuntime format (operations) + const moveItems = (args.items || args.operations || []).map((item: any) => ({ + newPath: item.newPath || item.destination || '', + oldPath: item.oldPath || item.source || '', + })); + return ( - {t('localFiles.moveFiles.itemsMoved', { count: items.length })} + + {t('localFiles.moveFiles.itemsMoved', { count: moveItems.length })} + - {items.map((item, index) => ( + {moveItems.map((item, index) => ( ))} diff --git a/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/ReadFileView.tsx b/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/ReadFileView.tsx index ac212e7b1e..37c5eb1ddc 100644 --- a/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/ReadFileView.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/ReadFileView.tsx @@ -1,4 +1,5 @@ -import type { LocalReadFileResult } from '@lobechat/electron-client-ipc'; +import { useToolRenderCapabilities } from '@lobechat/shared-tool-ui'; +import type { ReadFileState } from '@lobechat/tool-runtime'; import { ActionIcon, Flexbox, Icon, Markdown, Text } from '@lobehub/ui'; import { createStaticStyles } from 'antd-style'; import { AlignLeft, Asterisk, ExternalLink, FolderOpen } from 'lucide-react'; @@ -6,9 +7,6 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import FileIcon from '@/components/FileIcon'; -import { localFileService } from '@/services/electron/localFileService'; -import { useElectronStore } from '@/store/electron'; -import { desktopStateSelectors } from '@/store/electron/selectors'; const styles = createStaticStyles(({ css, cssVar }) => ({ actions: css` @@ -88,26 +86,36 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ `, })); -// Assuming the result object might include the original path and an optional warning -interface ReadFileViewProps extends LocalReadFileResult { - path: string; // The full path requested -} - -const ReadFileView = memo( - ({ filename, path, fileType, charCount, content, totalLineCount, totalCharCount, loc }) => { +const ReadFileView = memo( + ({ + filename: filenameProp, + path, + fileType, + charCount, + content, + totalLines, + totalCharCount, + loc, + }) => { const { t } = useTranslation('tool'); + const { openFile, openFolder, displayRelativePath } = useToolRenderCapabilities(); + const filename = filenameProp || path.split('/').pop() || path; - const handleOpenFile = (e: React.MouseEvent) => { - e.stopPropagation(); - localFileService.openLocalFile({ path }); - }; + const handleOpenFile = openFile + ? (e: React.MouseEvent) => { + e.stopPropagation(); + openFile(path); + } + : undefined; - const handleOpenFolder = (e: React.MouseEvent) => { - e.stopPropagation(); - localFileService.openLocalFolder({ isDirectory: false, path }); - }; + const handleOpenFolder = openFolder + ? (e: React.MouseEvent) => { + e.stopPropagation(); + openFolder(path); + } + : undefined; - const displayPath = useElectronStore(desktopStateSelectors.displayRelativePath(path)); + const displayPath = displayRelativePath ? displayRelativePath(path) : path; return ( @@ -125,41 +133,60 @@ const ReadFileView = memo( {filename} - {/* Actions on Hover */} - - - - + {(handleOpenFile || handleOpenFolder) && ( + + {handleOpenFile && ( + + )} + {handleOpenFolder && ( + + )} + + )} - - - - {charCount} / {totalCharCount} - - - - - - L{loc?.[0]}-{loc?.[1]} /{' '} - {totalLineCount} - - + {charCount !== undefined && ( + + + + {charCount} + {totalCharCount !== undefined && ( + <> + {' '} + / {totalCharCount} + + )} + + + )} + {loc && ( + + + + L{loc[0]}-{loc[1]} + {totalLines !== undefined && ( + <> + {' '} + / {totalLines} + + )} + + + )} - {/* Path */} {displayPath} diff --git a/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/index.tsx b/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/index.tsx index d3cbef5561..3c2284447d 100644 --- a/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/ReadLocalFile/index.tsx @@ -1,31 +1,24 @@ -import type { LocalReadFileState } from '@lobechat/builtin-tool-local-system'; -import type { LocalReadFileParams } from '@lobechat/electron-client-ipc'; -import type { ChatMessagePluginError } from '@lobechat/types'; +import { useToolRenderCapabilities } from '@lobechat/shared-tool-ui'; +import type { ReadFileState } from '@lobechat/tool-runtime'; +import type { BuiltinRenderProps } from '@lobechat/types'; import { memo } from 'react'; -import { useChatStore } from '@/store/chat'; -import { chatToolSelectors } from '@/store/chat/slices/builtinTool/selectors'; - import ReadFileSkeleton from './ReadFileSkeleton'; import ReadFileView from './ReadFileView'; -interface ReadFileQueryProps { - args: LocalReadFileParams; - messageId: string; - pluginError: ChatMessagePluginError; - pluginState: LocalReadFileState; -} +const ReadFileQuery = memo>( + ({ args, pluginState, messageId }) => { + const { isLoading } = useToolRenderCapabilities(); + const loading = isLoading?.(messageId); -const ReadFileQuery = memo(({ args, pluginState, messageId }) => { - const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId)); + if (loading) { + return ; + } - if (loading) { - return ; - } + if (!args?.path || !pluginState) return null; - if (!args?.path || !pluginState) return null; - - return ; -}); + return ; + }, +); export default ReadFileQuery; diff --git a/packages/builtin-tool-local-system/src/client/Render/RunCommand/index.tsx b/packages/builtin-tool-local-system/src/client/Render/RunCommand/index.tsx deleted file mode 100644 index 87e9d3201e..0000000000 --- a/packages/builtin-tool-local-system/src/client/Render/RunCommand/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { type RunCommandParams, type RunCommandResult } from '@lobechat/electron-client-ipc'; -import { type BuiltinRenderProps } from '@lobechat/types'; -import { Block, Flexbox, Highlighter } from '@lobehub/ui'; -import { createStaticStyles } from 'antd-style'; -import { memo } from 'react'; - -const styles = createStaticStyles(({ css, cssVar }) => ({ - container: css` - overflow: hidden; - padding-inline: 8px; - - & .ant-highlighter-highlighter-hover-actions { - inset-block-start: 4px; - inset-inline-end: 4px; - } - `, - head: css` - font-family: ${cssVar.fontFamilyCode}; - font-size: 12px; - `, - header: css` - .action-icon { - opacity: 0; - transition: opacity 0.2s ease; - } - - &:hover { - .action-icon { - opacity: 1; - } - } - `, - statusIcon: css` - font-size: 12px; - `, -})); - -interface RunCommandState { - message: string; - result: RunCommandResult; -} - -const RunCommand = memo>( - ({ args, pluginState }) => { - const { result } = pluginState || {}; - - return ( - - - - {args.command} - - {result?.output && ( - - {result.output} - - )} - - - ); - }, -); - -export default RunCommand; diff --git a/packages/builtin-tool-local-system/src/client/Render/SearchFiles/Result.tsx b/packages/builtin-tool-local-system/src/client/Render/SearchFiles/Result.tsx index df4351194a..c2805767c8 100644 --- a/packages/builtin-tool-local-system/src/client/Render/SearchFiles/Result.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/SearchFiles/Result.tsx @@ -1,21 +1,19 @@ -import type { FileResult } from '@lobechat/builtin-tool-local-system'; +import { useToolRenderCapabilities } from '@lobechat/shared-tool-ui'; import type { ChatMessagePluginError } from '@lobechat/types'; import { Flexbox, Skeleton } from '@lobehub/ui'; import { memo } from 'react'; -import { useChatStore } from '@/store/chat'; -import { chatToolSelectors } from '@/store/chat/selectors'; - import FileItem from '../../components/FileItem'; interface SearchFilesProps { messageId: string; pluginError: ChatMessagePluginError; - searchResults?: FileResult[]; + searchResults?: Array<{ isDirectory?: boolean; name?: string; path: string; size?: number }>; } const SearchFiles = memo(({ searchResults = [], messageId }) => { - const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId)); + const { isLoading } = useToolRenderCapabilities(); + const loading = isLoading?.(messageId); if (loading) { return ( diff --git a/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/index.tsx b/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/index.tsx index 7958f0f615..359636b294 100644 --- a/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/index.tsx @@ -1,25 +1,23 @@ -import type { LocalFileSearchState } from '@lobechat/builtin-tool-local-system'; -import type { LocalSearchFilesParams } from '@lobechat/electron-client-ipc'; +import { useToolRenderCapabilities } from '@lobechat/shared-tool-ui'; +import type { SearchFilesState } from '@lobechat/tool-runtime'; import { memo } from 'react'; -import { useChatStore } from '@/store/chat'; -import { chatToolSelectors } from '@/store/chat/selectors'; - import SearchView from './SearchView'; interface SearchQueryViewProps { - args: LocalSearchFilesParams; + args: any; messageId: string; - pluginState?: LocalFileSearchState; + pluginState?: SearchFilesState; } const SearchQueryView = memo(({ messageId, args, pluginState }) => { - const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId)); - const searchResults = pluginState?.searchResults || []; + const { isLoading } = useToolRenderCapabilities(); + const loading = isLoading?.(messageId); + const searchResults = pluginState?.results || []; return ( diff --git a/packages/builtin-tool-local-system/src/client/Render/SearchFiles/index.tsx b/packages/builtin-tool-local-system/src/client/Render/SearchFiles/index.tsx index a09539313e..181cb0d12a 100644 --- a/packages/builtin-tool-local-system/src/client/Render/SearchFiles/index.tsx +++ b/packages/builtin-tool-local-system/src/client/Render/SearchFiles/index.tsx @@ -1,5 +1,4 @@ -import type { LocalFileSearchState } from '@lobechat/builtin-tool-local-system'; -import type { LocalSearchFilesParams } from '@lobechat/electron-client-ipc'; +import type { SearchFilesState } from '@lobechat/tool-runtime'; import type { BuiltinRenderProps } from '@lobechat/types'; import { Flexbox } from '@lobehub/ui'; import { memo } from 'react'; @@ -7,7 +6,7 @@ import { memo } from 'react'; import SearchResult from './Result'; import SearchQuery from './SearchQuery'; -const SearchFiles = memo>( +const SearchFiles = memo>( ({ messageId, pluginError, args, pluginState }) => { return ( @@ -15,7 +14,7 @@ const SearchFiles = memo ); diff --git a/packages/builtin-tool-local-system/src/client/Render/index.ts b/packages/builtin-tool-local-system/src/client/Render/index.ts index 949e5f216e..15f2e3f0f3 100644 --- a/packages/builtin-tool-local-system/src/client/Render/index.ts +++ b/packages/builtin-tool-local-system/src/client/Render/index.ts @@ -1,9 +1,10 @@ +import { RunCommandRender } from '@lobechat/shared-tool-ui/renders'; + import { LocalSystemApiName } from '../..'; import EditLocalFile from './EditLocalFile'; import ListFiles from './ListFiles'; import MoveLocalFiles from './MoveLocalFiles'; import ReadLocalFile from './ReadLocalFile'; -import RunCommand from './RunCommand'; import SearchFiles from './SearchFiles'; import WriteFile from './WriteFile'; @@ -15,7 +16,7 @@ export const LocalSystemRenders = { [LocalSystemApiName.listLocalFiles]: ListFiles, [LocalSystemApiName.moveLocalFiles]: MoveLocalFiles, [LocalSystemApiName.readLocalFile]: ReadLocalFile, - [LocalSystemApiName.runCommand]: RunCommand, + [LocalSystemApiName.runCommand]: RunCommandRender, [LocalSystemApiName.searchLocalFiles]: SearchFiles, [LocalSystemApiName.writeLocalFile]: WriteFile, }; diff --git a/packages/builtin-tool-local-system/src/client/components/FileItem.tsx b/packages/builtin-tool-local-system/src/client/components/FileItem.tsx index c73877b431..ab36548a47 100644 --- a/packages/builtin-tool-local-system/src/client/components/FileItem.tsx +++ b/packages/builtin-tool-local-system/src/client/components/FileItem.tsx @@ -1,4 +1,4 @@ -import type { LocalFileItem } from '@lobechat/electron-client-ipc'; +import { useToolRenderCapabilities } from '@lobechat/shared-tool-ui'; import { ActionIcon, Flexbox } from '@lobehub/ui'; import { createStaticStyles } from 'antd-style'; import dayjs from 'dayjs'; @@ -7,7 +7,6 @@ import React, { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import FileIcon from '@/components/FileIcon'; -import { localFileService } from '@/services/electron/localFileService'; import { formatSize } from '@/utils/format'; const styles = createStaticStyles(({ css, cssVar }) => ({ @@ -47,13 +46,31 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ `, })); -interface FileItemProps extends LocalFileItem { +interface FileItemProps { + createdTime?: Date | string; + isDirectory?: boolean; + name?: string; + path?: string; showTime?: boolean; + size?: number; + type?: string; } + const FileItem = memo( - ({ isDirectory, name, path, size, type, showTime = false, createdTime }) => { + ({ isDirectory, name: nameProp, path, size, type, showTime = false, createdTime }) => { const { t } = useTranslation('tool'); const [isHovering, setIsHovering] = useState(false); + const { openFile, openFolder } = useToolRenderCapabilities(); + const name = nameProp || path?.split('/').pop() || ''; + + const handleClick = () => { + if (!path) return; + if (isDirectory) { + openFolder?.(path); + } else { + openFile?.(path); + } + }; return ( ( className={styles.container} gap={12} padding={'2px 8px'} - style={{ cursor: 'pointer', fontSize: 12, width: '100%' }} + style={{ + cursor: openFile || openFolder ? 'pointer' : 'default', + fontSize: 12, + width: '100%', + }} + onClick={handleClick} onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} - onClick={() => { - localFileService.openLocalFileOrFolder(path, isDirectory); - }} > ( style={{ overflow: 'hidden', width: '100%' }} >
{name}
- {showTime ? ( + {showTime && createdTime ? (
{dayjs(createdTime).format('MMM DD hh:mm')}
) : (
{path}
)}
- {isHovering ? ( + {isHovering && openFolder ? ( ( title={t('localFiles.openFolder')} onClick={(e) => { e.stopPropagation(); - localFileService.openLocalFolder({ isDirectory, path }); + if (path) openFolder(path); }} /> ) : ( - {formatSize(size)} + {size !== undefined ? formatSize(size) : ''} )}
); diff --git a/packages/builtin-tool-local-system/src/executor/index.ts b/packages/builtin-tool-local-system/src/executor/index.ts index 229cf9dc17..f06eada6cc 100644 --- a/packages/builtin-tool-local-system/src/executor/index.ts +++ b/packages/builtin-tool-local-system/src/executor/index.ts @@ -1,63 +1,24 @@ -/* eslint-disable import-x/consistent-type-specifier-style */ import type { EditLocalFileParams, - EditLocalFileResult, GetCommandOutputParams, - GetCommandOutputResult, GlobFilesParams, - GlobFilesResult, GrepContentParams, - GrepContentResult, KillCommandParams, - KillCommandResult, ListLocalFileParams, - LocalFileItem, - LocalMoveFilesResultItem, LocalReadFileParams, - LocalReadFileResult, LocalReadFilesParams, LocalSearchFilesParams, MoveLocalFilesParams, RenameLocalFileParams, - RenameLocalFileResult, RunCommandParams, - RunCommandResult, WriteLocalFileParams, } from '@lobechat/electron-client-ipc'; -import { - formatCommandOutput, - formatCommandResult, - formatEditResult, - formatFileContent, - formatFileList, - formatFileSearchResults, - formatGlobResults, - formatGrepResults, - formatKillResult, - formatMoveResults, - formatMultipleFiles, - formatRenameResult, - formatWriteResult, -} from '@lobechat/prompts'; -import { type BuiltinToolResult } from '@lobechat/types'; +import type { BuiltinToolResult } from '@lobechat/types'; import { BaseExecutor } from '@lobechat/types'; import { localFileService } from '@/services/electron/localFileService'; -import type { - EditLocalFileState, - GetCommandOutputState, - GlobFilesState, - GrepContentState, - KillCommandState, - LocalFileListState, - LocalFileSearchState, - LocalMoveFilesState, - LocalReadFilesState, - LocalReadFileState, - LocalRenameFileState, - RunCommandState, -} from '../types'; +import { LocalSystemExecutionRuntime } from '../ExecutionRuntime'; import { LocalSystemIdentifier } from '../types'; import { resolveArgsWithScope } from '../utils/path'; @@ -80,267 +41,131 @@ const LocalSystemApiEnum = { /** * Local System Tool Executor * - * Handles all local file system operations including file CRUD, shell commands, and search. + * Delegates standard computer operations to LocalSystemExecutionRuntime (extends ComputerRuntime). + * Handles scope resolution for paths before delegating. */ class LocalSystemExecutor extends BaseExecutor { readonly identifier = LocalSystemIdentifier; protected readonly apiEnum = LocalSystemApiEnum; + private runtime = new LocalSystemExecutionRuntime(localFileService); + + /** + * Convert BuiltinServerRuntimeOutput to BuiltinToolResult + */ + private toResult(output: { + content: string; + error?: any; + state?: any; + success: boolean; + }): BuiltinToolResult { + if (!output.success) { + return { + content: output.content, + error: output.error + ? { body: output.error, message: output.content, type: 'PluginServerError' } + : undefined, + success: false, + }; + } + return { content: output.content, state: output.state, success: true }; + } + // ==================== File Operations ==================== listLocalFiles = async (params: ListLocalFileParams): Promise => { try { - const result = await localFileService.listLocalFiles(params); - - const state: LocalFileListState = { - listResults: result.files, - totalCount: result.totalCount, - }; - - const content = formatFileList({ - directory: params.path, - files: result.files, + const result = await this.runtime.listFiles({ + directoryPath: params.path, sortBy: params.sortBy, sortOrder: params.sortOrder, - totalCount: result.totalCount, }); - - return { - content, - state, - success: true, - }; + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; readLocalFile = async (params: LocalReadFileParams): Promise => { try { - const result: LocalReadFileResult = await localFileService.readLocalFile(params); - - const state: LocalReadFileState = { fileContent: result }; - - const content = formatFileContent({ - content: result.content, - lineRange: params.loc, + const result = await this.runtime.readFile({ + endLine: params.loc?.[1], path: params.path, + startLine: params.loc?.[0], }); - - return { - content, - state, - success: true, - }; + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; readLocalFiles = async (params: LocalReadFilesParams): Promise => { try { - const results: LocalReadFileResult[] = await localFileService.readLocalFiles(params); - - const state: LocalReadFilesState = { filesContent: results }; - - const content = formatMultipleFiles(results); - - return { - content, - state, - success: true, - }; + const result = await this.runtime.readFiles(params); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; searchLocalFiles = async (params: LocalSearchFilesParams): Promise => { try { const resolvedParams = resolveArgsWithScope(params, 'directory'); - - const result: LocalFileItem[] = await localFileService.searchLocalFiles(resolvedParams); - - // Extract engine from first result (all results use same engine) - const engine = result[0]?.engine; - const state: LocalFileSearchState = { - engine, - resolvedPath: resolvedParams.directory, - searchResults: result, - }; - - const content = formatFileSearchResults(result); - - return { - content, - state, - success: true, - }; + const result = await this.runtime.searchFiles({ + directory: resolvedParams.directory || '', + }); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; moveLocalFiles = async (params: MoveLocalFilesParams): Promise => { try { - const results: LocalMoveFilesResultItem[] = await localFileService.moveLocalFiles(params); - - const successCount = results.filter((r) => r.success).length; - - const content = formatMoveResults(results); - - const state: LocalMoveFilesState = { - results, - successCount, - totalCount: results.length, - }; - - return { - content, - state, - success: true, - }; + const result = await this.runtime.moveFiles({ + operations: params.items.map((item) => ({ + destination: item.newPath, + source: item.oldPath, + })), + }); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; renameLocalFile = async (params: RenameLocalFileParams): Promise => { try { - const result: RenameLocalFileResult = await localFileService.renameLocalFile(params); - - if (!result.success) { - const state: LocalRenameFileState = { - error: result.error, - newPath: '', - oldPath: params.path, - success: false, - }; - - return { - content: formatRenameResult({ - error: result.error, - newName: params.newName, - oldPath: params.path, - success: false, - }), - state, - success: false, - }; - } - - const state: LocalRenameFileState = { - newPath: result.newPath!, + const result = await this.runtime.renameFile({ + newName: params.newName, oldPath: params.path, - success: true, - }; - - return { - content: formatRenameResult({ - newName: params.newName, - oldPath: params.path, - success: true, - }), - state, - success: true, - }; + }); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; writeLocalFile = async (params: WriteLocalFileParams): Promise => { try { - const result = await localFileService.writeFile(params); - - if (!result.success) { - return { - content: formatWriteResult({ - error: result.error, - path: params.path, - success: false, - }), - error: { message: result.error || 'Failed to write file', type: 'PluginServerError' }, - success: false, - }; - } - - return { - content: formatWriteResult({ - path: params.path, - success: true, - }), - success: true, - }; + const result = await this.runtime.writeFile(params); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; editLocalFile = async (params: EditLocalFileParams): Promise => { try { - const result: EditLocalFileResult = await localFileService.editLocalFile(params); - - if (!result.success) { - return { - content: `Edit failed: ${result.error}`, - success: false, - }; - } - - const content = formatEditResult({ - filePath: params.file_path, - linesAdded: result.linesAdded, - linesDeleted: result.linesDeleted, - replacements: result.replacements, + const result = await this.runtime.editFile({ + all: params.replace_all, + path: params.file_path, + replace: params.new_string, + search: params.old_string, }); - - const state: EditLocalFileState = { - diffText: result.diffText, - linesAdded: result.linesAdded, - linesDeleted: result.linesDeleted, - replacements: result.replacements, - }; - - return { - content, - state, - success: true, - }; + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; @@ -348,83 +173,32 @@ class LocalSystemExecutor extends BaseExecutor { runCommand = async (params: RunCommandParams): Promise => { try { - const result: RunCommandResult = await localFileService.runCommand(params); - - const content = formatCommandResult({ - error: result.error, - exitCode: result.exit_code, - shellId: result.shell_id, - stderr: result.stderr, - stdout: result.stdout, - success: result.success, - }); - - const state: RunCommandState = { message: content.split('\n\n')[0], result }; - - return { - content, - state, - success: result.success, - }; + const result = await this.runtime.runCommand(params); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; getCommandOutput = async (params: GetCommandOutputParams): Promise => { try { - const result: GetCommandOutputResult = await localFileService.getCommandOutput(params); - - const content = formatCommandOutput({ - error: result.error, - output: result.output, - running: result.running, - success: result.success, + const result = await this.runtime.getCommandOutput({ + commandId: params.shell_id, }); - - const state: GetCommandOutputState = { message: content.split('\n\n')[0], result }; - - return { - content, - state, - success: result.success, - }; + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; killCommand = async (params: KillCommandParams): Promise => { try { - const result: KillCommandResult = await localFileService.killCommand(params); - - const content = formatKillResult({ - error: result.error, - shellId: params.shell_id, - success: result.success, + const result = await this.runtime.killCommand({ + commandId: params.shell_id, }); - - const state: KillCommandState = { message: content, result }; - - return { - content, - state, - success: result.success, - }; + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; @@ -433,68 +207,37 @@ class LocalSystemExecutor extends BaseExecutor { grepContent = async (params: GrepContentParams): Promise => { try { const resolvedParams = resolveArgsWithScope(params, 'path'); - - const result: GrepContentResult = await localFileService.grepContent(resolvedParams); - - const content = result.success - ? formatGrepResults({ - matches: result.matches, - totalMatches: result.total_matches, - }) - : `Search failed: ${result.error || 'Unknown error'}`; - - const state: GrepContentState = { - message: content.split('\n')[0], - resolvedPath: resolvedParams.path, - result, - }; - - return { - content, - state, - success: result.success, - }; + const result = await this.runtime.grepContent({ + directory: resolvedParams.path || '', + pattern: resolvedParams.pattern, + }); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; globLocalFiles = async (params: GlobFilesParams): Promise => { try { const resolvedParams = resolveArgsWithScope(params, 'pattern'); - - const result: GlobFilesResult = await localFileService.globFiles(resolvedParams); - - const content = result.success - ? formatGlobResults({ - files: result.files, - totalFiles: result.total_files, - }) - : `Glob search failed: ${result.error || 'Unknown error'}`; - - const state: GlobFilesState = { - message: content.split('\n')[0], - resolvedPath: resolvedParams.pattern, - result, - }; - - return { - content, - state, - success: result.success, - }; + const result = await this.runtime.globFiles({ + pattern: resolvedParams.pattern, + }); + return this.toResult(result); } catch (error) { - return { - content: (error as Error).message, - error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, - success: false, - }; + return this.errorResult(error); } }; + + // ==================== Helpers ==================== + + private errorResult(error: unknown): BuiltinToolResult { + return { + content: (error as Error).message, + error: { body: error, message: (error as Error).message, type: 'PluginServerError' }, + success: false, + }; + } } // Export the executor instance for registration diff --git a/packages/builtin-tool-local-system/src/index.ts b/packages/builtin-tool-local-system/src/index.ts index ca7dd4e455..4e1a85f0b0 100644 --- a/packages/builtin-tool-local-system/src/index.ts +++ b/packages/builtin-tool-local-system/src/index.ts @@ -4,10 +4,8 @@ export { systemPrompt } from './systemRole'; export { type EditLocalFileState, type FileResult, - type GetCommandOutputState, type GlobFilesState, type GrepContentState, - type KillCommandState, type LocalFileListState, type LocalFileSearchState, type LocalMoveFilesState, diff --git a/packages/builtin-tool-local-system/src/types.ts b/packages/builtin-tool-local-system/src/types.ts index 1d697ae4d9..30b1e80246 100644 --- a/packages/builtin-tool-local-system/src/types.ts +++ b/packages/builtin-tool-local-system/src/types.ts @@ -1,14 +1,17 @@ -import { - type GetCommandOutputResult, - type GlobFilesResult, - type GrepContentResult, - type KillCommandResult, - type LocalFileItem, - type LocalMoveFilesResultItem, - type LocalReadFileResult, - type RunCommandResult, +import type { + LocalFileItem, + LocalMoveFilesResultItem, + LocalReadFileResult, } from '@lobechat/electron-client-ipc'; +// Re-export shared state types from @lobechat/tool-runtime +export type { + EditFileState as EditLocalFileState, + GlobFilesState, + GrepContentState, + RunCommandState, +} from '@lobechat/tool-runtime'; + export const LocalSystemIdentifier = 'lobe-local-system'; export const LocalSystemApiName = { @@ -41,6 +44,8 @@ export interface FileResult { type: string; } +// ==================== Local-System-Specific State Types ==================== + export interface LocalFileSearchState { /** Search engine used (e.g., 'mdfind', 'fd', 'find', 'fast-glob') */ engine?: string; @@ -75,39 +80,3 @@ export interface LocalRenameFileState { oldPath: string; success: boolean; } - -export interface RunCommandState { - message: string; - result: RunCommandResult; -} - -export interface GetCommandOutputState { - message: string; - result: GetCommandOutputResult; -} - -export interface KillCommandState { - message: string; - result: KillCommandResult; -} - -export interface GrepContentState { - message: string; - /** Resolved search path after scope resolution */ - resolvedPath?: string; - result: GrepContentResult; -} - -export interface GlobFilesState { - message: string; - /** Resolved full glob (path + pattern) after scope resolution. May contain glob metacharacters like `*` or `**`. */ - resolvedPath?: string; - result: GlobFilesResult; -} - -export interface EditLocalFileState { - diffText?: string; - linesAdded?: number; - linesDeleted?: number; - replacements: number; -} diff --git a/packages/builtin-tool-skills/package.json b/packages/builtin-tool-skills/package.json index ad0243f2c8..e98e48c7e9 100644 --- a/packages/builtin-tool-skills/package.json +++ b/packages/builtin-tool-skills/package.json @@ -10,10 +10,11 @@ }, "main": "./src/index.ts", "dependencies": { - "@lobechat/const": "workspace:*" + "@lobechat/const": "workspace:*", + "@lobechat/prompts": "workspace:*", + "@lobechat/shared-tool-ui": "workspace:*" }, "devDependencies": { - "@lobechat/prompts": "workspace:*", "@lobechat/types": "workspace:*" }, "peerDependencies": { diff --git a/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts b/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts index eeb1b1a323..21084ab51d 100644 --- a/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts +++ b/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts @@ -1,20 +1,20 @@ -import { resourcesTreePrompt } from '@lobechat/prompts'; -import { - type BuiltinServerRuntimeOutput, - type BuiltinSkill, - type SkillItem, - type SkillListItem, - type SkillResourceContent, +import { formatCommandResult, resourcesTreePrompt } from '@lobechat/prompts'; +import type { + BuiltinServerRuntimeOutput, + BuiltinSkill, + SkillItem, + SkillListItem, + SkillResourceContent, } from '@lobechat/types'; -import { - type ActivateSkillParams, - type CommandResult, - type ExecScriptParams, - type ExportFileParams, - type ReadReferenceParams, - type RunCommandOptions, - type RunCommandParams, +import type { + ActivateSkillParams, + CommandResult, + ExecScriptParams, + ExportFileParams, + ReadReferenceParams, + RunCommandOptions, + RunCommandParams, } from '../types'; /** @@ -77,17 +77,7 @@ export class SkillsExecutionRuntime { description, }); - const output = [result.output, result.stderr].filter(Boolean).join('\n'); - - return { - content: output || '(no output)', - state: { - command, - exitCode: result.exitCode, - success: result.success, - }, - success: result.success, - }; + return this.formatCommandOutput(command, result); } catch (e) { return { content: `Failed to execute command: ${(e as Error).message}`, @@ -106,18 +96,7 @@ export class SkillsExecutionRuntime { try { const result = await this.service.runCommand({ command }); - - const output = [result.output, result.stderr].filter(Boolean).join('\n'); - - return { - content: output || '(no output)', - state: { - command, - exitCode: result.exitCode, - success: result.success, - }, - success: result.success, - }; + return this.formatCommandOutput(command, result); } catch (e) { return { content: `Failed to execute command: ${(e as Error).message}`, @@ -138,17 +117,7 @@ export class SkillsExecutionRuntime { try { const result = await this.service.runCommand({ command }); - const output = [result.output, result.stderr].filter(Boolean).join('\n'); - - return { - content: output || '(no output)', - state: { - command, - exitCode: result.exitCode, - success: result.success, - }, - success: result.success, - }; + return this.formatCommandOutput(command, result); } catch (e) { return { content: `Failed to execute command: ${(e as Error).message}`, @@ -320,4 +289,27 @@ export class SkillsExecutionRuntime { success: true, }; } + + /** + * Format command result using the shared formatCommandResult from @lobechat/prompts. + * This ensures consistent content format across all runtimes. + */ + private formatCommandOutput(command: string, result: CommandResult): BuiltinServerRuntimeOutput { + const content = formatCommandResult({ + stderr: result.stderr, + stdout: result.output, + success: result.success, + exitCode: result.exitCode, + }); + + return { + content, + state: { + command, + exitCode: result.exitCode, + success: result.success, + }, + success: result.success, + }; + } } diff --git a/packages/builtin-tool-skills/src/client/Inspector/RunCommand/index.tsx b/packages/builtin-tool-skills/src/client/Inspector/RunCommand/index.tsx index 3767ce1d07..465a460972 100644 --- a/packages/builtin-tool-skills/src/client/Inspector/RunCommand/index.tsx +++ b/packages/builtin-tool-skills/src/client/Inspector/RunCommand/index.tsx @@ -1,60 +1,7 @@ 'use client'; -import { type BuiltinInspectorProps } from '@lobechat/types'; -import { createStaticStyles, cssVar, cx } from 'antd-style'; -import { Check, X } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createRunCommandInspector } from '@lobechat/shared-tool-ui/inspectors'; -import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles'; - -import type { CommandResult, RunCommandParams } from '../../../types'; - -const styles = createStaticStyles(({ css }) => ({ - statusIcon: css` - margin-block-end: -2px; - margin-inline-start: 4px; - `, -})); - -export const RunCommandInspector = memo>( - ({ args, partialArgs, isArgumentsStreaming, isLoading, pluginState }) => { - const { t } = useTranslation('plugin'); - - const description = args?.description || partialArgs?.description; - - if (isArgumentsStreaming) { - if (!description) - return ( -
- {t('builtins.lobe-skills.apiName.runCommand')} -
- ); - - return ( -
- {t('builtins.lobe-skills.apiName.runCommand')}: - {description} -
- ); - } - - return ( -
- - {t('builtins.lobe-skills.apiName.runCommand')}: - {description && {description}} - {isLoading ? null : pluginState?.success !== undefined ? ( - pluginState.success ? ( - - ) : ( - - ) - ) : null} - -
- ); - }, +export const RunCommandInspector = createRunCommandInspector( + 'builtins.lobe-skills.apiName.runCommand', ); - -RunCommandInspector.displayName = 'RunCommandInspector'; diff --git a/packages/builtin-tool-skills/src/client/Render/RunCommand/index.tsx b/packages/builtin-tool-skills/src/client/Render/RunCommand/index.tsx deleted file mode 100644 index 3dc3c5d9e5..0000000000 --- a/packages/builtin-tool-skills/src/client/Render/RunCommand/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import type { BuiltinRenderProps } from '@lobechat/types'; -import { Block, Flexbox, Highlighter } from '@lobehub/ui'; -import { createStaticStyles } from 'antd-style'; -import { memo } from 'react'; - -import type { CommandResult, RunCommandParams } from '../../../types'; - -const styles = createStaticStyles(({ css }) => ({ - container: css` - overflow: hidden; - padding-inline: 8px 0; - `, -})); - -const RunCommand = memo>( - ({ args, content, pluginState }) => { - return ( - - - - {args?.command || ''} - - {(pluginState?.output || content) && ( - - {pluginState?.output || content} - - )} - {pluginState?.stderr && ( - - {pluginState.stderr} - - )} - - - ); - }, -); - -RunCommand.displayName = 'RunCommand'; - -export default RunCommand; diff --git a/packages/builtin-tool-skills/src/client/Render/index.ts b/packages/builtin-tool-skills/src/client/Render/index.ts index de0489fb5b..5faab88c6e 100644 --- a/packages/builtin-tool-skills/src/client/Render/index.ts +++ b/packages/builtin-tool-skills/src/client/Render/index.ts @@ -1,12 +1,13 @@ +import { RunCommandRender } from '@lobechat/shared-tool-ui/renders'; + import { SkillsApiName } from '../../types'; import ExecScript from './ExecScript'; import ReadReference from './ReadReference'; -import RunCommand from './RunCommand'; import RunSkill from './RunSkill'; export const SkillsRenders = { [SkillsApiName.execScript]: ExecScript, [SkillsApiName.readReference]: ReadReference, - [SkillsApiName.runCommand]: RunCommand, + [SkillsApiName.runCommand]: RunCommandRender, [SkillsApiName.activateSkill]: RunSkill, }; diff --git a/packages/prompts/src/prompts/fileSystem/formatGrepResults.ts b/packages/prompts/src/prompts/fileSystem/formatGrepResults.ts index ad2e142b67..ac49935817 100644 --- a/packages/prompts/src/prompts/fileSystem/formatGrepResults.ts +++ b/packages/prompts/src/prompts/fileSystem/formatGrepResults.ts @@ -1,5 +1,5 @@ export interface FormatGrepResultsParams { - matches: string[]; + matches: Array; maxDisplay?: number; totalMatches: number; } @@ -16,7 +16,19 @@ export const formatGrepResults = ({ } const displayMatches = matches.slice(0, maxDisplay); - const matchList = displayMatches.map((m) => ` ${m}`).join('\n'); + const matchList = displayMatches + .map((m) => { + if (typeof m === 'string') return ` ${m}`; + const parts: string[] = []; + if (m.path) parts.push(m.path); + if (m.lineNumber !== undefined) parts.push(`:${m.lineNumber}`); + if (m.content) { + if (parts.length > 0) parts.push(`: ${m.content}`); + else parts.push(m.content); + } + return ` ${parts.join('')}`; + }) + .join('\n'); const moreInfo = matches.length > maxDisplay ? `\n ... and ${matches.length - maxDisplay} more` : ''; diff --git a/packages/shared-tool-ui/package.json b/packages/shared-tool-ui/package.json new file mode 100644 index 0000000000..9fbb26fcc7 --- /dev/null +++ b/packages/shared-tool-ui/package.json @@ -0,0 +1,24 @@ +{ + "name": "@lobechat/shared-tool-ui", + "version": "1.0.0", + "private": true, + "exports": { + ".": "./src/index.ts", + "./renders": "./src/Render/index.ts", + "./inspectors": "./src/Inspector/index.ts" + }, + "dependencies": { + "@lobechat/tool-runtime": "workspace:*" + }, + "devDependencies": { + "@lobechat/types": "workspace:*" + }, + "peerDependencies": { + "@lobehub/ui": "^5", + "antd-style": "*", + "lucide-react": "*", + "path-browserify-esm": "*", + "react": "*", + "react-i18next": "*" + } +} diff --git a/packages/shared-tool-ui/src/Inspector/EditLocalFile/index.tsx b/packages/shared-tool-ui/src/Inspector/EditLocalFile/index.tsx new file mode 100644 index 0000000000..9d54bb508f --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/EditLocalFile/index.tsx @@ -0,0 +1,109 @@ +'use client'; + +import type { EditFileState } from '@lobechat/tool-runtime'; +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { Icon, Text } from '@lobehub/ui'; +import { createStaticStyles, cssVar, cx } from 'antd-style'; +import { Minus, Plus } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FilePathDisplay } from '../../components/FilePathDisplay'; +import { inspectorTextStyles, shinyTextStyles } from '../../styles'; + +const styles = createStaticStyles(({ css, cssVar }) => ({ + separator: css` + margin-inline: 2px; + color: ${cssVar.colorTextQuaternary}; + `, +})); + +interface EditFileArgs { + all?: boolean; + file_path?: string; + path?: string; + replace?: string; + search?: string; +} + +export interface EditLocalFileInspectorProps extends BuiltinInspectorProps< + EditFileArgs, + EditFileState +> { + translationKey: string; +} + +export const EditLocalFileInspector = memo( + ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading, translationKey }) => { + const { t } = useTranslation('plugin'); + + const filePath = + args?.file_path || args?.path || partialArgs?.file_path || partialArgs?.path || ''; + + if (isArgumentsStreaming) { + if (!filePath) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + +
+ ); + } + + const linesAdded = pluginState?.linesAdded ?? 0; + const linesDeleted = pluginState?.linesDeleted ?? 0; + + const statsParts: ReactNode[] = []; + if (linesAdded > 0) { + statsParts.push( + + + {linesAdded} + , + ); + } + if (linesDeleted > 0) { + statsParts.push( + + + {linesDeleted} + , + ); + } + + return ( +
+ {t(translationKey as any)}: + + {!isLoading && statsParts.length > 0 && ( + <> + {' '} + {statsParts.map((part, index) => ( + + {index > 0 && / } + {part} + + ))} + + )} +
+ ); + }, +); + +EditLocalFileInspector.displayName = 'EditLocalFileInspector'; + +export const createEditLocalFileInspector = (translationKey: string) => { + const Inspector = memo>((props) => ( + + )); + Inspector.displayName = 'EditLocalFileInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/GlobLocalFiles/index.tsx b/packages/shared-tool-ui/src/Inspector/GlobLocalFiles/index.tsx new file mode 100644 index 0000000000..da390500da --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/GlobLocalFiles/index.tsx @@ -0,0 +1,68 @@ +'use client'; + +import type { GlobFilesState } from '@lobechat/tool-runtime'; +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { createStaticStyles, cssVar, cx } from 'antd-style'; +import { Check, X } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '../../styles'; + +const styles = createStaticStyles(({ css }) => ({ + statusIcon: css` + margin-block-end: -2px; + margin-inline-start: 4px; + `, +})); + +interface GlobFilesArgs { + directory?: string; + pattern?: string; +} + +export const createGlobLocalFilesInspector = (translationKey: string) => { + const Inspector = memo>( + ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { + const { t } = useTranslation('plugin'); + + const pattern = args?.pattern || partialArgs?.pattern || ''; + + if (isArgumentsStreaming) { + if (!pattern) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + {pattern} +
+ ); + } + + const hasFiles = (pluginState?.totalCount ?? 0) > 0; + + return ( +
+ + {t(translationKey as any)}: + {pattern && {pattern}} + {isLoading ? null : pluginState ? ( + hasFiles ? ( + + ) : ( + + ) + ) : null} + +
+ ); + }, + ); + Inspector.displayName = 'GlobLocalFilesInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/GrepContent/index.tsx b/packages/shared-tool-ui/src/Inspector/GrepContent/index.tsx new file mode 100644 index 0000000000..b188e40bf1 --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/GrepContent/index.tsx @@ -0,0 +1,76 @@ +'use client'; + +import type { GrepContentState } from '@lobechat/tool-runtime'; +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { Text } from '@lobehub/ui'; +import { cssVar, cx } from 'antd-style'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '../../styles'; + +interface GrepContentArgs { + directory?: string; + path?: string; + pattern?: string; +} + +interface CreateGrepContentInspectorOptions { + noResultsKey: string; + translationKey: string; +} + +export const createGrepContentInspector = ({ + translationKey, + noResultsKey, +}: CreateGrepContentInspectorOptions) => { + const Inspector = memo>( + ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { + const { t } = useTranslation('plugin'); + + const pattern = args?.pattern || partialArgs?.pattern || ''; + + if (isArgumentsStreaming) { + if (!pattern) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + {pattern} +
+ ); + } + + const resultCount = pluginState?.totalMatches ?? 0; + const hasResults = resultCount > 0; + + return ( +
+ {t(translationKey as any)}: + {pattern && {pattern}} + {!isLoading && + pluginState && + (hasResults ? ( + ({resultCount}) + ) : ( + + ({t(noResultsKey as any)}) + + ))} +
+ ); + }, + ); + Inspector.displayName = 'GrepContentInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/ListLocalFiles/index.tsx b/packages/shared-tool-ui/src/Inspector/ListLocalFiles/index.tsx new file mode 100644 index 0000000000..eaf766fa18 --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/ListLocalFiles/index.tsx @@ -0,0 +1,53 @@ +'use client'; + +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { cx } from 'antd-style'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FilePathDisplay } from '../../components/FilePathDisplay'; +import { inspectorTextStyles, shinyTextStyles } from '../../styles'; + +interface ListFilesArgs { + directoryPath?: string; + path?: string; +} + +export const createListLocalFilesInspector = (translationKey: string) => { + const Inspector = memo>( + ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { + const { t } = useTranslation('plugin'); + + const dirPath = args?.path || args?.directoryPath || partialArgs?.path || ''; + const resultCount = pluginState?.totalCount ?? pluginState?.files?.length ?? 0; + + if (isArgumentsStreaming) { + if (!dirPath) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + +
+ ); + } + + return ( +
+ {t(translationKey as any)}: + + {!isLoading && resultCount > 0 && ( + ({resultCount}) + )} +
+ ); + }, + ); + Inspector.displayName = 'ListLocalFilesInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/ReadLocalFile/index.tsx b/packages/shared-tool-ui/src/Inspector/ReadLocalFile/index.tsx new file mode 100644 index 0000000000..b72e90bedd --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/ReadLocalFile/index.tsx @@ -0,0 +1,61 @@ +'use client'; + +import type { ReadFileState } from '@lobechat/tool-runtime'; +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { cx } from 'antd-style'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FilePathDisplay } from '../../components/FilePathDisplay'; +import { inspectorTextStyles, shinyTextStyles } from '../../styles'; + +interface ReadFileArgs { + endLine?: number; + loc?: [number, number]; + path?: string; + startLine?: number; +} + +export const createReadLocalFileInspector = (translationKey: string) => { + const Inspector = memo>( + ({ args, partialArgs, isArgumentsStreaming, isLoading }) => { + const { t } = useTranslation('plugin'); + + const filePath = args?.path || partialArgs?.path || ''; + + const lineRange = useMemo(() => { + const start = args?.startLine ?? args?.loc?.[0]; + const end = args?.endLine ?? args?.loc?.[1]; + if (start !== undefined && end !== undefined) return `L${start}-L${end}`; + if (start !== undefined) return `L${start}`; + return undefined; + }, [args]); + + if (isArgumentsStreaming) { + if (!filePath) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + +
+ ); + } + + return ( +
+ {t(translationKey as any)}: + + {lineRange && ({lineRange})} +
+ ); + }, + ); + Inspector.displayName = 'ReadLocalFileInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx b/packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx new file mode 100644 index 0000000000..cfc762cff1 --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx @@ -0,0 +1,88 @@ +'use client'; + +import type { RunCommandState } from '@lobechat/tool-runtime'; +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { createStaticStyles, cssVar, cx } from 'antd-style'; +import { Check, X } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '../../styles'; + +const styles = createStaticStyles(({ css }) => ({ + statusIcon: css` + margin-block-end: -2px; + margin-inline-start: 4px; + `, +})); + +interface RunCommandArgs { + background?: boolean; + command: string; + description?: string; + timeout?: number; +} + +export interface RunCommandInspectorProps extends BuiltinInspectorProps< + RunCommandArgs, + RunCommandState +> { + /** i18n key for the API name label, e.g. 'builtins.lobe-local-system.apiName.runCommand' */ + translationKey: string; +} + +export const RunCommandInspector = memo( + ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading, translationKey }) => { + const { t } = useTranslation('plugin'); + + const description = args?.description || partialArgs?.description || args?.command || ''; + + if (isArgumentsStreaming) { + if (!description) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + {description} +
+ ); + } + + const isSuccess = pluginState?.success || pluginState?.exitCode === 0; + + return ( +
+ + {t(translationKey as any)}: + {description && {description}} + {isLoading ? null : pluginState?.success !== undefined ? ( + isSuccess ? ( + + ) : ( + + ) + ) : null} + +
+ ); + }, +); + +RunCommandInspector.displayName = 'RunCommandInspector'; + +/** + * Factory to create a RunCommandInspector with a bound translation key. + * Use this in each package's inspector registry to avoid wrapper components. + */ +export const createRunCommandInspector = (translationKey: string) => { + const Inspector = memo>((props) => ( + + )); + Inspector.displayName = 'RunCommandInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/SearchLocalFiles/index.tsx b/packages/shared-tool-ui/src/Inspector/SearchLocalFiles/index.tsx new file mode 100644 index 0000000000..52dce2ed55 --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/SearchLocalFiles/index.tsx @@ -0,0 +1,86 @@ +'use client'; + +import type { SearchFilesState } from '@lobechat/tool-runtime'; +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { Text } from '@lobehub/ui'; +import { cssVar, cx } from 'antd-style'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '../../styles'; + +interface SearchFilesArgs { + keyword?: string; + keywords?: string; + query?: string; +} + +interface CreateSearchLocalFilesInspectorOptions { + noResultsKey: string; + translationKey: string; +} + +export const createSearchLocalFilesInspector = ({ + translationKey, + noResultsKey, +}: CreateSearchLocalFilesInspectorOptions) => { + const Inspector = memo>( + ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => { + const { t } = useTranslation('plugin'); + + // Support all keyword field variants + const query = + args?.keyword || + args?.keywords || + args?.query || + partialArgs?.keyword || + partialArgs?.keywords || + partialArgs?.query || + ''; + + if (isArgumentsStreaming) { + if (!query) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + {query} +
+ ); + } + + const resultCount = pluginState?.results?.length ?? pluginState?.totalCount ?? 0; + const hasResults = resultCount > 0; + + return ( +
+ + {t(translationKey as any)}: + {query && {query}} + {!isLoading && + pluginState && + (hasResults ? ( + ({resultCount}) + ) : ( + + ({t(noResultsKey as any)}) + + ))} + +
+ ); + }, + ); + Inspector.displayName = 'SearchLocalFilesInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/WriteLocalFile/index.tsx b/packages/shared-tool-ui/src/Inspector/WriteLocalFile/index.tsx new file mode 100644 index 0000000000..7555e7be44 --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/WriteLocalFile/index.tsx @@ -0,0 +1,61 @@ +'use client'; + +import type { BuiltinInspectorProps } from '@lobechat/types'; +import { Icon, Text } from '@lobehub/ui'; +import { cssVar, cx } from 'antd-style'; +import { Plus } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FilePathDisplay } from '../../components/FilePathDisplay'; +import { inspectorTextStyles, shinyTextStyles } from '../../styles'; + +interface WriteFileArgs { + content?: string; + path?: string; +} + +export const createWriteLocalFileInspector = (translationKey: string) => { + const Inspector = memo>( + ({ args, partialArgs, isArgumentsStreaming, isLoading }) => { + const { t } = useTranslation('plugin'); + + const filePath = args?.path || partialArgs?.path || ''; + const lineCount = args?.content?.split('\n').length; + + if (isArgumentsStreaming) { + if (!filePath) + return ( +
+ {t(translationKey as any)} +
+ ); + + return ( +
+ {t(translationKey as any)}: + +
+ ); + } + + return ( +
+ {t(translationKey as any)}: + + {!isLoading && lineCount && ( + <> + {' '} + + + {lineCount} + + + )} +
+ ); + }, + ); + Inspector.displayName = 'WriteLocalFileInspector'; + return Inspector; +}; diff --git a/packages/shared-tool-ui/src/Inspector/index.ts b/packages/shared-tool-ui/src/Inspector/index.ts new file mode 100644 index 0000000000..e7dab2ede6 --- /dev/null +++ b/packages/shared-tool-ui/src/Inspector/index.ts @@ -0,0 +1,8 @@ +export { createEditLocalFileInspector } from './EditLocalFile'; +export { createGlobLocalFilesInspector } from './GlobLocalFiles'; +export { createGrepContentInspector } from './GrepContent'; +export { createListLocalFilesInspector } from './ListLocalFiles'; +export { createReadLocalFileInspector } from './ReadLocalFile'; +export { createRunCommandInspector, RunCommandInspector } from './RunCommand'; +export { createSearchLocalFilesInspector } from './SearchLocalFiles'; +export { createWriteLocalFileInspector } from './WriteLocalFile'; diff --git a/packages/builtin-tool-cloud-sandbox/src/client/Render/RunCommand/index.tsx b/packages/shared-tool-ui/src/Render/RunCommand/index.tsx similarity index 79% rename from packages/builtin-tool-cloud-sandbox/src/client/Render/RunCommand/index.tsx rename to packages/shared-tool-ui/src/Render/RunCommand/index.tsx index 1339c73dda..b95768551a 100644 --- a/packages/builtin-tool-cloud-sandbox/src/client/Render/RunCommand/index.tsx +++ b/packages/shared-tool-ui/src/Render/RunCommand/index.tsx @@ -1,12 +1,11 @@ 'use client'; +import type { RunCommandState } from '@lobechat/tool-runtime'; import type { BuiltinRenderProps } from '@lobechat/types'; import { Block, Flexbox, Highlighter } from '@lobehub/ui'; import { createStaticStyles } from 'antd-style'; import { memo } from 'react'; -import type { RunCommandState } from '../../../types'; - const styles = createStaticStyles(({ css }) => ({ container: css` overflow: hidden; @@ -14,15 +13,17 @@ const styles = createStaticStyles(({ css }) => ({ `, })); -interface RunCommandParams { +interface RunCommandArgs { background?: boolean; command: string; description?: string; timeout?: number; } -const RunCommand = memo>( - ({ args, pluginState }) => { +const RunCommand = memo>( + ({ args, content, pluginState }) => { + const output = pluginState?.output || pluginState?.stdout || content; + return ( @@ -33,9 +34,9 @@ const RunCommand = memo>( style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }} variant={'borderless'} > - {args.command} + {args?.command || ''} - {pluginState?.output && ( + {output && ( >( style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }} variant={'filled'} > - {pluginState.output} + {output} )} {pluginState?.stderr && ( diff --git a/packages/shared-tool-ui/src/Render/index.ts b/packages/shared-tool-ui/src/Render/index.ts new file mode 100644 index 0000000000..5ff72a984b --- /dev/null +++ b/packages/shared-tool-ui/src/Render/index.ts @@ -0,0 +1 @@ +export { default as RunCommandRender } from './RunCommand'; diff --git a/packages/shared-tool-ui/src/components/FilePathDisplay.tsx b/packages/shared-tool-ui/src/components/FilePathDisplay.tsx new file mode 100644 index 0000000000..ab50c3dc5f --- /dev/null +++ b/packages/shared-tool-ui/src/components/FilePathDisplay.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { MaterialFileTypeIcon, Text } from '@lobehub/ui'; +import { createStaticStyles, cssVar } from 'antd-style'; +import path from 'path-browserify-esm'; +import { memo, useMemo } from 'react'; + +const styles = createStaticStyles(({ css }) => ({ + icon: css` + flex-shrink: 0; + margin-inline-end: 4px; + `, + text: css` + overflow: hidden; + color: ${cssVar.colorText}; + text-overflow: ellipsis; + white-space: nowrap; + `, +})); + +interface FilePathDisplayProps { + filePath: string; + isDirectory?: boolean; +} + +export const FilePathDisplay = memo(({ filePath, isDirectory }) => { + const { displayPath, name } = useMemo(() => { + if (!filePath) return { displayPath: '', name: '' }; + const { base, dir } = path.parse(filePath); + const parentDir = path.basename(dir); + return { + displayPath: parentDir ? `${parentDir}/${base}` : base, + name: base, + }; + }, [filePath]); + + if (!filePath) return null; + + return ( + <> + {name && ( + + )} + {displayPath && ( + + {displayPath} + + )} + + ); +}); + +FilePathDisplay.displayName = 'FilePathDisplay'; diff --git a/packages/shared-tool-ui/src/context.tsx b/packages/shared-tool-ui/src/context.tsx new file mode 100644 index 0000000000..005ca07fd4 --- /dev/null +++ b/packages/shared-tool-ui/src/context.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { createContext, use } from 'react'; + +/** + * Capabilities that can be injected into shared tool render components. + * + * - local-system: provides all (Electron IPC + store) + * - cloud-sandbox: provides none (renders without loading state, open file actions) + */ +export interface ToolRenderCapabilities { + /** Display a path relative to working directory. Returns the path as-is if not provided. */ + displayRelativePath?: (path: string) => string; + /** Whether a tool call is currently loading for a given messageId */ + isLoading?: (messageId: string) => boolean; + /** Open a file in the OS file manager */ + openFile?: (path: string) => void; + /** Open the containing folder of a file in the OS file manager */ + openFolder?: (path: string) => void; +} + +const ToolRenderContext = createContext({}); + +export const ToolRenderProvider = ToolRenderContext.Provider; + +export const useToolRenderCapabilities = () => use(ToolRenderContext); diff --git a/packages/shared-tool-ui/src/index.ts b/packages/shared-tool-ui/src/index.ts new file mode 100644 index 0000000000..0ca7a4a3c5 --- /dev/null +++ b/packages/shared-tool-ui/src/index.ts @@ -0,0 +1,2 @@ +export type { ToolRenderCapabilities } from './context'; +export { ToolRenderProvider, useToolRenderCapabilities } from './context'; diff --git a/packages/shared-tool-ui/src/styles.ts b/packages/shared-tool-ui/src/styles.ts new file mode 100644 index 0000000000..7e34419ac4 --- /dev/null +++ b/packages/shared-tool-ui/src/styles.ts @@ -0,0 +1,73 @@ +import { createStaticStyles, keyframes } from 'antd-style'; + +/** + * Inspector text style — ellipsis + secondary color + flex align + */ +export const inspectorTextStyles = createStaticStyles(({ css, cssVar }) => ({ + root: css` + overflow: hidden; + display: flex; + align-items: center; + + min-width: 0; + + color: ${cssVar.colorTextSecondary}; + text-overflow: ellipsis; + white-space: nowrap; + `, +})); + +/** + * Highlight underline effect using gradient background + */ +export const highlightTextStyles = createStaticStyles(({ css, cssVar }) => { + const highlightBase = (highlightColor: string) => css` + overflow: hidden; + + min-width: 0; + margin-inline-start: 4px; + padding-block-end: 1px; + + color: ${cssVar.colorText}; + text-overflow: ellipsis; + + background: linear-gradient(to top, ${highlightColor} 40%, transparent 40%); + `; + + return { + gold: highlightBase(cssVar.gold4), + info: highlightBase(cssVar.colorInfoBg), + primary: highlightBase(cssVar.colorPrimaryBgHover), + warning: highlightBase(cssVar.colorWarningBg), + }; +}); + +const shine = keyframes` + 0% { + background-position: 100%; + } + + 100% { + background-position: -100%; + } +`; + +/** + * Shiny loading text animation + */ +export const shinyTextStyles = createStaticStyles(({ css, cssVar }) => ({ + shinyText: css` + color: color-mix(in srgb, ${cssVar.colorText} 45%, transparent); + + background: linear-gradient( + 120deg, + color-mix(in srgb, ${cssVar.colorTextBase} 0%, transparent) 40%, + ${cssVar.colorTextSecondary} 50%, + color-mix(in srgb, ${cssVar.colorTextBase} 0%, transparent) 60% + ); + background-clip: text; + background-size: 200% 100%; + + animation: ${shine} 1.5s linear infinite; + `, +})); diff --git a/packages/tool-runtime/package.json b/packages/tool-runtime/package.json new file mode 100644 index 0000000000..49ac700857 --- /dev/null +++ b/packages/tool-runtime/package.json @@ -0,0 +1,15 @@ +{ + "name": "@lobechat/tool-runtime", + "version": "1.0.0", + "private": true, + "exports": { + ".": "./src/index.ts" + }, + "main": "./src/index.ts", + "dependencies": { + "@lobechat/prompts": "workspace:*" + }, + "devDependencies": { + "@lobechat/types": "workspace:*" + } +} diff --git a/packages/tool-runtime/src/ComputerRuntime.ts b/packages/tool-runtime/src/ComputerRuntime.ts new file mode 100644 index 0000000000..45f01a1184 --- /dev/null +++ b/packages/tool-runtime/src/ComputerRuntime.ts @@ -0,0 +1,473 @@ +import { + formatCommandOutput, + formatCommandResult, + formatEditResult, + formatFileContent, + formatFileList, + formatFileSearchResults, + formatGlobResults, + formatGrepResults, + formatKillResult, + formatMoveResults, + formatRenameResult, + formatWriteResult, +} from '@lobechat/prompts'; +import type { BuiltinServerRuntimeOutput } from '@lobechat/types'; + +import type { + EditFileParams, + EditFileState, + GetCommandOutputParams, + GetCommandOutputState, + GlobFilesParams, + GlobFilesState, + GrepContentParams, + GrepContentState, + KillCommandParams, + KillCommandState, + ListFilesParams, + ListFilesState, + MoveFilesParams, + MoveFilesState, + ReadFileParams, + ReadFileState, + RenameFileParams, + RenameFileState, + RunCommandParams, + RunCommandState, + SearchFilesParams, + SearchFilesState, + ServiceResult, + WriteFileParams, + WriteFileState, +} from './types'; + +/** + * ComputerRuntime — abstract base for computer operations (file system, shell, search). + * + * Subclasses implement `callService` to delegate to their specific backend + * (Electron IPC, cloud sandbox API, etc.). The base class handles: + * - Normalizing raw results into formatted content via `@lobechat/prompts` + * - Building consistent state objects for UI rendering + */ +export abstract class ComputerRuntime { + /** + * Call the underlying service to execute a tool. + * Each subclass maps this to its own transport (IPC, HTTP, tRPC, etc.). + */ + protected abstract callService( + toolName: string, + params: Record, + ): Promise; + + // ==================== File Operations ==================== + + async listFiles(args: ListFilesParams): Promise { + try { + const result = await this.callService('listLocalFiles', args); + + if (!result.success) { + return this.errorOutput(result, { files: [], totalCount: 0 }); + } + + const files = result.result?.files || []; + const totalCount = result.result?.totalCount; + + const state: ListFilesState = { files, totalCount }; + + const content = formatFileList({ + directory: args.directoryPath, + files: files.map((f: { isDirectory: boolean; name: string }) => ({ + isDirectory: f.isDirectory, + name: f.name, + })), + sortBy: args.sortBy, + sortOrder: args.sortOrder, + totalCount, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async readFile(args: ReadFileParams): Promise { + try { + const result = await this.callService('readLocalFile', args); + + if (!result.success) { + return this.errorOutput(result, { + content: '', + endLine: args.endLine, + path: args.path, + startLine: args.startLine, + }); + } + + const r = result.result || {}; + const fileContent = r.content || ''; + + const state: ReadFileState = { + charCount: r.charCount ?? fileContent.length, + content: fileContent, + endLine: args.endLine, + fileType: r.fileType, + filename: r.filename, + loc: r.loc, + path: args.path, + startLine: args.startLine, + totalCharCount: r.totalCharCount, + totalLines: r.totalLineCount ?? r.totalLines, + }; + + const lineRange: [number, number] | undefined = + args.startLine !== undefined && args.endLine !== undefined + ? [args.startLine, args.endLine] + : undefined; + + const content = formatFileContent({ + content: fileContent, + lineRange, + path: args.path, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async writeFile(args: WriteFileParams): Promise { + try { + const result = await this.callService('writeLocalFile', args); + + if (!result.success) { + return this.errorOutput(result, { path: args.path, success: false }); + } + + const state: WriteFileState = { + bytesWritten: result.result?.bytesWritten, + path: args.path, + success: true, + }; + + const content = formatWriteResult({ path: args.path, success: true }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async editFile(args: EditFileParams): Promise { + try { + const result = await this.callService('editLocalFile', args); + + if (!result.success) { + return this.errorOutput(result, { path: args.path, replacements: 0 }); + } + + const state: EditFileState = { + diffText: result.result?.diffText, + linesAdded: result.result?.linesAdded, + linesDeleted: result.result?.linesDeleted, + path: args.path, + replacements: result.result?.replacements || 0, + }; + + const content = formatEditResult({ + filePath: args.path, + linesAdded: state.linesAdded, + linesDeleted: state.linesDeleted, + replacements: state.replacements, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async searchFiles(args: SearchFilesParams): Promise { + try { + const result = await this.callService('searchLocalFiles', args); + + if (!result.success) { + return this.errorOutput(result, { results: [], totalCount: 0 }); + } + + const rawResults = result.result?.results || result.result; + const results = Array.isArray(rawResults) ? rawResults : []; + const state: SearchFilesState = { + results, + totalCount: result.result?.totalCount || results.length, + }; + + const content = formatFileSearchResults( + results.map((r: { path: string }) => ({ path: r.path })), + ); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async moveFiles(args: MoveFilesParams): Promise { + try { + const result = await this.callService('moveLocalFiles', args); + + if (!result.success) { + return this.errorOutput(result, { + results: [], + successCount: 0, + totalCount: args.operations.length, + }); + } + + const rawResults = result.result?.results || result.result; + const results = Array.isArray(rawResults) ? rawResults : []; + const successCount = + result.result?.successCount ?? + results.filter((r: { success: boolean }) => r.success).length; + + const state: MoveFilesState = { + results, + successCount, + totalCount: args.operations.length, + }; + + const content = formatMoveResults(results); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async renameFile(args: RenameFileParams): Promise { + try { + const result = await this.callService('renameLocalFile', args); + + if (!result.success) { + const errorMsg = result.error?.message || result.result?.error; + return { + content: formatRenameResult({ + error: errorMsg, + newName: args.newName, + oldPath: args.oldPath, + success: false, + }), + state: { + error: errorMsg, + newPath: '', + oldPath: args.oldPath, + success: false, + } satisfies RenameFileState, + success: true, + }; + } + + const state: RenameFileState = { + error: result.result?.error, + newPath: result.result?.newPath || '', + oldPath: args.oldPath, + success: true, + }; + + const content = formatRenameResult({ + error: result.result?.error, + newName: args.newName, + oldPath: args.oldPath, + success: true, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + // ==================== Shell Commands ==================== + + async runCommand(args: RunCommandParams): Promise { + try { + const result = await this.callService('runCommand', args); + + if (!result.success) { + return this.errorOutput(result, { + error: result.error?.message, + isBackground: args.background || false, + success: false, + }); + } + + const r = result.result || {}; + + const state: RunCommandState = { + commandId: r.commandId || r.shell_id, + error: r.error, + exitCode: r.exitCode ?? r.exit_code, + isBackground: args.background || false, + output: r.output, + stderr: r.stderr, + stdout: r.stdout, + success: result.success, + }; + + const content = formatCommandResult({ + error: r.error, + exitCode: r.exitCode ?? r.exit_code, + shellId: r.commandId || r.shell_id, + stderr: r.stderr, + stdout: r.stdout || r.output, + success: result.success, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async getCommandOutput(args: GetCommandOutputParams): Promise { + try { + const result = await this.callService('getCommandOutput', args); + + if (!result.success) { + return this.errorOutput(result, { + error: result.error?.message, + running: false, + success: false, + }); + } + + const r = result.result || {}; + + const state: GetCommandOutputState = { + error: r.error, + newOutput: r.newOutput || r.output, + running: r.running ?? false, + success: result.success, + }; + + const content = formatCommandOutput({ + error: r.error, + output: r.newOutput || r.output, + running: r.running ?? false, + success: result.success, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async killCommand(args: KillCommandParams): Promise { + try { + const result = await this.callService('killCommand', args); + + if (!result.success) { + return this.errorOutput(result, { + commandId: args.commandId, + error: result.error?.message, + success: false, + }); + } + + const state: KillCommandState = { + commandId: args.commandId, + error: result.result?.error, + success: result.success, + }; + + const content = formatKillResult({ + error: result.result?.error, + shellId: args.commandId, + success: result.success, + }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + // ==================== Search & Find ==================== + + async grepContent(args: GrepContentParams): Promise { + try { + const result = await this.callService('grepContent', args); + + if (!result.success) { + return this.errorOutput(result, { + matches: [], + pattern: args.pattern, + totalMatches: 0, + }); + } + + const r = result.result || {}; + const matches = r.matches || []; + const totalMatches = r.totalMatches ?? r.total_matches ?? 0; + + const state: GrepContentState = { + matches, + pattern: args.pattern, + totalMatches, + }; + + const content = formatGrepResults({ matches, totalMatches }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + async globFiles(args: GlobFilesParams): Promise { + try { + const result = await this.callService('globLocalFiles', args); + + if (!result.success) { + return this.errorOutput(result, { + files: [], + pattern: args.pattern, + totalCount: 0, + }); + } + + const files = result.result?.files || []; + const totalCount = result.result?.totalCount ?? result.result?.total_files ?? files.length; + + const state: GlobFilesState = { + files, + pattern: args.pattern, + totalCount, + }; + + const content = formatGlobResults({ files, totalFiles: totalCount }); + + return { content, state, success: true }; + } catch (error) { + return this.handleError(error); + } + } + + // ==================== Helpers ==================== + + protected handleError(error: unknown): BuiltinServerRuntimeOutput { + const errorMessage = error instanceof Error ? error.message : String(error); + return { content: errorMessage, error, success: false }; + } + + private errorOutput(result: ServiceResult, state: any): BuiltinServerRuntimeOutput { + return { + content: result.error?.message || JSON.stringify(result.error), + state, + success: true, + }; + } +} diff --git a/packages/tool-runtime/src/index.ts b/packages/tool-runtime/src/index.ts new file mode 100644 index 0000000000..cac3077c12 --- /dev/null +++ b/packages/tool-runtime/src/index.ts @@ -0,0 +1,2 @@ +export { ComputerRuntime } from './ComputerRuntime'; +export type * from './types'; diff --git a/packages/tool-runtime/src/types.ts b/packages/tool-runtime/src/types.ts new file mode 100644 index 0000000000..55fc2b31e5 --- /dev/null +++ b/packages/tool-runtime/src/types.ts @@ -0,0 +1,192 @@ +/** + * Normalized result returned by the service layer. + * Each ComputerRuntime subclass maps its raw service response into this shape. + */ +export interface ServiceResult { + error?: { message: string; name?: string }; + result: any; + success: boolean; +} + +// ==================== Params ==================== + +export interface ListFilesParams { + directoryPath: string; + sortBy?: string; + sortOrder?: string; +} + +export interface ReadFileParams { + endLine?: number; + path: string; + startLine?: number; +} + +export interface WriteFileParams { + content: string; + createDirectories?: boolean; + path: string; +} + +export interface EditFileParams { + all?: boolean; + path: string; + replace: string; + search: string; +} + +export interface SearchFilesParams { + directory: string; + fileType?: string; + keyword?: string; + modifiedAfter?: string; + modifiedBefore?: string; +} + +export interface MoveFilesParams { + operations: Array<{ + destination: string; + source: string; + }>; +} + +export interface RenameFileParams { + newName: string; + oldPath: string; +} + +export interface GlobFilesParams { + directory?: string; + pattern: string; +} + +export interface RunCommandParams { + background?: boolean; + command: string; + timeout?: number; +} + +export interface GetCommandOutputParams { + commandId: string; +} + +export interface KillCommandParams { + commandId: string; +} + +export interface GrepContentParams { + directory: string; + filePattern?: string; + pattern: string; + recursive?: boolean; +} + +// ==================== State ==================== + +export interface ListFilesState { + files: Array<{ + isDirectory: boolean; + name: string; + path?: string; + size?: number; + }>; + totalCount?: number; +} + +export interface ReadFileState { + /** Character count of the returned content */ + charCount?: number; + content: string; + endLine?: number; + /** Base filename extracted from path */ + filename?: string; + /** Detected file type (e.g., 'ts', 'md', 'json') */ + fileType?: string; + /** Line range as tuple [start, end] */ + loc?: [number, number]; + path: string; + startLine?: number; + /** Total character count of the entire file */ + totalCharCount?: number; + /** Total line count of the entire file */ + totalLines?: number; +} + +export interface WriteFileState { + bytesWritten?: number; + path: string; + success: boolean; +} + +export interface EditFileState { + diffText?: string; + linesAdded?: number; + linesDeleted?: number; + path: string; + replacements: number; +} + +export interface SearchFilesState { + results: Array<{ + isDirectory?: boolean; + modifiedAt?: string; + name?: string; + path: string; + size?: number; + }>; + totalCount: number; +} + +export interface MoveFilesState { + results: Array<{ + destination?: string; + error?: string; + source?: string; + success: boolean; + }>; + successCount: number; + totalCount: number; +} + +export interface RenameFileState { + error?: string; + newPath: string; + oldPath: string; + success: boolean; +} + +export interface GlobFilesState { + files: string[]; + pattern: string; + totalCount: number; +} + +export interface RunCommandState { + commandId?: string; + error?: string; + exitCode?: number; + isBackground: boolean; + output?: string; + stderr?: string; + stdout?: string; + success: boolean; +} + +export interface GetCommandOutputState { + error?: string; + newOutput?: string; + running: boolean; + success: boolean; +} + +export interface KillCommandState { + commandId: string; + error?: string; + success: boolean; +} + +export interface GrepContentState { + matches: Array; + pattern: string; + totalMatches: number; +} diff --git a/src/features/PluginsUI/Render/BuiltinType/index.tsx b/src/features/PluginsUI/Render/BuiltinType/index.tsx index e8e7cb365d..8c68919bc3 100644 --- a/src/features/PluginsUI/Render/BuiltinType/index.tsx +++ b/src/features/PluginsUI/Render/BuiltinType/index.tsx @@ -1,8 +1,10 @@ import { getBuiltinRender } from '@lobechat/builtin-tools/renders'; +import { ToolRenderProvider } from '@lobechat/shared-tool-ui'; import { safeParseJSON } from '@lobechat/utils'; import { memo } from 'react'; import { useParseContent } from '../useParseContent'; +import { useToolRenderCaps } from './useToolRenderCaps'; export interface BuiltinTypeProps { apiName?: string; @@ -34,6 +36,7 @@ const BuiltinType = memo( apiName, }) => { const { data } = useParseContent(content); + const caps = useToolRenderCaps(); const Render = getBuiltinRender(identifier, apiName); @@ -42,16 +45,18 @@ const BuiltinType = memo( const args = safeParseJSON(argumentsStr); return ( - + + + ); }, ); diff --git a/src/features/PluginsUI/Render/BuiltinType/useToolRenderCaps.ts b/src/features/PluginsUI/Render/BuiltinType/useToolRenderCaps.ts new file mode 100644 index 0000000000..940f644c2b --- /dev/null +++ b/src/features/PluginsUI/Render/BuiltinType/useToolRenderCaps.ts @@ -0,0 +1,39 @@ +import type { ToolRenderCapabilities } from '@lobechat/shared-tool-ui'; +import { useMemo } from 'react'; + +import { localFileService } from '@/services/electron/localFileService'; +import { useChatStore } from '@/store/chat'; +import { chatToolSelectors } from '@/store/chat/slices/builtinTool/selectors'; +import { useElectronStore } from '@/store/electron'; +import { desktopStateSelectors } from '@/store/electron/selectors'; + +/** + * Provides platform-aware capabilities for tool render components. + * In Electron: provides file operations, loading state, relative paths. + * In browser: provides only loading state (no file operations). + */ +export const useToolRenderCaps = (): ToolRenderCapabilities => { + const isElectron = typeof window !== 'undefined' && !!(window as any).__ELECTRON__; + + return useMemo(() => { + const caps: ToolRenderCapabilities = { + isLoading: (messageId: string) => { + return chatToolSelectors.isSearchingLocalFiles(messageId)(useChatStore.getState()); + }, + }; + + if (isElectron) { + caps.openFile = (path: string) => { + localFileService.openLocalFile({ path }); + }; + caps.openFolder = (path: string) => { + localFileService.openLocalFolder({ isDirectory: false, path }); + }; + caps.displayRelativePath = (path: string) => { + return desktopStateSelectors.displayRelativePath(path)(useElectronStore.getState()); + }; + } + + return caps; + }, [isElectron]); +}; diff --git a/src/locales/default/plugin.ts b/src/locales/default/plugin.ts index 227fb6195c..04801ac7cb 100644 --- a/src/locales/default/plugin.ts +++ b/src/locales/default/plugin.ts @@ -52,6 +52,7 @@ export default { 'builtins.lobe-cloud-sandbox.apiName.runCommand': 'Run command', 'builtins.lobe-cloud-sandbox.apiName.searchLocalFiles': 'Search files', 'builtins.lobe-cloud-sandbox.apiName.writeLocalFile': 'Write file', + 'builtins.lobe-cloud-sandbox.inspector.noResults': 'No results', 'builtins.lobe-cloud-sandbox.title': 'Cloud Sandbox', 'builtins.lobe-group-agent-builder.apiName.batchCreateAgents': 'Batch create agents', 'builtins.lobe-group-agent-builder.apiName.createAgent': 'Create agent',