authorgravatar for realahmedroach@gmail.comAhmed R. <realahmedroach@gmail.com> 2026-03-07 04:12:54-06:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-07 02:12:54-08:00
log0a902790cacb20d38c6f36fe331ac283a2c74b0f
tree34cf6c134bb2d78857282765c395c3bf877daaaf
parent0837c8b639f3fb54c155d824c308c6b03038fa9c
signature Signed by PGP key B5690EEEBB952194

Fix crash when rendering code blocks with unsupported languages (#266 & #210) (#279)

The app crashed when displaying code blocks with language identifiers not bundled in Shiki (e.g. "env" from .env file edits). The language string was cast to SupportedLanguages without validation, causing resolveLanguage to throw an unhandled error that killed the thread. Three layers of defense added: - getHighlighterPromise falls back to "text" when Shiki rejects - codeToHtml call wrapped in try/catch with "text" fallback - CodeHighlightErrorBoundary prevents component tree crashes Closes #266 Co-authored-by: Ahmed R <Ahmed@Ahmeds-MacBook-Pro.local>

1 files changed, 48 insertions(+), 13 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+48-13
......@@ -4,7 +4,7 @@ import {
44 type SupportedLanguages,
55} from "@pierre/diffs";
66import { CheckIcon, CopyIcon } from "lucide-react";
7import {
7import React, {
88 Children,
99 Suspense,
1010 isValidElement,
......@@ -28,6 +28,27 @@ import { resolveMarkdownFileLinkTarget } from "../markdown-links";
2828import { readNativeApi } from "../nativeApi";
2929import { preferredTerminalEditor } from "../terminal-links";
3030
31class CodeHighlightErrorBoundary extends React.Component<
32 { fallback: ReactNode; children: ReactNode },
33 { hasError: boolean }
34> {
35 constructor(props: { fallback: ReactNode; children: ReactNode }) {
36 super(props);
37 this.state = { hasError: false };
38 }
39
40 static getDerivedStateFromError() {
41 return { hasError: true };
42 }
43
44 override render() {
45 if (this.state.hasError) {
46 return this.props.fallback;
47 }
48 return this.props.children;
49 }
50}
51
3152interface ChatMarkdownProps {
3253 text: string;
3354 cwd: string | undefined;
......@@ -99,6 +120,14 @@ function getHighlighterPromise(language: string): Promise<DiffsHighlighter> {
99120 themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],
100121 langs: [language as SupportedLanguages],
101122 preferredHighlighter: "shiki-js",
123 }).catch((err) => {
124 highlighterPromiseCache.delete(language);
125 if (language === "text") {
126 // "text" itself failed — Shiki cannot initialize at all, surface the error
127 throw err;
128 }
129 // Language not supported by Shiki — fall back to "text"
130 return getHighlighterPromise("text");
102131 });
103132 highlighterPromiseCache.set(language, promise);
104133 return promise;
......@@ -179,10 +208,14 @@ function SuspenseShikiCodeBlock({
179208 }
180209
181210 const highlighter = use(getHighlighterPromise(language));
182 const highlightedHtml = useMemo(
183 () => highlighter.codeToHtml(code, { lang: language, theme: themeName }),
184 [code, highlighter, language, themeName],
185 );
211 const highlightedHtml = useMemo(() => {
212 try {
213 return highlighter.codeToHtml(code, { lang: language, theme: themeName });
214 } catch {
215 // If highlighting fails for this language, render as plain text
216 return highlighter.codeToHtml(code, { lang: "text", theme: themeName });
217 }
218 }, [code, highlighter, language, themeName]);
186219
187220 useEffect(() => {
188221 if (!isStreaming) {
......@@ -235,14 +268,16 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
235268
236269 return (
237270 <MarkdownCodeBlock code={codeBlock.code}>
238 <Suspense fallback={<pre {...props}>{children}</pre>}>
239 <SuspenseShikiCodeBlock
240 className={codeBlock.className}
241 code={codeBlock.code}
242 themeName={diffThemeName}
243 isStreaming={isStreaming}
244 />
245 </Suspense>
271 <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}</pre>}>
272 <Suspense fallback={<pre {...props}>{children}</pre>}>
273 <SuspenseShikiCodeBlock
274 className={codeBlock.className}
275 code={codeBlock.code}
276 themeName={diffThemeName}
277 isStreaming={isStreaming}
278 />
279 </Suspense>
280 </CodeHighlightErrorBoundary>
246281 </MarkdownCodeBlock>
247282 );
248283 },