+
= ({ chart }) => {
else transcriptMermaidSources.delete(element);
}}
style={{
- maxWidth: "70%",
- margin: "0 auto",
+ ...MERMAID_CONTAINER_STYLE,
["--diagram-zoom" as string]: `${diagramZoom}`,
- minHeight: `${MIN_HEIGHT}px`,
- ...(showPendingPlaceholder
- ? {
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- color: "var(--color-text-secondary)",
- fontStyle: "italic",
- }
- : {}),
+ ...(showPendingPlaceholder ? MERMAID_PENDING_STYLE : {}),
}}
// SECURITY AUDIT: displaySvg is produced by sanitizeMermaidSvg(), which strips
// active SVG/HTML content before insertion. React children are used for the
// pending placeholder so untrusted chart text is never rendered as HTML.
{...(!showPendingPlaceholder ? { dangerouslySetInnerHTML: { __html: displaySvg } } : {})}
>
- {showPendingPlaceholder ? "Rendering diagram..." : null}
+ {showPendingPlaceholder ? MERMAID_PENDING_TEXT : null}
{isModalOpen && (
diff --git a/src/browser/features/Messages/MermaidPendingFrame.tsx b/src/browser/features/Messages/MermaidPendingFrame.tsx
new file mode 100644
index 00000000000..426c9a1dcd6
--- /dev/null
+++ b/src/browser/features/Messages/MermaidPendingFrame.tsx
@@ -0,0 +1,41 @@
+import type { CSSProperties } from "react";
+
+// Shared by Mermaid's own pending state and the lazy-load fallback, so both reserve the
+// same box and nothing shifts when the Mermaid chunk arrives. Keep `mermaid` out of this
+// module: it is on the first load (T3, #5971).
+export const MERMAID_FRAME_STYLE: CSSProperties = {
+ position: "relative",
+ margin: "1em 0",
+ background: "var(--color-code-bg)",
+ borderRadius: "4px",
+ padding: "16px",
+};
+
+export const MERMAID_CONTAINER_STYLE: CSSProperties = {
+ maxWidth: "70%",
+ margin: "0 auto",
+ minHeight: "300px",
+};
+
+export const MERMAID_PENDING_STYLE: CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ color: "var(--color-text-secondary)",
+ fontStyle: "italic",
+};
+
+export const MERMAID_PENDING_TEXT = "Rendering diagram...";
+
+export function MermaidPendingFrame() {
+ return (
+
+
+ {MERMAID_PENDING_TEXT}
+
+
+ );
+}
diff --git a/src/browser/features/RightSidebar/ArtifactsTab/ArtifactViewer.tsx b/src/browser/features/RightSidebar/ArtifactsTab/ArtifactViewer.tsx
index 49890e73f86..51c2129a101 100644
--- a/src/browser/features/RightSidebar/ArtifactsTab/ArtifactViewer.tsx
+++ b/src/browser/features/RightSidebar/ArtifactsTab/ArtifactViewer.tsx
@@ -1,6 +1,7 @@
-import type React from "react";
+import React from "react";
import { Check, Copy, Download, ExternalLink } from "lucide-react";
-import { Mermaid } from "@/browser/features/Messages/Mermaid";
+import { LazyFeature } from "@/browser/components/LazyFeature/LazyFeature";
+import { MermaidPendingFrame } from "@/browser/features/Messages/MermaidPendingFrame";
import { useCopyToClipboard } from "@/browser/hooks/useCopyToClipboard";
import { isDesktopMode } from "@/browser/hooks/useDesktopTitlebar";
import type { ArtifactReadResult } from "@/common/orpc/schemas/artifacts";
@@ -20,6 +21,12 @@ import type { ArtifactAnnotationPick } from "./artifactAnnotation";
import type { ArtifactInteractionHandlers } from "./artifactInteractions";
import { useAgentBrowserAvailable } from "./useAgentBrowserAvailable";
+// Lazy so the `mermaid` package stays off the first load (T3, #5971): diagrams render only
+// when an artifact or message contains one.
+const Mermaid = React.lazy(() =>
+ import("@/browser/features/Messages/Mermaid").then((m) => ({ default: m.Mermaid }))
+);
+
// Every renderer here goes through React elements, so artifact content (agent-written,
// therefore untrusted) is always escaped. HTML and SVG only ever render inside
// SandboxedArtifactFrame; never route them through dangerouslySetInnerHTML.
@@ -192,7 +199,9 @@ export function ArtifactViewer(props: {
case "mermaid":
return (
-
+ }>
+
+
);
case "diff":