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")}
; }