import { type ComponentType, type CSSProperties, type ReactNode, lazy, Suspense, useEffect, useState, useSyncExternalStore, } from "react"; import { cn } from "../lib/cn"; import { loadPierreDiffs, pierreDiffsRevision, subscribePierreDiffs, } from "../lib/pierre-diffs-loader"; import { useOpenGeniText } from "../messages/runtime"; /** Pierre `File` props subset we drive — the single-file, syntax-highlighted view * (Shiki), the read counterpart of `PatchDiff`. */ type FileComponent = ComponentType<{ file: { name: string; contents: string; lang?: string }; options?: { theme?: string | { dark: string; light: string }; themeType?: "dark" | "light"; overflow?: "scroll" | "wrap"; stickyHeader?: boolean; showLineNumbers?: boolean; }; disableWorkerPool?: boolean; className?: string; }>; export type PierreFileProps = { /** Target path (used for the header + language inference). */ path: string; /** The decoded text contents. */ contents: string; themeType?: "dark" | "light" | undefined; /** Shiki bundled theme names (dark/light) — derived from the host palette. */ theme?: { dark: string; light: string } | undefined; /** Disable Pierre's worker pool if its worker bundling fights the host bundler. */ disableWorkerPool?: boolean | undefined; /** Rendered while the (lazy) Pierre bundle loads. */ loading?: ReactNode | undefined; /** Rendered if `@pierre/diffs/react` is not installed / fails to import. */ fallback?: ReactNode | undefined; className?: string | undefined; }; // Lazy-load `@pierre/diffs/react`'s `File` so Shiki + the worker pool stay off the // critical path (and out of an SSR bundle) until a file is actually viewed. const LazyFile = lazy(async () => { const mod = (await loadPierreDiffs()) as { File: FileComponent }; return { default: mod.File }; }); /** * The Pierre-backed single-file VIEWER: Shiki-highlighted, language inferred from * the filename — the read complement of `PierreDiff`. Wired to `fs.read` so * clicking any file in the tree shows its contents (NOT a diff; no repo needed). * Falls back to a plain `
` when `@pierre/diffs` is absent / fails to import.
*/
export function PierreFile({
path,
contents,
themeType,
theme,
disableWorkerPool,
loading,
fallback,
className,
}: PierreFileProps) {
const [failed, setFailed] = useState(false);
const [ready, setReady] = useState(false);
// Probe the import once so a hard failure (peer missing) shows `fallback`
// rather than a Suspense boundary that never resolves.
const loaderRevision = useSyncExternalStore(
subscribePierreDiffs,
pierreDiffsRevision,
pierreDiffsRevision,
);
useEffect(() => {
let cancelled = false;
setFailed(false);
loadPierreDiffs().then(
() => {
if (!cancelled) setReady(true);
},
() => {
if (!cancelled) setFailed(true);
},
);
return () => {
cancelled = true;
};
}, [loaderRevision]);
const name = path.split("/").filter(Boolean).pop() ?? path;
if (failed) {
return (
{fallback ?? }
);
}
const options = {
overflow: "scroll" as const,
stickyHeader: true,
showLineNumbers: true,
...(theme
? { theme }
: {
theme: {
dark: "github-dark-high-contrast",
light: "github-light-high-contrast",
},
}),
themeType: themeType ?? "dark",
};
// Same shadow-DOM override vars as PierreDiff: pin the base background to the
// dock surface so the viewer reads as part of the panel, not a black seam.
const pierreVars = {
"--diffs-dark-bg": "var(--og-color-bg)",
"--diffs-light-bg": "var(--og-color-bg)",
"--diffs-bg-buffer-override": "var(--og-color-surface-1)",
"--diffs-bg-separator-override": "var(--og-color-surface-1)",
// Pierre's default (fg mixed 65% into bg) falls under 4.5:1 on the lifted
// graphite separator; quiet meta text keeps AA on every diff surface.
"--diffs-fg-number-override": "var(--og-color-fg-subtle)",
"--diffs-font-size": "var(--og-code-font-size)",
"--diffs-line-height": "var(--og-code-line-height)",
} as CSSProperties;
return (
}>
{/* `cacheKey` keys Pierre's worker-pool highlight cache on path+size so a
re-select of the same file is instant but an edited file re-highlights. */}
{ready ? (
) : (
(loading ?? )
)}
);
}
function PlainFile({ name, contents }: { name: string; contents: string }) {
return (
{contents}
);
}
function FileSkeleton() {
const t = useOpenGeniText();
return {t("pierreFile.loadingFile")};
}