Skip to content

feat(components): render streaming markdown with @lobehub/streamdown - #1002

Merged
Leeeon233 merged 2 commits into
LodyAI:mainfrom
Innei:feat/lobehub-streamdown
Sep 26, 2026
Merged

Leeeon233 merged 2 commits into
LodyAI:mainfrom
Innei:feat/lobehub-streamdown

Conversation

@Innei

@Innei Innei commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Related issue

Refs #421

Problem / pressure

While a turn streams, Vercel streamdown re-runs remend + marked lexing over the whole message on every frame (#421, cause 1), and streamed text arrives in visible chunk-sized jumps. Its word-level animated mode wraps every word of the turn in a span, so it was never enabled. Much of its bundled UI was already switched off or overridden: Mermaid full-screen and pan/zoom were disabled, and index.css had to fight its utility classes with !important.

Summary

  • Two render paths. A streaming turn lazy-loads @lobehub/streamdown: finished blocks are frozen, only the open tail block is re-lexed, reveal commits are throttled to 48–96 ms, and only in-flight characters fade in (settled text has no spans). Finished text renders through plain react-markdown. The streaming renderer stays mounted 1 s after the stream ends so its buffered tail finishes revealing, then hands off.
  • Old Safari. The engine bundle contains lookbehind regex literals (a parse error before Safari 16.4 — the same constraint behind the remend patch and the lazy @pierre/diffs import). A failed lazy load resolves to the static renderer instead of breaking the turn.
  • Lody-owned UI replaces what Vercel streamdown bundled: markdown-code-block.tsx (container, copy button, token body), markdown-code-highlight.ts (tokens from the existing highlight worker, main-thread fallback, bounded cache), and markdown-mermaid-block.tsx (render, copy, SVG/PNG/source download through a new armed Menu in ui/armed-overlays.tsx). data-streamdown="…" attributes stay as the styling and test hooks.
  • Streaming code blocks ([Feature Request] Reduce streaming-time renderer energy: per-frame full-message markdown re-parse and Shiki re-highlight #421, cause 2, partially): tokenized per reveal commit instead of per frame; the previous highlight stays on screen with appended text plain until the worker answers; streaming prefixes are no longer written to the highlight cache.
  • remend HTML tags. @lobehub/streamdown completed the tail with remend's defaults, which would re-introduce the p<q truncation fixed in fix(components): preserve Markdown math document tails #843. Fixed upstream: 1.4.0 adds a remend option (lobehub/streamdown#5) and the renderer passes { htmlTags: false }. 1.4.0 is added to minimumReleaseAgeExclude because it was adopted on release day.
  • Fence completeness comes from a small remark plugin, so an unclosed streaming fence reports data-incomplete and an unclosed Mermaid fence stays a code block until it closes.
  • Math uses remark-math + rehype-katex directly with the options @streamdown/math passed. Dead Vercel-specific CSS, Tailwind content globs, and the packageExtensions shim are removed.
  • Docs: ai-gui/AGENTS.md rule, ai-gui/README.md, mermaid-diagram-rendering.md, and a bilingual note 2026-09-26-lobehub-streamdown.

Visual explanation

flowchart TD
  T[MarkdownRenderer text] --> N[normalizeTexMathDelimiters]
  N --> S{isStreaming, or ended less than 1 s ago?}
  S -- no --> RM[react-markdown<br/>whole document]
  S -- yes --> L[lazy import @lobehub/streamdown]
  L -- load failed --> RM
  L -- loaded --> SD[Streamdown<br/>smooth reveal, frozen blocks,<br/>remend tail htmlTags off, char fade]
  RM --> C[shared components]
  SD --> C
  C --> PRE[pre handler reads language, meta, unclosed flag]
  PRE --> CB[MarkdownFencedCodeBlock<br/>worker tokens]
  PRE --> DF[MarkdownDiffBlock]
  PRE --> MM[MarkdownMermaidBlock<br/>closed fences only]
Loading

Demo recording

Recorded in the locally built desktop app (apps/electron build of this branch, launched through the e2e Playwright Electron harness in an isolated profile). The agent is a synthetic ACP script that streams a fixed Markdown reply in 2–10 character chunks, so no model or user data is involved; the recording script stays under the ignored e2e/artifacts/. It shows the paced reveal and fade of in-flight text, the code block and table while streaming, the p<q line keeping the rest of the reply, the Mermaid fence staying a code block until it closes, and the handoff to static rendering (0 fade spans afterwards).

streaming-markdown-demo.mp4

Before / after

Before After
Every frame re-lexes the whole streaming message Finished blocks freeze; only the open tail is re-lexed, at 48–96 ms commits
Streamed text jumps in chunk-sized steps Paced reveal; in-flight characters fade in; settled text has no spans
Finished messages go through the streaming pipeline Finished messages render with plain react-markdown
Streaming code-block prefixes fill the highlight LRU Only finished blocks are cached; streaming blocks keep their last highlight
Code/Mermaid UI from Vercel streamdown, overridden in CSS Lody-owned components on the same data-streamdown hooks

Test plan

  • pnpm --filter @lody/components typecheck — pass (re-run after the 1.4.0 bump).
  • NODE_ENV=test vitest run in packages/components — 509 files, 4224 tests pass.
  • New/updated coverage in tests/markdown-streaming-reparse.test.ts: streaming→static handoff (fade spans only while streaming), unclosed-fence flag, and the p<q math tail on both paths. The streaming p<q case fails without the remend option and passes with it. tests/markdown-mermaid-fullscreen.test.tsx passes unchanged against the new block markup.
  • oxlint --type-aware on changed files (0 errors), Oxfmt, pnpm run lint:i18n, pnpm run docs check, pnpm check:public-boundary, and pnpm install --frozen-lockfile — pass.
  • Storybook: code highlighting through the worker on the theme probe; on the pre-rebase base also tables, KaTeX, Mermaid action bar and download menu, and the streaming demo (175 in-flight fade spans at peak, 0 after handoff).
  • Not done: renderer CPU re-measurement against the numbers in [Feature Request] Reduce streaming-time renderer energy: per-frame full-message markdown re-parse and Shiki re-highlight #421; old-Safari fallback not tested on a device; THIRD_PARTY_NOTICES.md not regenerated (the generator also rewrites ~9k lines of unrelated stale entries).

Context handoff

Original user prompt

Show original prompt
接一下 https://github.com/lobehub/streamdown 这个库

Shared conversation

Status: unavailable
Reason: Authored in a Claude Code CLI session, not a Lody conversation, so there is no shared Lody conversation link to publish.

Replace Vercel streamdown. A streaming turn lazy-loads @lobehub/streamdown
for tail-only re-lexing, paced reveal, and in-flight character fade; finished
text renders through react-markdown. A failed engine load (Safari < 16.4 cannot
parse its lookbehind literals) falls back to static rendering.

Code blocks, highlighting through the existing worker, and Mermaid
rendering/download move into Lody-owned components, keeping the
data-streamdown styling and test hooks. A pnpm patch keeps remend's HTML-tag
completion off so a TeX comparison such as p<q no longer drops the streamed
remainder.

Refs LodyAI#421

Model: claude-opus-5-5
… a patch

@lobehub/streamdown 1.4.0 exposes the remend options (lobehub/streamdown#5),
so the streaming renderer passes { htmlTags: false } directly and the pnpm
patch is removed. 1.4.0 is excluded from the minimum release age because it
was adopted on release day.

Refs LodyAI#421

Model: claude-opus-5-5
@Innei
Innei marked this pull request as ready for review September 26, 2026 09:31
@Leeeon233 Leeeon233 added status:pr-policy-bypass Maintainer exempted this PR from contribution policy and removed status:needs-pr-attention External PR needs contributor attention before review labels Sep 26, 2026
@Leeeon233

Copy link
Copy Markdown
Contributor

Thanks

@Leeeon233
Leeeon233 merged commit c65c647 into LodyAI:main Sep 26, 2026
8 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants