diff --git a/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx b/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx index 7a89654c11..c74a3d0495 100644 --- a/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx +++ b/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx @@ -203,6 +203,23 @@ describe("parseIncompleteCodeFence", () => { it("returns null when the block is not a fence at all", () => { expect(parseIncompleteCodeFence("just text")).toBeNull(); }); + + it("recovers a final-but-never-closed fence (small LLMs drop the closing ```)", () => { + // Regression: live probe against Qwen3-0.6B caught the model emitting a + // complete SVG body but no closing ```. parseCodeFence rejects (strict + // ``` ... ``` shape), so the StreamdownBlock fallback must still find the + // fence via parseIncompleteCodeFence even after streaming completes -- or + // HtmlSvgRenderer never mounts and the user sees a plain code block. + const finalNoClose = + "```svg\n" + + ""; + expect(parseCodeFence(finalNoClose)).toBeNull(); + const fence = parseIncompleteCodeFence(finalNoClose); + expect(fence).not.toBeNull(); + expect(fence?.language).toBe("svg"); + expect(fence?.source).toContain(""); + }); }); describe("Fence helpers", () => { diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 7201c9956b..b728f5302c 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -222,13 +222,14 @@ function renderHighlightedCode(props: BlockProps, codeFence: CodeFenceInfo) { function StreamdownBlock(props: BlockProps) { const hasMermaidFence = props.content.includes("```mermaid"); const mermaidSource = getMermaidSource(props.content); - // parseCodeFence requires a closing ```; while the fence is still - // streaming we fall through to parseIncompleteCodeFence so HtmlSvgRenderer - // can mount with isIncomplete=true and lock the Code tab on partial - // HTML/SVG fences (the advertised stream-in behaviour). + // parseCodeFence requires a closing ```; we fall back to + // parseIncompleteCodeFence both while the fence is still streaming AND + // when a finished reply forgot to emit the closing ``` (small local LLMs + // routinely drop it). Without the second fallback the HtmlSvgRenderer + // never mounts on an unclosed final message and the reply degrades to a + // plain code block. const codeFence = - parseCodeFence(props.content) ?? - (props.isIncomplete ? parseIncompleteCodeFence(props.content) : null); + parseCodeFence(props.content) ?? parseIncompleteCodeFence(props.content); if (props.isIncomplete && hasMermaidFence) { return (