mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
♻️ refactor: unify tool content formatting with ComputerRuntime and shared UI (#13470)
* ♻️ 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) <noreply@anthropic.com> * 🐛 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) <noreply@anthropic.com> * ♻️ 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) <noreply@anthropic.com> * 🐛 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) <noreply@anthropic.com> * 🐛 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) <noreply@anthropic.com> * 🐛 fix: handle missing path in grep matches to avoid undefined display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 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) <noreply@anthropic.com> * 🐛 fix: add missing cloud-sandbox i18n key for noResults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f96edd56fb
commit
be99aaebd0
69 changed files with 2305 additions and 2523 deletions
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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:*"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<BuiltinServerRuntimeOutput> {
|
||||
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<string, any>,
|
||||
): Promise<SandboxCallToolResult> {
|
||||
return this.sandboxService.callTool(toolName, params);
|
||||
}
|
||||
|
||||
async readLocalFile(args: ReadLocalFileParams): Promise<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<string, any>,
|
||||
): 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EditLocalFileParams, EditLocalFileState>
|
||||
>(({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.editLocalFile')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.editLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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(
|
||||
<Text code as={'span'} color={cssVar.colorSuccess} fontSize={12} key="added">
|
||||
<Icon icon={Plus} size={12} />
|
||||
{linesAdded}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
if (linesDeleted > 0) {
|
||||
statsParts.push(
|
||||
<Text code as={'span'} color={cssVar.colorError} fontSize={12} key="deleted">
|
||||
<Icon icon={Minus} size={12} />
|
||||
{linesDeleted}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.editLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{!isLoading && statsParts.length > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
{statsParts.map((part, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && <span className={styles.separator}> / </span>}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
EditLocalFileInspector.displayName = 'EditLocalFileInspector';
|
||||
export const EditLocalFileInspector = createEditLocalFileInspector(
|
||||
'builtins.lobe-cloud-sandbox.apiName.editLocalFile',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BuiltinInspectorProps<GlobFilesParams, GlobFilesState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const pattern = args?.pattern || partialArgs?.pattern || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!pattern)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.globLocalFiles')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.globLocalFiles')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{pattern}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if glob was successful
|
||||
const totalCount = pluginState?.totalCount ?? 0;
|
||||
const hasResults = totalCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.globLocalFiles')}: </span>
|
||||
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
|
||||
{isLoading ? null : pluginState ? (
|
||||
hasResults ? (
|
||||
<>
|
||||
<span style={{ marginInlineStart: 4 }}>({totalCount})</span>
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
</>
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
export const GlobLocalFilesInspector = createGlobLocalFilesInspector(
|
||||
'builtins.lobe-cloud-sandbox.apiName.globLocalFiles',
|
||||
);
|
||||
|
||||
GlobLocalFilesInspector.displayName = 'GlobLocalFilesInspector';
|
||||
|
|
|
|||
|
|
@ -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<GrepContentParams, GrepContentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const pattern = args?.pattern || partialArgs?.pattern || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!pattern)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.grepContent')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.grepContent')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{pattern}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check result count
|
||||
const resultCount = pluginState?.totalMatches ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.grepContent')}: </span>
|
||||
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
|
||||
{!isLoading &&
|
||||
pluginState &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t('builtins.lobe-local-system.inspector.noResults')})
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
export const GrepContentInspector = createGrepContentInspector({
|
||||
noResultsKey: 'builtins.lobe-cloud-sandbox.inspector.noResults',
|
||||
translationKey: 'builtins.lobe-cloud-sandbox.apiName.grepContent',
|
||||
});
|
||||
|
||||
GrepContentInspector.displayName = 'GrepContentInspector';
|
||||
|
|
|
|||
|
|
@ -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<ListLocalFilesParams, ListLocalFilesState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const path = args?.path || partialArgs?.path || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!path)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.listLocalFiles')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.listLocalFiles')}: </span>
|
||||
<FilePathDisplay isDirectory filePath={path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show result count if available
|
||||
const resultCount = pluginState?.files?.length ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.listLocalFiles')}: </span>
|
||||
<FilePathDisplay isDirectory filePath={path} />
|
||||
{!isLoading &&
|
||||
pluginState?.files &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t('builtins.lobe-local-system.inspector.noResults')})
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ListLocalFilesInspector.displayName = 'ListLocalFilesInspector';
|
||||
export const ListLocalFilesInspector = createListLocalFilesInspector(
|
||||
'builtins.lobe-cloud-sandbox.apiName.listLocalFiles',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<ReadLocalFileParams, ReadLocalFileState>
|
||||
>(({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.readLocalFile')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.readLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lineRangeText && <span className={styles.lineRange}>{lineRangeText}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.readLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lineRangeText && <span className={styles.lineRange}>{lineRangeText}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ReadLocalFileInspector.displayName = 'ReadLocalFileInspector';
|
||||
export const ReadLocalFileInspector = createReadLocalFileInspector(
|
||||
'builtins.lobe-cloud-sandbox.apiName.readLocalFile',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BuiltinInspectorProps<RunCommandParams, RunCommandState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const description = args?.description || partialArgs?.description;
|
||||
|
||||
if (isArgumentsStreaming) {
|
||||
if (!description)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.runCommand')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.runCommand')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.runCommand')}: </span>
|
||||
{description && <span className={highlightTextStyles.primary}>{description}</span>}
|
||||
{isLoading ? null : pluginState?.success && pluginState?.exitCode === 0 ? (
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
export const RunCommandInspector = createRunCommandInspector(
|
||||
'builtins.lobe-cloud-sandbox.apiName.runCommand',
|
||||
);
|
||||
|
||||
RunCommandInspector.displayName = 'RunCommandInspector';
|
||||
|
|
|
|||
|
|
@ -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<SearchLocalFilesParams, SearchLocalFilesState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const query = args?.query || partialArgs?.query || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!query)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.searchLocalFiles')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.searchLocalFiles')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{query}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if search returned results
|
||||
const resultCount = pluginState?.results?.length ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.searchLocalFiles')}: </span>
|
||||
{query && <span className={highlightTextStyles.primary}>{query}</span>}
|
||||
{!isLoading &&
|
||||
pluginState?.results &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t('builtins.lobe-local-system.inspector.noResults')})
|
||||
</Text>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
export const SearchLocalFilesInspector = createSearchLocalFilesInspector({
|
||||
noResultsKey: 'builtins.lobe-cloud-sandbox.inspector.noResults',
|
||||
translationKey: 'builtins.lobe-cloud-sandbox.apiName.searchLocalFiles',
|
||||
});
|
||||
|
||||
SearchLocalFilesInspector.displayName = 'SearchLocalFilesInspector';
|
||||
|
|
|
|||
|
|
@ -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<WriteLocalFileParams, WriteLocalFileState>
|
||||
>(({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.writeLocalFile')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(inspectorTextStyles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
|
||||
>
|
||||
<span>{t('builtins.lobe-cloud-sandbox.apiName.writeLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lines > 0 && (
|
||||
<Text code as={'span'} color={cssVar.colorSuccess} fontSize={12}>
|
||||
{' '}
|
||||
<Icon icon={Plus} size={12} />
|
||||
{lines}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
WriteLocalFileInspector.displayName = 'WriteLocalFileInspector';
|
||||
export const WriteLocalFileInspector = createWriteLocalFileInspector(
|
||||
'builtins.lobe-cloud-sandbox.apiName.writeLocalFile',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class CloudSandboxExecutor extends BaseExecutor<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
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<typeof CloudSandboxApiName> {
|
|||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
const runtime = this.getRuntime(ctx);
|
||||
const result = await runtime.globLocalFiles(params);
|
||||
const result = await runtime.globFiles(params);
|
||||
return this.toBuiltinResult(result);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:*"
|
||||
|
|
|
|||
285
packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts
Normal file
285
packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts
Normal file
|
|
@ -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<any>;
|
||||
getCommandOutput: (params: any) => Promise<any>;
|
||||
globFiles: (params: any) => Promise<any>;
|
||||
grepContent: (params: any) => Promise<any>;
|
||||
killCommand: (params: any) => Promise<any>;
|
||||
listLocalFiles: (params: any) => Promise<any>;
|
||||
moveLocalFiles: (params: any) => Promise<any>;
|
||||
readLocalFile: (params: any) => Promise<any>;
|
||||
readLocalFiles: (params: any) => Promise<any>;
|
||||
renameLocalFile: (params: any) => Promise<any>;
|
||||
runCommand: (params: any) => Promise<any>;
|
||||
searchLocalFiles: (params: any) => Promise<any>;
|
||||
writeFile: (params: any) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps IPC tool names to localFileService method names.
|
||||
* IPC service uses different method names than the standard tool names.
|
||||
*/
|
||||
const SERVICE_METHOD_MAP: Record<string, keyof ILocalSystemService> = {
|
||||
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<string, any>,
|
||||
): Promise<ServiceResult> {
|
||||
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<any>;
|
||||
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<string, any>): 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<BuiltinServerRuntimeOutput> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EditLocalFileParams, EditLocalFileState>
|
||||
>(({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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(
|
||||
<Text code as={'span'} color={cssVar.colorSuccess} fontSize={12} key="added">
|
||||
<Icon icon={Plus} size={12} />
|
||||
{linesAdded}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
if (linesDeleted > 0) {
|
||||
statsParts.push(
|
||||
<Text code as={'span'} color={cssVar.colorError} fontSize={12} key="deleted">
|
||||
<Icon icon={Minus} size={12} />
|
||||
{linesDeleted}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.editLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{!isLoading && statsParts.length > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
{statsParts.map((part, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && <span className={styles.separator}> / </span>}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
EditLocalFileInspector.displayName = 'EditLocalFileInspector';
|
||||
export const EditLocalFileInspector = createEditLocalFileInspector(
|
||||
'builtins.lobe-local-system.apiName.editLocalFile',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BuiltinInspectorProps<GlobFilesParams, GlobFilesState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const pattern = args?.pattern || partialArgs?.pattern || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!pattern)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{pattern}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if glob was successful
|
||||
const isSuccess = pluginState?.result?.success;
|
||||
const engine = pluginState?.result?.engine;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}: </span>
|
||||
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
|
||||
{isLoading ? null : pluginState?.result ? (
|
||||
isSuccess ? (
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)
|
||||
) : null}
|
||||
{!isLoading && engine && (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
[{engine}]
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
export const GlobLocalFilesInspector = createGlobLocalFilesInspector(
|
||||
'builtins.lobe-local-system.apiName.globLocalFiles',
|
||||
);
|
||||
|
||||
GlobLocalFilesInspector.displayName = 'GlobLocalFilesInspector';
|
||||
|
|
|
|||
|
|
@ -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<GrepContentParams, GrepContentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const pattern = args?.pattern || partialArgs?.pattern || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!pattern)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.grepContent')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.grepContent')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{pattern}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check result count
|
||||
const resultCount = pluginState?.result?.total_matches ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
const engine = pluginState?.result?.engine;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.grepContent')}: </span>
|
||||
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
|
||||
{!isLoading &&
|
||||
pluginState?.result &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t('builtins.lobe-local-system.inspector.noResults')})
|
||||
</Text>
|
||||
))}
|
||||
{!isLoading && engine && (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
[{engine}]
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export const GrepContentInspector = createGrepContentInspector({
|
||||
noResultsKey: 'builtins.lobe-local-system.inspector.noResults',
|
||||
translationKey: 'builtins.lobe-local-system.apiName.grepContent',
|
||||
});
|
||||
|
||||
GrepContentInspector.displayName = 'GrepContentInspector';
|
||||
|
|
|
|||
|
|
@ -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<ListLocalFileParams, LocalFileListState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const path = args?.path || partialArgs?.path || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!path)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.listLocalFiles')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.listLocalFiles')}: </span>
|
||||
<FilePathDisplay isDirectory filePath={path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show result count if available
|
||||
const resultCount = pluginState?.listResults?.length ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.listLocalFiles')}: </span>
|
||||
<Flexbox allowShrink horizontal align={'center'} justify={'center'}>
|
||||
<FilePathDisplay isDirectory filePath={path} />
|
||||
</Flexbox>
|
||||
{!isLoading &&
|
||||
pluginState?.listResults &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t('builtins.lobe-local-system.inspector.noResults')})
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ListLocalFilesInspector.displayName = 'ListLocalFilesInspector';
|
||||
export const ListLocalFilesInspector = createListLocalFilesInspector(
|
||||
'builtins.lobe-local-system.apiName.listLocalFiles',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<LocalReadFileParams, LocalReadFileState>
|
||||
>(({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lineRangeText && <span className={styles.lineRange}>{lineRangeText}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.readLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lineRangeText && <span className={styles.lineRange}>{lineRangeText}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ReadLocalFileInspector.displayName = 'ReadLocalFileInspector';
|
||||
export const ReadLocalFileInspector = createReadLocalFileInspector(
|
||||
'builtins.lobe-local-system.apiName.readLocalFile',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BuiltinInspectorProps<RunCommandParams, RunCommandState>>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.runCommand')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.runCommand')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Get execution result from pluginState
|
||||
const result = pluginState?.result;
|
||||
const isSuccess = result?.success || result?.exit_code === 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.runCommand')}: </span>
|
||||
{description && <span className={highlightTextStyles.primary}>{description}</span>}
|
||||
{isLoading ? null : result?.success !== undefined ? (
|
||||
isSuccess ? (
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
export const RunCommandInspector = createRunCommandInspector(
|
||||
'builtins.lobe-local-system.apiName.runCommand',
|
||||
);
|
||||
|
||||
RunCommandInspector.displayName = 'RunCommandInspector';
|
||||
|
||||
export default RunCommandInspector;
|
||||
|
|
|
|||
|
|
@ -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<LocalSearchFilesParams, LocalFileSearchState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const keywords = args?.keywords || partialArgs?.keywords || '';
|
||||
|
||||
// During argument streaming
|
||||
if (isArgumentsStreaming) {
|
||||
if (!keywords)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{keywords}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if search returned results
|
||||
const resultCount = pluginState?.searchResults?.length ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
const engine = pluginState?.engine;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.searchLocalFiles')}: </span>
|
||||
{keywords && <span className={highlightTextStyles.primary}>{keywords}</span>}
|
||||
{!isLoading &&
|
||||
pluginState?.searchResults &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t('builtins.lobe-local-system.inspector.noResults')})
|
||||
</Text>
|
||||
))}
|
||||
{!isLoading && engine && (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
[{engine}]
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
export const SearchLocalFilesInspector = createSearchLocalFilesInspector({
|
||||
noResultsKey: 'builtins.lobe-local-system.inspector.noResults',
|
||||
translationKey: 'builtins.lobe-local-system.apiName.searchLocalFiles',
|
||||
});
|
||||
|
||||
SearchLocalFilesInspector.displayName = 'SearchLocalFilesInspector';
|
||||
|
|
|
|||
|
|
@ -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<BuiltinInspectorProps<WriteLocalFileParams>>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-local-system.apiName.writeLocalFile')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(inspectorTextStyles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
|
||||
>
|
||||
<span>{t('builtins.lobe-local-system.apiName.writeLocalFile')}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lines > 0 && (
|
||||
<Text code as={'span'} color={cssVar.colorSuccess} fontSize={12}>
|
||||
{' '}
|
||||
<Icon icon={Plus} size={12} />
|
||||
{lines}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
export const WriteLocalFileInspector = createWriteLocalFileInspector(
|
||||
'builtins.lobe-local-system.apiName.writeLocalFile',
|
||||
);
|
||||
|
||||
WriteLocalFileInspector.displayName = 'WriteLocalFileInspector';
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<EditLocalFileParams, EditLocalFileState>>(
|
||||
const EditLocalFile = memo<BuiltinRenderProps<any, EditLocalFileState>>(
|
||||
({ args, pluginState, pluginError }) => {
|
||||
if (!args) return <Skeleton active />;
|
||||
|
||||
// Support both IPC format (file_path) and ComputerRuntime format (path)
|
||||
const filePath = args.file_path || args.path || '';
|
||||
|
||||
return (
|
||||
<Flexbox gap={12}>
|
||||
{pluginError ? (
|
||||
|
|
@ -19,7 +21,7 @@ const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFile
|
|||
/>
|
||||
) : pluginState?.diffText ? (
|
||||
<PatchDiff
|
||||
fileName={args.file_path}
|
||||
fileName={filePath}
|
||||
patch={pluginState.diffText}
|
||||
showHeader={false}
|
||||
variant="borderless"
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import type { LocalFileItem } from '@lobechat/electron-client-ipc';
|
||||
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 {
|
||||
listResults?: LocalFileItem[];
|
||||
listResults?: Array<{ isDirectory: boolean; name: string; path?: string; size?: number }>;
|
||||
messageId: string;
|
||||
pluginError: ChatMessagePluginError;
|
||||
}
|
||||
|
||||
const SearchFiles = memo<SearchFilesProps>(({ 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<SearchFilesProps>(({ listResults = [], messageId }) =>
|
|||
return (
|
||||
<Flexbox gap={2} style={{ maxHeight: 140, overflow: 'scroll' }}>
|
||||
{listResults.map((item) => (
|
||||
<FileItem key={item.path} {...item} showTime />
|
||||
<FileItem key={item.path || item.name} {...item} showTime />
|
||||
))}
|
||||
</Flexbox>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<ListLocalFileParams, LocalFileListState>>(
|
||||
const ListFiles = memo<BuiltinRenderProps<any, ListFilesState>>(
|
||||
({ messageId, pluginError, pluginState }) => {
|
||||
return (
|
||||
<SearchResult
|
||||
listResults={pluginState?.listResults}
|
||||
listResults={pluginState?.files}
|
||||
messageId={messageId}
|
||||
pluginError={pluginError}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<MoveFileItemProps>(({ 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 (
|
||||
<Flexbox horizontal align="center" className={styles.item} gap={8} width="100%">
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<MoveLocalFilesParams>>(({ args }) => {
|
||||
const { items } = args;
|
||||
interface MoveFilesArgs {
|
||||
items?: Array<{ newPath: string; oldPath: string }>;
|
||||
operations?: Array<{ destination: string; source: string }>;
|
||||
}
|
||||
|
||||
const MoveLocalFiles = memo<BuiltinRenderProps<MoveFilesArgs>>(({ 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 (
|
||||
<Flexbox gap={8}>
|
||||
<Text type="secondary">{t('localFiles.moveFiles.itemsMoved', { count: items.length })}</Text>
|
||||
<Text type="secondary">
|
||||
{t('localFiles.moveFiles.itemsMoved', { count: moveItems.length })}
|
||||
</Text>
|
||||
<Flexbox gap={6}>
|
||||
{items.map((item, index) => (
|
||||
{moveItems.map((item, index) => (
|
||||
<MoveFileItem key={index} newPath={item.newPath} oldPath={item.oldPath} />
|
||||
))}
|
||||
</Flexbox>
|
||||
|
|
|
|||
|
|
@ -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<ReadFileViewProps>(
|
||||
({ filename, path, fileType, charCount, content, totalLineCount, totalCharCount, loc }) => {
|
||||
const ReadFileView = memo<ReadFileState>(
|
||||
({
|
||||
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 (
|
||||
<Flexbox className={styles.container} gap={12}>
|
||||
|
|
@ -125,41 +133,60 @@ const ReadFileView = memo<ReadFileViewProps>(
|
|||
<Text ellipsis className={styles.fileName}>
|
||||
{filename}
|
||||
</Text>
|
||||
{/* Actions on Hover */}
|
||||
<Flexbox horizontal className={styles.actions} gap={2} style={{ marginLeft: 8 }}>
|
||||
<ActionIcon
|
||||
icon={ExternalLink}
|
||||
size="small"
|
||||
title={t('localFiles.openFile')}
|
||||
onClick={handleOpenFile}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={FolderOpen}
|
||||
size="small"
|
||||
title={t('localFiles.openFolder')}
|
||||
onClick={handleOpenFolder}
|
||||
/>
|
||||
</Flexbox>
|
||||
{(handleOpenFile || handleOpenFolder) && (
|
||||
<Flexbox horizontal className={styles.actions} gap={2} style={{ marginLeft: 8 }}>
|
||||
{handleOpenFile && (
|
||||
<ActionIcon
|
||||
icon={ExternalLink}
|
||||
size="small"
|
||||
title={t('localFiles.openFile')}
|
||||
onClick={handleOpenFile}
|
||||
/>
|
||||
)}
|
||||
{handleOpenFolder && (
|
||||
<ActionIcon
|
||||
icon={FolderOpen}
|
||||
size="small"
|
||||
title={t('localFiles.openFolder')}
|
||||
onClick={handleOpenFolder}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox horizontal align={'center'} className={styles.meta} gap={16}>
|
||||
<Flexbox horizontal align={'center'} gap={4}>
|
||||
<Icon icon={Asterisk} size={'small'} />
|
||||
<span>
|
||||
{charCount} / <span className={styles.lineCount}>{totalCharCount}</span>
|
||||
</span>
|
||||
</Flexbox>
|
||||
<Flexbox horizontal align={'center'} gap={4}>
|
||||
<Icon icon={AlignLeft} size={'small'} />
|
||||
<span>
|
||||
L{loc?.[0]}-{loc?.[1]} /{' '}
|
||||
<span className={styles.lineCount}>{totalLineCount}</span>
|
||||
</span>
|
||||
</Flexbox>
|
||||
{charCount !== undefined && (
|
||||
<Flexbox horizontal align={'center'} gap={4}>
|
||||
<Icon icon={Asterisk} size={'small'} />
|
||||
<span>
|
||||
{charCount}
|
||||
{totalCharCount !== undefined && (
|
||||
<>
|
||||
{' '}
|
||||
/ <span className={styles.lineCount}>{totalCharCount}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Flexbox>
|
||||
)}
|
||||
{loc && (
|
||||
<Flexbox horizontal align={'center'} gap={4}>
|
||||
<Icon icon={AlignLeft} size={'small'} />
|
||||
<span>
|
||||
L{loc[0]}-{loc[1]}
|
||||
{totalLines !== undefined && (
|
||||
<>
|
||||
{' '}
|
||||
/ <span className={styles.lineCount}>{totalLines}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
|
||||
{/* Path */}
|
||||
<Text ellipsis className={styles.path} type={'secondary'}>
|
||||
{displayPath}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<{ path: string }, ReadFileState>>(
|
||||
({ args, pluginState, messageId }) => {
|
||||
const { isLoading } = useToolRenderCapabilities();
|
||||
const loading = isLoading?.(messageId);
|
||||
|
||||
const ReadFileQuery = memo<ReadFileQueryProps>(({ args, pluginState, messageId }) => {
|
||||
const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId));
|
||||
if (loading) {
|
||||
return <ReadFileSkeleton />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <ReadFileSkeleton />;
|
||||
}
|
||||
if (!args?.path || !pluginState) return null;
|
||||
|
||||
if (!args?.path || !pluginState) return null;
|
||||
|
||||
return <ReadFileView {...pluginState.fileContent} path={args.path} />;
|
||||
});
|
||||
return <ReadFileView {...pluginState} path={args.path} />;
|
||||
},
|
||||
);
|
||||
|
||||
export default ReadFileQuery;
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<RunCommandParams, RunCommandState>>(
|
||||
({ args, pluginState }) => {
|
||||
const { result } = pluginState || {};
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} gap={8}>
|
||||
<Block gap={8} padding={'0 8px 0'} variant={'outlined'}>
|
||||
<Highlighter
|
||||
wrap
|
||||
language={'sh'}
|
||||
showLanguage={false}
|
||||
style={{ padding: 8 }}
|
||||
variant={'borderless'}
|
||||
>
|
||||
{args.command}
|
||||
</Highlighter>
|
||||
{result?.output && (
|
||||
<Highlighter wrap language={'text'} showLanguage={false} variant={'filled'}>
|
||||
{result.output}
|
||||
</Highlighter>
|
||||
)}
|
||||
</Block>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default RunCommand;
|
||||
|
|
@ -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<SearchFilesProps>(({ searchResults = [], messageId }) => {
|
||||
const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId));
|
||||
const { isLoading } = useToolRenderCapabilities();
|
||||
const loading = isLoading?.(messageId);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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<SearchQueryViewProps>(({ 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 (
|
||||
<SearchView
|
||||
defaultQuery={args?.keywords}
|
||||
defaultQuery={args?.keywords || args?.keyword || args?.query || ''}
|
||||
resultsNumber={searchResults.length}
|
||||
searching={loading || !pluginState}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<LocalSearchFilesParams, LocalFileSearchState>>(
|
||||
const SearchFiles = memo<BuiltinRenderProps<any, SearchFilesState>>(
|
||||
({ messageId, pluginError, args, pluginState }) => {
|
||||
return (
|
||||
<Flexbox gap={4}>
|
||||
|
|
@ -15,7 +14,7 @@ const SearchFiles = memo<BuiltinRenderProps<LocalSearchFilesParams, LocalFileSea
|
|||
<SearchResult
|
||||
messageId={messageId}
|
||||
pluginError={pluginError}
|
||||
searchResults={pluginState?.searchResults}
|
||||
searchResults={pluginState?.results}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<FileItemProps>(
|
||||
({ 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 (
|
||||
<Flexbox
|
||||
|
|
@ -62,15 +79,17 @@ const FileItem = memo<FileItemProps>(
|
|||
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);
|
||||
}}
|
||||
>
|
||||
<FileIcon
|
||||
fileName={name}
|
||||
fileName={name || ''}
|
||||
fileType={type}
|
||||
isDirectory={isDirectory}
|
||||
size={16}
|
||||
|
|
@ -83,13 +102,13 @@ const FileItem = memo<FileItemProps>(
|
|||
style={{ overflow: 'hidden', width: '100%' }}
|
||||
>
|
||||
<div className={styles.title}>{name}</div>
|
||||
{showTime ? (
|
||||
{showTime && createdTime ? (
|
||||
<div className={styles.path}>{dayjs(createdTime).format('MMM DD hh:mm')}</div>
|
||||
) : (
|
||||
<div className={styles.path}>{path}</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
{isHovering ? (
|
||||
{isHovering && openFolder ? (
|
||||
<Flexbox direction={'horizontal-reverse'} gap={8} style={{ minWidth: 50 }}>
|
||||
<ActionIcon
|
||||
icon={FolderOpen}
|
||||
|
|
@ -98,12 +117,12 @@ const FileItem = memo<FileItemProps>(
|
|||
title={t('localFiles.openFolder')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
localFileService.openLocalFolder({ isDirectory, path });
|
||||
if (path) openFolder(path);
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
) : (
|
||||
<span className={styles.size}>{formatSize(size)}</span>
|
||||
<span className={styles.size}>{size !== undefined ? formatSize(size) : ''}</span>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<typeof LocalSystemApiEnum> {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<typeof LocalSystemApiEnum> {
|
|||
|
||||
runCommand = async (params: RunCommandParams): Promise<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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<typeof LocalSystemApiEnum> {
|
|||
grepContent = async (params: GrepContentParams): Promise<BuiltinToolResult> => {
|
||||
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<BuiltinToolResult> => {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BuiltinInspectorProps<RunCommandParams, CommandResult>>(
|
||||
({ args, partialArgs, isArgumentsStreaming, isLoading, pluginState }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const description = args?.description || partialArgs?.description;
|
||||
|
||||
if (isArgumentsStreaming) {
|
||||
if (!description)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-skills.apiName.runCommand')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-skills.apiName.runCommand')}: </span>
|
||||
<span className={highlightTextStyles.primary}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t('builtins.lobe-skills.apiName.runCommand')}: </span>
|
||||
{description && <span className={highlightTextStyles.primary}>{description}</span>}
|
||||
{isLoading ? null : pluginState?.success !== undefined ? (
|
||||
pluginState.success ? (
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
export const RunCommandInspector = createRunCommandInspector(
|
||||
'builtins.lobe-skills.apiName.runCommand',
|
||||
);
|
||||
|
||||
RunCommandInspector.displayName = 'RunCommandInspector';
|
||||
|
|
|
|||
|
|
@ -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<BuiltinRenderProps<RunCommandParams, CommandResult>>(
|
||||
({ args, content, pluginState }) => {
|
||||
return (
|
||||
<Flexbox className={styles.container} gap={8}>
|
||||
<Block gap={8} padding={8} variant={'outlined'}>
|
||||
<Highlighter
|
||||
wrap
|
||||
language={'sh'}
|
||||
showLanguage={false}
|
||||
style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }}
|
||||
variant={'borderless'}
|
||||
>
|
||||
{args?.command || ''}
|
||||
</Highlighter>
|
||||
{(pluginState?.output || content) && (
|
||||
<Highlighter
|
||||
wrap
|
||||
language={'text'}
|
||||
showLanguage={false}
|
||||
style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }}
|
||||
variant={'filled'}
|
||||
>
|
||||
{pluginState?.output || content}
|
||||
</Highlighter>
|
||||
)}
|
||||
{pluginState?.stderr && (
|
||||
<Highlighter wrap language={'text'} showLanguage={false} variant={'filled'}>
|
||||
{pluginState.stderr}
|
||||
</Highlighter>
|
||||
)}
|
||||
</Block>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
RunCommand.displayName = 'RunCommand';
|
||||
|
||||
export default RunCommand;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export interface FormatGrepResultsParams {
|
||||
matches: string[];
|
||||
matches: Array<string | { content?: string; lineNumber?: number; path: string }>;
|
||||
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` : '';
|
||||
|
||||
|
|
|
|||
24
packages/shared-tool-ui/package.json
Normal file
24
packages/shared-tool-ui/package.json
Normal file
|
|
@ -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": "*"
|
||||
}
|
||||
}
|
||||
109
packages/shared-tool-ui/src/Inspector/EditLocalFile/index.tsx
Normal file
109
packages/shared-tool-ui/src/Inspector/EditLocalFile/index.tsx
Normal file
|
|
@ -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<EditLocalFileInspectorProps>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const linesAdded = pluginState?.linesAdded ?? 0;
|
||||
const linesDeleted = pluginState?.linesDeleted ?? 0;
|
||||
|
||||
const statsParts: ReactNode[] = [];
|
||||
if (linesAdded > 0) {
|
||||
statsParts.push(
|
||||
<Text code as={'span'} color={cssVar.colorSuccess} fontSize={12} key="added">
|
||||
<Icon icon={Plus} size={12} />
|
||||
{linesAdded}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
if (linesDeleted > 0) {
|
||||
statsParts.push(
|
||||
<Text code as={'span'} color={cssVar.colorError} fontSize={12} key="deleted">
|
||||
<Icon icon={Minus} size={12} />
|
||||
{linesDeleted}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{!isLoading && statsParts.length > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
{statsParts.map((part, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && <span className={styles.separator}> / </span>}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
EditLocalFileInspector.displayName = 'EditLocalFileInspector';
|
||||
|
||||
export const createEditLocalFileInspector = (translationKey: string) => {
|
||||
const Inspector = memo<BuiltinInspectorProps<any, any>>((props) => (
|
||||
<EditLocalFileInspector {...props} translationKey={translationKey} />
|
||||
));
|
||||
Inspector.displayName = 'EditLocalFileInspector';
|
||||
return Inspector;
|
||||
};
|
||||
|
|
@ -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<BuiltinInspectorProps<GlobFilesArgs, GlobFilesState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const pattern = args?.pattern || partialArgs?.pattern || '';
|
||||
|
||||
if (isArgumentsStreaming) {
|
||||
if (!pattern)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<span className={highlightTextStyles.primary}>{pattern}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasFiles = (pluginState?.totalCount ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
|
||||
{isLoading ? null : pluginState ? (
|
||||
hasFiles ? (
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Inspector.displayName = 'GlobLocalFilesInspector';
|
||||
return Inspector;
|
||||
};
|
||||
76
packages/shared-tool-ui/src/Inspector/GrepContent/index.tsx
Normal file
76
packages/shared-tool-ui/src/Inspector/GrepContent/index.tsx
Normal file
|
|
@ -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<BuiltinInspectorProps<GrepContentArgs, GrepContentState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const pattern = args?.pattern || partialArgs?.pattern || '';
|
||||
|
||||
if (isArgumentsStreaming) {
|
||||
if (!pattern)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<span className={highlightTextStyles.primary}>{pattern}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const resultCount = pluginState?.totalMatches ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
{pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
|
||||
{!isLoading &&
|
||||
pluginState &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t(noResultsKey as any)})
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Inspector.displayName = 'GrepContentInspector';
|
||||
return Inspector;
|
||||
};
|
||||
|
|
@ -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<BuiltinInspectorProps<ListFilesArgs, any>>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay isDirectory filePath={dirPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay isDirectory filePath={dirPath} />
|
||||
{!isLoading && resultCount > 0 && (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Inspector.displayName = 'ListLocalFilesInspector';
|
||||
return Inspector;
|
||||
};
|
||||
|
|
@ -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<BuiltinInspectorProps<ReadFileArgs, ReadFileState>>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{lineRange && <span style={{ marginInlineStart: 4 }}>({lineRange})</span>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Inspector.displayName = 'ReadLocalFileInspector';
|
||||
return Inspector;
|
||||
};
|
||||
88
packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx
Normal file
88
packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx
Normal file
|
|
@ -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<RunCommandInspectorProps>(
|
||||
({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading, translationKey }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const description = args?.description || partialArgs?.description || args?.command || '';
|
||||
|
||||
if (isArgumentsStreaming) {
|
||||
if (!description)
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<span className={highlightTextStyles.primary}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isSuccess = pluginState?.success || pluginState?.exitCode === 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
{description && <span className={highlightTextStyles.primary}>{description}</span>}
|
||||
{isLoading ? null : pluginState?.success !== undefined ? (
|
||||
isSuccess ? (
|
||||
<Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
|
||||
) : (
|
||||
<X className={styles.statusIcon} color={cssVar.colorError} size={14} />
|
||||
)
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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<BuiltinInspectorProps<RunCommandArgs, RunCommandState>>((props) => (
|
||||
<RunCommandInspector {...props} translationKey={translationKey} />
|
||||
));
|
||||
Inspector.displayName = 'RunCommandInspector';
|
||||
return Inspector;
|
||||
};
|
||||
|
|
@ -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<BuiltinInspectorProps<SearchFilesArgs, SearchFilesState>>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<span className={highlightTextStyles.primary}>{query}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const resultCount = pluginState?.results?.length ?? pluginState?.totalCount ?? 0;
|
||||
const hasResults = resultCount > 0;
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span style={{ marginInlineStart: 2 }}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
{query && <span className={highlightTextStyles.primary}>{query}</span>}
|
||||
{!isLoading &&
|
||||
pluginState &&
|
||||
(hasResults ? (
|
||||
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
|
||||
) : (
|
||||
<Text
|
||||
as={'span'}
|
||||
color={cssVar.colorTextDescription}
|
||||
fontSize={12}
|
||||
style={{ marginInlineStart: 4 }}
|
||||
>
|
||||
({t(noResultsKey as any)})
|
||||
</Text>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Inspector.displayName = 'SearchLocalFilesInspector';
|
||||
return Inspector;
|
||||
};
|
||||
|
|
@ -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<BuiltinInspectorProps<WriteFileArgs, any>>(
|
||||
({ 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 (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
||||
<span>{t(translationKey as any)}: </span>
|
||||
<FilePathDisplay filePath={filePath} />
|
||||
{!isLoading && lineCount && (
|
||||
<>
|
||||
{' '}
|
||||
<Text code as={'span'} color={cssVar.colorSuccess} fontSize={12}>
|
||||
<Icon icon={Plus} size={12} />
|
||||
{lineCount}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Inspector.displayName = 'WriteLocalFileInspector';
|
||||
return Inspector;
|
||||
};
|
||||
8
packages/shared-tool-ui/src/Inspector/index.ts
Normal file
8
packages/shared-tool-ui/src/Inspector/index.ts
Normal file
|
|
@ -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';
|
||||
|
|
@ -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<BuiltinRenderProps<RunCommandParams, RunCommandState>>(
|
||||
({ args, pluginState }) => {
|
||||
const RunCommand = memo<BuiltinRenderProps<RunCommandArgs, RunCommandState>>(
|
||||
({ args, content, pluginState }) => {
|
||||
const output = pluginState?.output || pluginState?.stdout || content;
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} gap={8}>
|
||||
<Block gap={8} padding={8} variant={'outlined'}>
|
||||
|
|
@ -33,9 +34,9 @@ const RunCommand = memo<BuiltinRenderProps<RunCommandParams, RunCommandState>>(
|
|||
style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }}
|
||||
variant={'borderless'}
|
||||
>
|
||||
{args.command}
|
||||
{args?.command || ''}
|
||||
</Highlighter>
|
||||
{pluginState?.output && (
|
||||
{output && (
|
||||
<Highlighter
|
||||
wrap
|
||||
language={'text'}
|
||||
|
|
@ -43,7 +44,7 @@ const RunCommand = memo<BuiltinRenderProps<RunCommandParams, RunCommandState>>(
|
|||
style={{ maxHeight: 200, overflow: 'auto', paddingInline: 8 }}
|
||||
variant={'filled'}
|
||||
>
|
||||
{pluginState.output}
|
||||
{output}
|
||||
</Highlighter>
|
||||
)}
|
||||
{pluginState?.stderr && (
|
||||
1
packages/shared-tool-ui/src/Render/index.ts
Normal file
1
packages/shared-tool-ui/src/Render/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as RunCommandRender } from './RunCommand';
|
||||
65
packages/shared-tool-ui/src/components/FilePathDisplay.tsx
Normal file
65
packages/shared-tool-ui/src/components/FilePathDisplay.tsx
Normal file
|
|
@ -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<FilePathDisplayProps>(({ 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 && (
|
||||
<MaterialFileTypeIcon
|
||||
className={styles.icon}
|
||||
fallbackUnknownType={false}
|
||||
filename={name}
|
||||
size={16}
|
||||
type={isDirectory ? 'folder' : 'file'}
|
||||
variant={'raw'}
|
||||
/>
|
||||
)}
|
||||
{displayPath && (
|
||||
<Text
|
||||
className={styles.text}
|
||||
ellipsis={{
|
||||
tooltipWhenOverflow: true,
|
||||
}}
|
||||
>
|
||||
{displayPath}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FilePathDisplay.displayName = 'FilePathDisplay';
|
||||
26
packages/shared-tool-ui/src/context.tsx
Normal file
26
packages/shared-tool-ui/src/context.tsx
Normal file
|
|
@ -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<ToolRenderCapabilities>({});
|
||||
|
||||
export const ToolRenderProvider = ToolRenderContext.Provider;
|
||||
|
||||
export const useToolRenderCapabilities = () => use(ToolRenderContext);
|
||||
2
packages/shared-tool-ui/src/index.ts
Normal file
2
packages/shared-tool-ui/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export type { ToolRenderCapabilities } from './context';
|
||||
export { ToolRenderProvider, useToolRenderCapabilities } from './context';
|
||||
73
packages/shared-tool-ui/src/styles.ts
Normal file
73
packages/shared-tool-ui/src/styles.ts
Normal file
|
|
@ -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;
|
||||
`,
|
||||
}));
|
||||
15
packages/tool-runtime/package.json
Normal file
15
packages/tool-runtime/package.json
Normal file
|
|
@ -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:*"
|
||||
}
|
||||
}
|
||||
473
packages/tool-runtime/src/ComputerRuntime.ts
Normal file
473
packages/tool-runtime/src/ComputerRuntime.ts
Normal file
|
|
@ -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<string, any>,
|
||||
): Promise<ServiceResult>;
|
||||
|
||||
// ==================== File Operations ====================
|
||||
|
||||
async listFiles(args: ListFilesParams): Promise<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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<BuiltinServerRuntimeOutput> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
2
packages/tool-runtime/src/index.ts
Normal file
2
packages/tool-runtime/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { ComputerRuntime } from './ComputerRuntime';
|
||||
export type * from './types';
|
||||
192
packages/tool-runtime/src/types.ts
Normal file
192
packages/tool-runtime/src/types.ts
Normal file
|
|
@ -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<string | { content?: string; lineNumber?: number; path: string }>;
|
||||
pattern: string;
|
||||
totalMatches: number;
|
||||
}
|
||||
|
|
@ -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<BuiltinTypeProps>(
|
|||
apiName,
|
||||
}) => {
|
||||
const { data } = useParseContent(content);
|
||||
const caps = useToolRenderCaps();
|
||||
|
||||
const Render = getBuiltinRender(identifier, apiName);
|
||||
|
||||
|
|
@ -42,16 +45,18 @@ const BuiltinType = memo<BuiltinTypeProps>(
|
|||
const args = safeParseJSON(argumentsStr);
|
||||
|
||||
return (
|
||||
<Render
|
||||
apiName={apiName}
|
||||
args={args || {}}
|
||||
content={data}
|
||||
identifier={identifier}
|
||||
messageId={messageId || toolCallId || ''}
|
||||
pluginError={pluginError}
|
||||
pluginState={pluginState}
|
||||
toolCallId={toolCallId}
|
||||
/>
|
||||
<ToolRenderProvider value={caps}>
|
||||
<Render
|
||||
apiName={apiName}
|
||||
args={args || {}}
|
||||
content={data}
|
||||
identifier={identifier}
|
||||
messageId={messageId || toolCallId || ''}
|
||||
pluginError={pluginError}
|
||||
pluginState={pluginState}
|
||||
toolCallId={toolCallId}
|
||||
/>
|
||||
</ToolRenderProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<ToolRenderCapabilities>(() => {
|
||||
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]);
|
||||
};
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue