Studio: render unclosed HTML/SVG fences as the tabbed preview too
End-to-end probe against Qwen3-0.6B caught a real renderer-engagement bug: the model emits a complete SVG body inside a ``` fence but forgets the closing ```. parseCodeFence is strict about the closing backticks, and the StreamdownBlock fallback only tried parseIncompleteCodeFence while isIncomplete was true. Once streaming finished, parseCodeFence returned null, the fallback was skipped, and HtmlSvgRenderer never mounted -- the user saw a plain code block where the new Preview / Code tabs should have been. Fix: always fall back to parseIncompleteCodeFence when parseCodeFence fails. The fallback is safe for non-fence content (returns null) and for non-HTML/SVG fences (HtmlSvgRenderer only engages on html/svg languages; everything else falls through to renderHighlightedCode). Adds a vitest case pinning the unclosed-final-fence path so a future regression is loud.
This commit is contained in:
parent
4f79a76397
commit
6e4d71d09f
2 changed files with 24 additions and 6 deletions
|
|
@ -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<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 10\">" +
|
||||
"<circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"orange\"/></svg>";
|
||||
expect(parseCodeFence(finalNoClose)).toBeNull();
|
||||
const fence = parseIncompleteCodeFence(finalNoClose);
|
||||
expect(fence).not.toBeNull();
|
||||
expect(fence?.language).toBe("svg");
|
||||
expect(fence?.source).toContain("<circle");
|
||||
expect(fence?.source).toContain("</svg>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Fence helpers", () => {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue