fix: contain markdown render crashes with an error boundary (#1586)

Streamdown bundles Mermaid and renders ```mermaid blocks itself; a
diagram it can't parse throws during render and, with no boundary,
white-screens the whole review page. Wrap the markdown render so any
crash falls back to the raw text instead.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-22 12:54:05 -07:00 • committed by GitHub
parent f54fc154ca
commit 609e5551c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,4 +1,4 @@
import { memo, useMemo } from "react"; import { Component, memo, useMemo } from "react";
import { Streamdown } from "streamdown"; import { Streamdown } from "streamdown";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import "streamdown/styles.css"; import "streamdown/styles.css";
@ -76,6 +76,46 @@ const STREAMDOWN_COMPONENTS = {
const SHIKI_THEME: ["github-light", "github-dark"] = ["github-light", "github-dark"]; const SHIKI_THEME: ["github-light", "github-dark"] = ["github-light", "github-dark"];
interface BoundaryProps {
content: string;
children: ReactNode;
}
interface BoundaryState {
failed: boolean;
key: string;
}
// Streamdown bundles Mermaid and renders ```mermaid blocks itself; a diagram it
// can't parse throws during render and, with no boundary, white-screens the
// whole page. Contain it and fall back to the raw markdown text.
class MarkdownErrorBoundary extends Component<BoundaryProps, BoundaryState> {
state: BoundaryState = { failed: false, key: this.props.content };
static getDerivedStateFromError(): Partial<BoundaryState> {
return { failed: true };
}
static getDerivedStateFromProps(
props: BoundaryProps,
state: BoundaryState
): Partial<BoundaryState> | null {
if (props.content !== state.key) return { failed: false, key: props.content };
return null;
}
render(): ReactNode {
if (this.state.failed) {
return (
<pre className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-sans text-[color:var(--ui-text)]">
{this.props.content}
</pre>
);
}
return this.props.children;
}
}
export const Markdown = memo(function Markdown({ export const Markdown = memo(function Markdown({
content, content,
isLive = false, isLive = false,
@ -99,17 +139,19 @@ export const Markdown = memo(function Markdown({
return ( return (
<div className="min-w-0 max-w-full text-[13px] leading-6 break-words [overflow-wrap:anywhere] [&_.streamdown]:text-[color:var(--ui-text)]"> <div className="min-w-0 max-w-full text-[13px] leading-6 break-words [overflow-wrap:anywhere] [&_.streamdown]:text-[color:var(--ui-text)]">
<Streamdown <MarkdownErrorBoundary content={content}>
mode={isLive ? "streaming" : "static"} <Streamdown
parseIncompleteMarkdown={isLive} mode={isLive ? "streaming" : "static"}
isAnimating={isLive} parseIncompleteMarkdown={isLive}
animated={isLive ? STREAMDOWN_ANIMATED : false} isAnimating={isLive}
shikiTheme={SHIKI_THEME} animated={isLive ? STREAMDOWN_ANIMATED : false}
className="streamdown-agent min-w-0 max-w-full" shikiTheme={SHIKI_THEME}
components={components} className="streamdown-agent min-w-0 max-w-full"
> components={components}
{content} >
</Streamdown> {content}
</Streamdown>
</MarkdownErrorBoundary>
</div> </div>
); );
}); });