[GH-ISSUE #5588] fix(embed): streaming chat markdown answers visibly "jump" in size when model emits bullet lists #5182

Closed
opened 2026-06-05 14:52:30 -04:00 by yindo · 0 comments
Owner

Originally created by @gitofinho on GitHub (May 7, 2026).
Original GitHub issue: https://github.com/Mintplex-Labs/anything-llm/issues/5588

Originally assigned to: @shatfield4 on GitHub.

Summary

When the chat assistant streams a response, lines visibly jump in size for one frame whenever the model emits a hyphen-prefixed line (text\n-). The line above the dash briefly renders as an <h2> heading and reverts to a paragraph as soon as more content arrives. In responses with bullet lists this happens per bullet, so the chat text appears to repeatedly “grow” and shrink during streaming.

Affects the embed widget at embed/src/utils/chat/markdown.js. The same root cause exists in the main app at frontend/src/utils/chat/markdown.js — happy to file a sibling issue if preferred.

Reproduction

Token-by-token streaming exposes intermediate markdown-it parses. A single - arriving in a chunk turns the previous paragraph into a setext <h2>; the next chunk converts the same content back to <p>:

const md = require('markdown-it')({ html: false, typographer: true }).disable('list');

md.render('Title\n')            // → <p>Title</p>
md.render('Title\n-')           // → <h2>Title</h2>            ← jump (size + weight + margin)
md.render('Title\n- item')      // → <p>Title\n- item</p>      ← revert to paragraph

md.render('연수 일정\n-')          // → <h2>연수 일정</h2>          ← same in Korean responses
md.render('연수 일정\n- 4/15')     // → <p>연수 일정\n- 4/15</p>     ← revert

Embed widget setup amplifies the visual jump:

  • tailwind.config.js sets corePlugins.preflight = false, so browser-default heading sizes (h1=2em, h2=1.5em, h3=1.17em) leak through.
  • PromptReply re-renders on every textResponseChunk via dangerouslySetInnerHTML (embed/src/components/ChatWindow/ChatContainer/ChatHistory/PromptReply/index.jsx), so each transient parse is painted.

A short screen capture is easy to produce against any workspace whose responses include bullet lists; happy to attach one if useful.

Root cause

markdown-it’s lheading rule (setext headings: Title\n===<h1>, Title\n---<h2>) fires the moment a single - or = lands on the next line. In a streaming context, that single character is the very next chunk, so the previous paragraph flips into a heading for one render frame and flips back when the rest of the bullet body arrives. ATX headings (#, ##, …) are stable through the same streaming pattern and are not part of this issue.

Proposed fix

A one-line patch to the existing .disable() call in src/utils/chat/markdown.js:

- .disable("list");
+ .disable(["list", "lheading"]);

Behavior change is scoped to setext headings only:

  • Title\n=== no longer renders as <h1> (renders as paragraph + literal ===).
  • Title\n--- no longer renders as <h2> (renders as paragraph + horizontal rule once the third dash lands, which is the same end-state as a blank-line-separated --- and matches what users typically expect).
  • ATX (# Header, ## Section, …) is unchanged.

In practice, current LLM outputs we’ve sampled use ATX headings exclusively; we have not observed setext output from any major model. If preserving setext is still desired, a config-flag escape hatch would also work — happy to take direction.

Test approach — question before opening the PR

CONTRIBUTING.md asks for unit tests on bug fixes, but I notice this repo currently has no test runner wired up (no vitest/jest devDep, no test script in package.json, no *.test.* files). I’d like your preference before opening the PR:

  • (a) Bundle a minimal vitest setup with the fix — devDep + vitest.config.js + a small markdown.test.js.
  • (b) Add a single dependency-free scripts/streaming-render-spec.mjs that asserts via node and exits non-zero on failure. No new devDeps; runnable as node scripts/streaming-render-spec.mjs.
  • (c) Skip tests for this fix; treat the markdown-it transition table above as the regression spec.

Happy with whichever you’d rather maintain. I have all three forms ready and will open the PR in whichever shape you prefer.

Originally created by @gitofinho on GitHub (May 7, 2026). Original GitHub issue: https://github.com/Mintplex-Labs/anything-llm/issues/5588 Originally assigned to: @shatfield4 on GitHub. ## Summary When the chat assistant streams a response, lines visibly **jump in size for one frame** whenever the model emits a hyphen-prefixed line (`text\n-`). The line above the dash briefly renders as an `<h2>` heading and reverts to a paragraph as soon as more content arrives. In responses with bullet lists this happens *per bullet*, so the chat text appears to repeatedly “grow” and shrink during streaming. Affects the embed widget at [`embed/src/utils/chat/markdown.js`](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/src/utils/chat/markdown.js). The same root cause exists in the main app at `frontend/src/utils/chat/markdown.js` — happy to file a sibling issue if preferred. ## Reproduction Token-by-token streaming exposes intermediate `markdown-it` parses. A single `-` arriving in a chunk turns the previous paragraph into a setext `<h2>`; the next chunk converts the same content back to `<p>`: ```js const md = require('markdown-it')({ html: false, typographer: true }).disable('list'); md.render('Title\n') // → <p>Title</p> md.render('Title\n-') // → <h2>Title</h2> ← jump (size + weight + margin) md.render('Title\n- item') // → <p>Title\n- item</p> ← revert to paragraph md.render('연수 일정\n-') // → <h2>연수 일정</h2> ← same in Korean responses md.render('연수 일정\n- 4/15') // → <p>연수 일정\n- 4/15</p> ← revert ``` Embed widget setup amplifies the visual jump: - `tailwind.config.js` sets `corePlugins.preflight = false`, so browser-default heading sizes (h1=2em, h2=1.5em, h3=1.17em) leak through. - `PromptReply` re-renders on every `textResponseChunk` via `dangerouslySetInnerHTML` (`embed/src/components/ChatWindow/ChatContainer/ChatHistory/PromptReply/index.jsx`), so each transient parse is painted. A short screen capture is easy to produce against any workspace whose responses include bullet lists; happy to attach one if useful. ## Root cause `markdown-it`’s **`lheading`** rule (setext headings: `Title\n===` → `<h1>`, `Title\n---` → `<h2>`) fires the moment a single `-` or `=` lands on the next line. In a streaming context, that single character is the very next chunk, so the previous paragraph flips into a heading for one render frame and flips back when the rest of the bullet body arrives. ATX headings (`#`, `##`, …) are stable through the same streaming pattern and are not part of this issue. ## Proposed fix A one-line patch to the existing `.disable()` call in [`src/utils/chat/markdown.js`](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/src/utils/chat/markdown.js#L46): ```diff - .disable("list"); + .disable(["list", "lheading"]); ``` Behavior change is scoped to setext headings only: - `Title\n===` no longer renders as `<h1>` (renders as paragraph + literal `===`). - `Title\n---` no longer renders as `<h2>` (renders as paragraph + horizontal rule once the third dash lands, which is the same end-state as a blank-line-separated `---` and matches what users typically expect). - ATX (`# Header`, `## Section`, …) is unchanged. In practice, current LLM outputs we’ve sampled use ATX headings exclusively; we have not observed setext output from any major model. If preserving setext is still desired, a config-flag escape hatch would also work — happy to take direction. ## Test approach — question before opening the PR [CONTRIBUTING.md](https://github.com/Mintplex-Labs/anything-llm/blob/master/CONTRIBUTING.md) asks for unit tests on bug fixes, but I notice this repo currently has no test runner wired up (no `vitest`/`jest` devDep, no `test` script in `package.json`, no `*.test.*` files). I’d like your preference before opening the PR: - **(a)** Bundle a minimal `vitest` setup with the fix — devDep + `vitest.config.js` + a small `markdown.test.js`. - **(b)** Add a single dependency-free `scripts/streaming-render-spec.mjs` that asserts via `node` and exits non-zero on failure. No new devDeps; runnable as `node scripts/streaming-render-spec.mjs`. - **(c)** Skip tests for this fix; treat the markdown-it transition table above as the regression spec. Happy with whichever you’d rather maintain. I have all three forms ready and will open the PR in whichever shape you prefer.
yindo closed this issue 2026-06-05 14:52:30 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Mintplex-Labs/anything-llm#5182