diff --git a/apps/fabro-web/app/routes/run-sandbox.test.tsx b/apps/fabro-web/app/routes/run-sandbox.test.tsx
index 4fd17ad62..fa933a96b 100644
--- a/apps/fabro-web/app/routes/run-sandbox.test.tsx
+++ b/apps/fabro-web/app/routes/run-sandbox.test.tsx
@@ -58,6 +58,12 @@ mock.module("@pierre/trees", () => ({ themeToTreeStyles: () => ({}) }));
mock.module("@pierre/theme/pierre-dark", () => ({ default: {} }));
mock.module("@pierre/diffs/react", () => ({
File: () =>
,
+ Virtualizer: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
+ WorkerPoolContextProvider: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
}));
const { default: RunSandbox, formatBytesAsMemory, normalizeSandboxMode } =
diff --git a/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.test.tsx b/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.test.tsx
index 617fc7bb5..b14b609c8 100644
--- a/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.test.tsx
+++ b/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.test.tsx
@@ -25,6 +25,9 @@ let filesState: FilesQueryState = makeEmptyFilesState();
let lastFileArgs: { id: string | undefined; path: string | null | undefined } | null = null;
let fileState: FileQueryState = makeEmptyFileState();
let lastTreeOptions: CapturedTreeOptions | null = null;
+const pierreFileCalls: Array<{ file: { name: string; contents: string; cacheKey?: string } }> = [];
+const providerCalls: any[] = [];
+const virtualizerCalls: any[] = [];
function makeEmptyFilesState(): FilesQueryState {
return {
@@ -81,11 +84,22 @@ mock.module("@pierre/theme/pierre-dark", () => ({
}));
mock.module("@pierre/diffs/react", () => ({
- File: (props: { file: { name: string; contents: string } }) => (
-
- {props.file.contents}
-
- ),
+ File: (props: { file: { name: string; contents: string; cacheKey?: string } }) => {
+ pierreFileCalls.push(props);
+ return (
+
+ {props.file.contents}
+
+ );
+ },
+ Virtualizer: (props: any) => {
+ virtualizerCalls.push(props);
+ return {props.children}
;
+ },
+ WorkerPoolContextProvider: (props: any) => {
+ providerCalls.push(props);
+ return {props.children}
;
+ },
}));
const filesystemPanelModule = await import("./filesystem-panel");
@@ -119,6 +133,9 @@ beforeEach(() => {
lastFilesArgs = null;
lastFileArgs = null;
lastTreeOptions = null;
+ pierreFileCalls.length = 0;
+ providerCalls.length = 0;
+ virtualizerCalls.length = 0;
filesState = makeEmptyFilesState();
fileState = makeEmptyFileState();
});
@@ -380,6 +397,38 @@ describe("FilesystemPanel render", () => {
&& node.props["data-file-name"] === "README.md",
);
expect(previews).toHaveLength(1);
+ expect(providerCalls).toHaveLength(1);
+ expect(virtualizerCalls).toHaveLength(1);
+ expect(pierreFileCalls[0].file.cacheKey).toContain(
+ "fabro-sandbox-file:run_1:/README.md:",
+ );
+ });
+
+ test("renders an empty text file without mounting Pierre File", () => {
+ filesState = {
+ ...makeEmptyFilesState(),
+ data: { data: [{ name: ".dockerenv", is_dir: false, size: 0 }] },
+ };
+ fileState = {
+ ...makeEmptyFileState(),
+ data: new TextEncoder().encode("").buffer as ArrayBuffer,
+ };
+ const renderer = renderPanel();
+ act(() => {
+ lastTreeOptions?.onSelectionChange?.([".dockerenv"]);
+ });
+ expect(lastFileArgs).toEqual({
+ id: "run_1",
+ path: "/.dockerenv",
+ });
+ expect(findByTestId(renderer, "pierre-file")).toHaveLength(0);
+ const titles = renderer.root.findAll(
+ (node) =>
+ node.type === "p" &&
+ Array.isArray(node.children) &&
+ node.children.includes("Empty file"),
+ );
+ expect(titles).toHaveLength(1);
});
test("renders binary fallback when file contents contain a null byte", () => {
diff --git a/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.tsx b/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.tsx
index 1ea9f935b..835920a19 100644
--- a/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.tsx
+++ b/apps/fabro-web/app/routes/run-sandbox/filesystem-panel.tsx
@@ -15,17 +15,23 @@ import {
import {
FileTree,
useFileTree,
- useFileTreeSelection,
} from "@pierre/trees/react";
import { themeToTreeStyles } from "@pierre/trees";
import pierreDark from "@pierre/theme/pierre-dark";
-import { File } from "@pierre/diffs/react";
+import {
+ File,
+ Virtualizer,
+ WorkerPoolContextProvider,
+ type FileContents,
+} from "@pierre/diffs/react";
import type { SandboxFileEntry } from "@qltysh/fabro-api-client";
import { useSandboxFile, useSandboxFiles } from "../../lib/queries";
import { ApiError } from "../../lib/api-client";
import { EmptyState, ErrorState, LoadingState } from "../../components/state";
import { SECONDARY_BUTTON_CLASS, Tooltip } from "../../components/ui";
+import { workerFactory } from "../../lib/pierre-diffs-worker";
+import { stringHash } from "../run-files/cache-keys";
export const DEFAULT_DIR = "/";
@@ -34,6 +40,8 @@ export const DEFAULT_DIR = "/";
// download-only state.
export const TEXT_PREVIEW_BYTE_LIMIT = 256 * 1024;
const BINARY_SAMPLE_BYTES = 8 * 1024;
+const pierrePoolOptions = { workerFactory };
+const pierreHighlighterOptions = { theme: "pierre-dark" };
type TreeThemeStyle = CSSProperties & Record<`--${string}`, string | number>;
@@ -114,6 +122,18 @@ export function formatFileSize(bytes: number | undefined): string | null {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
}
+export function sandboxFileCacheKey({
+ runId,
+ path,
+ contents,
+}: {
+ runId: string;
+ path: string;
+ contents: string;
+}): string {
+ return `fabro-sandbox-file:${runId}:${path}:${stringHash(contents)}`;
+}
+
interface BuiltTreeInputs {
paths: string[];
fileEntries: Map;
@@ -486,8 +506,13 @@ function PreviewPane({
-
);
@@ -495,10 +520,14 @@ function PreviewPane({
function PreviewBody({
preview,
+ runId,
fileName,
+ filePath,
}: {
preview: PreviewState;
+ runId: string;
fileName: string;
+ filePath: string;
}) {
if (preview.status === "loading") {
return (
@@ -538,10 +567,39 @@ function PreviewBody({
);
}
+ if ((preview.text ?? "").length === 0) {
+ return (
+
+
+
+ );
+ }
+ const file: FileContents = {
+ name: fileName,
+ contents: preview.text ?? "",
+ cacheKey: sandboxFileCacheKey({
+ runId,
+ path: filePath,
+ contents: preview.text ?? "",
+ }),
+ };
return (
-
+
+
+
+
+
);
}