File size: 3,275 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | 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<string | null>(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 (
<section
data-testid="plugin-files-section"
className="flex min-w-0 flex-col gap-2"
>
<h3 className="text-sm font-medium text-white">
{t(I18nKey.COMMON$FILES)}
</h3>
<div className="min-w-0 rounded-lg border border-[var(--oh-border)] bg-[rgba(255,255,255,0.04)]">
<div className="max-h-48 overflow-y-auto custom-scrollbar">
<FileTreeView
paths={files}
selectedPath={effectivePath}
onSelectFile={(path) =>
setSelectedPath((prev) => (prev === path ? null : path))
}
/>
</div>
{effectivePath ? (
<div
data-testid="plugin-file-content"
className="flex h-60 min-w-0 flex-col border-t border-[var(--oh-border)]"
>
<p
className="truncate px-3 py-2 text-xs text-tertiary-alt"
title={effectivePath}
>
{effectivePath}
</p>
<div className="min-h-0 flex-1">
{isLoading ? (
<div className="flex h-full items-center justify-center">
<LoadingSpinner size="small" />
</div>
) : isError ? (
<p className="px-3 py-2 text-xs text-tertiary-light">
{t(I18nKey.FILES$LOAD_ERROR)}
</p>
) : content?.kind === "binary" ? (
<p className="px-3 py-2 text-xs text-tertiary-light">
{t(I18nKey.FILES$BINARY_FALLBACK)}
</p>
) : (
<HighlightedSourceView
path={effectivePath}
text={content?.text ?? ""}
/>
)}
</div>
</div>
) : null}
</div>
</section>
);
}
|