Skip to content

feat: replace react-markdown with streamdown for jitter-free streaming - #17

Merged
quiet-node merged 3 commits into
mainfrom
worktree-unified-moseying-starfish
Apr 2, 2026
Merged

feat: replace react-markdown with streamdown for jitter-free streaming#17
quiet-node merged 3 commits into
mainfrom
worktree-unified-moseying-starfish

Conversation

@quiet-node

Copy link
Copy Markdown
Owner

Summary

  • Replace react-markdown + remark-gfm with streamdown (Vercel's streaming-aware markdown renderer) to eliminate chat bubble height jitter during LLM token streaming
  • Streamdown splits content into blocks and memoizes completed ones, so only the last in-progress paragraph re-renders on each token
  • Add isStreaming prop through ChatBubble -> MarkdownRenderer to toggle between streaming and static render modes
  • Disable link safety modal so links render as native <a> with href, target="_blank", and rel="noopener noreferrer"
  • Update CLAUDE.md architecture description and security documentation

Test plan

  • All 142 tests pass (bun run test)
  • 100% code coverage maintained (bun run test:coverage)
  • Full validation passes with zero warnings/errors (bun run validate-build)
  • Link test restored with href, rel, and target assertions
  • XSS prevention tests pass (script tags, iframes, event handlers, javascript: URLs all stripped)
  • Manual QA: verify streaming text appears without bubble jitter
  • Manual QA: verify links in AI responses are clickable

🤖 Generated with Claude Code

quiet-node and others added 3 commits April 1, 2026 22:28
react-markdown re-parses the entire accumulated text on every token,
producing different DOM trees for incomplete markdown and causing
chat bubble height to oscillate during fast streaming. Streamdown
(by Vercel) splits content into blocks and memoizes completed ones,
so only the last in-progress block re-renders on each token.

- Swap react-markdown + remark-gfm for streamdown
- Add isStreaming prop through ChatBubble → MarkdownRenderer
- Remove failed height-ratchet approach from ConversationView
- Update tests for Streamdown's element rendering conventions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Code review found four issues:

1. Links rendered as dead <button> elements (linkSafety modal was
   intercepting). Disable linkSafety so Streamdown renders native
   <a> tags with href, target="_blank", and rel="noopener noreferrer".
   Restore the href assertion in the link test.

2. CLAUDE.md architecture section said "DOMPurify sanitization" but
   DOMPurify was never used. Update to reflect actual renderer
   (Streamdown with rehype-sanitize).

3. Security rationale JSDoc was deleted in the streamdown migration.
   Add documentation of Streamdown's XSS protection model.

4. Add inline comments explaining controls={false} and
   linkSafety={{ enabled: false }} prop choices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
mermaid (transitive dep via streamdown) pins lodash-es to 4.17.23 which
has known Code Injection and Prototype Pollution vulnerabilities. Override
to >=4.18.0 which is outside the vulnerable range (<=4.17.23). mermaid
is tree-shaken and never loaded at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node
quiet-node merged commit 1f41737 into main Apr 2, 2026
3 checks passed
@quiet-node
quiet-node deleted the worktree-unified-moseying-starfish branch April 2, 2026 04:14
quiet-node added a commit that referenced this pull request Apr 10, 2026
#17)

* feat: replace react-markdown with streamdown for jitter-free streaming

react-markdown re-parses the entire accumulated text on every token,
producing different DOM trees for incomplete markdown and causing
chat bubble height to oscillate during fast streaming. Streamdown
(by Vercel) splits content into blocks and memoizes completed ones,
so only the last in-progress block re-renders on each token.

- Swap react-markdown + remark-gfm for streamdown
- Add isStreaming prop through ChatBubble → MarkdownRenderer
- Remove failed height-ratchet approach from ConversationView
- Update tests for Streamdown's element rendering conventions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: restore functional links and update security documentation

Code review found four issues:

1. Links rendered as dead <button> elements (linkSafety modal was
   intercepting). Disable linkSafety so Streamdown renders native
   <a> tags with href, target="_blank", and rel="noopener noreferrer".
   Restore the href assertion in the link test.

2. CLAUDE.md architecture section said "DOMPurify sanitization" but
   DOMPurify was never used. Update to reflect actual renderer
   (Streamdown with rehype-sanitize).

3. Security rationale JSDoc was deleted in the streamdown migration.
   Add documentation of Streamdown's XSS protection model.

4. Add inline comments explaining controls={false} and
   linkSafety={{ enabled: false }} prop choices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: override lodash-es to 4.18.0+ to resolve audit vulnerabilities

mermaid (transitive dep via streamdown) pins lodash-es to 4.17.23 which
has known Code Injection and Prototype Pollution vulnerabilities. Override
to >=4.18.0 which is outside the vulnerable range (<=4.17.23). mermaid
is tree-shaken and never loaded at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 10, 2026
#17)

* feat: replace react-markdown with streamdown for jitter-free streaming

react-markdown re-parses the entire accumulated text on every token,
producing different DOM trees for incomplete markdown and causing
chat bubble height to oscillate during fast streaming. Streamdown
(by Vercel) splits content into blocks and memoizes completed ones,
so only the last in-progress block re-renders on each token.

- Swap react-markdown + remark-gfm for streamdown
- Add isStreaming prop through ChatBubble → MarkdownRenderer
- Remove failed height-ratchet approach from ConversationView
- Update tests for Streamdown's element rendering conventions

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: restore functional links and update security documentation

Code review found four issues:

1. Links rendered as dead <button> elements (linkSafety modal was
   intercepting). Disable linkSafety so Streamdown renders native
   <a> tags with href, target="_blank", and rel="noopener noreferrer".
   Restore the href assertion in the link test.

2. CLAUDE.md architecture section said "DOMPurify sanitization" but
   DOMPurify was never used. Update to reflect actual renderer
   (Streamdown with rehype-sanitize).

3. Security rationale JSDoc was deleted in the streamdown migration.
   Add documentation of Streamdown's XSS protection model.

4. Add inline comments explaining controls={false} and
   linkSafety={{ enabled: false }} prop choices.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: override lodash-es to 4.18.0+ to resolve audit vulnerabilities

mermaid (transitive dep via streamdown) pins lodash-es to 4.17.23 which
has known Code Injection and Prototype Pollution vulnerabilities. Override
to >=4.18.0 which is outside the vulnerable range (<=4.17.23). mermaid
is tree-shaken and never loaded at runtime.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 11, 2026
#17)

* feat: replace react-markdown with streamdown for jitter-free streaming

react-markdown re-parses the entire accumulated text on every token,
producing different DOM trees for incomplete markdown and causing
chat bubble height to oscillate during fast streaming. Streamdown
(by Vercel) splits content into blocks and memoizes completed ones,
so only the last in-progress block re-renders on each token.

- Swap react-markdown + remark-gfm for streamdown
- Add isStreaming prop through ChatBubble → MarkdownRenderer
- Remove failed height-ratchet approach from ConversationView
- Update tests for Streamdown's element rendering conventions

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: restore functional links and update security documentation

Code review found four issues:

1. Links rendered as dead <button> elements (linkSafety modal was
   intercepting). Disable linkSafety so Streamdown renders native
   <a> tags with href, target="_blank", and rel="noopener noreferrer".
   Restore the href assertion in the link test.

2. CLAUDE.md architecture section said "DOMPurify sanitization" but
   DOMPurify was never used. Update to reflect actual renderer
   (Streamdown with rehype-sanitize).

3. Security rationale JSDoc was deleted in the streamdown migration.
   Add documentation of Streamdown's XSS protection model.

4. Add inline comments explaining controls={false} and
   linkSafety={{ enabled: false }} prop choices.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: override lodash-es to 4.18.0+ to resolve audit vulnerabilities

mermaid (transitive dep via streamdown) pins lodash-es to 4.17.23 which
has known Code Injection and Prototype Pollution vulnerabilities. Override
to >=4.18.0 which is outside the vulnerable range (<=4.17.23). mermaid
is tree-shaken and never loaded at runtime.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant