import React from "react"; import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import { FileTreeView } from "#/components/features/files-tab/file-tree-view"; import { HighlightedSourceView } from "#/components/features/files-tab/highlighted-source-view"; import { LoadingSpinner } from "#/components/shared/loading-spinner"; import { usePluginFileContent } from "#/hooks/query/use-plugin-file-content"; interface PluginFilesSectionProps { /** Plugin directory on the agent-server (`path`/`install_path`). */ basePath: string; /** File paths relative to `basePath`, as reported by the agent-server. */ files: string[]; } /** * "Files" section of the plugin detail modal: the plugin's file tree with an * inline viewer for the selected file. Clicking the selected file again * deselects it and closes the viewer. */ export function PluginFilesSection({ basePath, files, }: PluginFilesSectionProps) { const { t } = useTranslation("openhands"); const [selectedPath, setSelectedPath] = React.useState(null); // Derived rather than synced: a refetch that drops the selected file cannot // leave a dangling selection. const effectivePath = selectedPath && files.includes(selectedPath) ? selectedPath : null; const { data: content, isLoading, isError, } = usePluginFileContent(effectivePath ? basePath : null, effectivePath); return (

{t(I18nKey.COMMON$FILES)}

setSelectedPath((prev) => (prev === path ? null : path)) } />
{effectivePath ? (

{effectivePath}

{isLoading ? (
) : isError ? (

{t(I18nKey.FILES$LOAD_ERROR)}

) : content?.kind === "binary" ? (

{t(I18nKey.FILES$BINARY_FALLBACK)}

) : ( )}
) : null}
); }