fix(desktop): isolate message render crashes from the root boundary
Streamdown runs our `preprocess` inside its own useMemo, and the user bubble runs `extractEmbeddedImages`/directive parsing inside theirs — so anything thrown while rendering one message (a regex/stack overflow on adversarial content) escapes to the ROOT error boundary and takes down the entire app, as seen in a reported `RangeError: Maximum call stack size exceeded` from a single message. Wrap both the assistant preprocess pipeline and the user-message directive passes in try/catch that degrade to the raw text. One bad message now renders plain instead of nuking the transcript.
This commit is contained in:
parent
547a014e7e
commit
b82eca2beb
@ -327,8 +327,23 @@ function shortLabel(type: HermesRefType, id: string): string {
|
|||||||
* inline chips. Embedded MEDIA images render below as a thumbnail row.
|
* inline chips. Embedded MEDIA images render below as a thumbnail row.
|
||||||
*/
|
*/
|
||||||
export function DirectiveContent({ text }: { text: string }) {
|
export function DirectiveContent({ text }: { text: string }) {
|
||||||
const { cleanedText, images } = useMemo(() => extractEmbeddedImages(text ?? ''), [text])
|
// Both passes run text through regexes; on pathological input they can throw
|
||||||
const segments = useMemo(() => hermesDirectiveFormatter.parse(cleanedText), [cleanedText])
|
// (or overflow) and, since this renders inside a useMemo under the message,
|
||||||
|
// bubble up to the root error boundary. Degrade gracefully to plain text.
|
||||||
|
const { cleanedText, images } = useMemo(() => {
|
||||||
|
try {
|
||||||
|
return extractEmbeddedImages(text ?? '')
|
||||||
|
} catch {
|
||||||
|
return { cleanedText: text ?? '', images: [] }
|
||||||
|
}
|
||||||
|
}, [text])
|
||||||
|
const segments = useMemo(() => {
|
||||||
|
try {
|
||||||
|
return hermesDirectiveFormatter.parse(cleanedText)
|
||||||
|
} catch {
|
||||||
|
return [{ kind: 'text', text: cleanedText }] as Unstable_DirectiveSegment[]
|
||||||
|
}
|
||||||
|
}, [cleanedText])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="whitespace-pre-line" data-slot="aui_directive-text">
|
<span className="whitespace-pre-line" data-slot="aui_directive-text">
|
||||||
|
|||||||
@ -57,7 +57,16 @@ const mathPlugin = createMemoizedMathPlugin({ singleDollarTextMath: true })
|
|||||||
// flush) with a tail-bounded repair — see lib/remend-tail.ts. Must stay
|
// flush) with a tail-bounded repair — see lib/remend-tail.ts. Must stay
|
||||||
// module-scope so the prop identity is stable across renders.
|
// module-scope so the prop identity is stable across renders.
|
||||||
function preprocessWithTailRepair(text: string): string {
|
function preprocessWithTailRepair(text: string): string {
|
||||||
return tailBoundedRemend(preprocessMarkdown(text))
|
// Streamdown runs `preprocess` inside its own useMemo, so anything thrown
|
||||||
|
// here escapes to the ROOT error boundary and takes down the whole app — a
|
||||||
|
// single adversarial message (e.g. content that overflows a regex/stack)
|
||||||
|
// shouldn't be able to do that. Degrade to the raw text instead; it still
|
||||||
|
// renders, just without our cosmetic normalization.
|
||||||
|
try {
|
||||||
|
return tailBoundedRemend(preprocessMarkdown(text))
|
||||||
|
} catch {
|
||||||
|
return text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized block splitter. Streamdown calls `parseMarkdownIntoBlocks` (a full
|
// Memoized block splitter. Streamdown calls `parseMarkdownIntoBlocks` (a full
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user