lobehub/packages/shared-tool-ui/src/Inspector/RunCommand/index.tsx
Arvin Xu be99aaebd0
♻️ 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>
2026-04-02 19:42:45 +08:00

88 lines
2.9 KiB
TypeScript

'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;
};