Fenced code blocks don't render incrementally during streaming
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- react, typescript
Línea de trabajo
Reproduce el problema con una cadena Markdown en crecimiento que se pase a Streamdown y, después, examina el comportamiento de parseMarkdownIntoBlocks y marked's Lexer.lex con fences sin cerrar. Sigue hasIncompleteCodeFence, Block, useIsCodeFenceIncomplete y parseIncompleteMarkdown para comprobar si los tokens de código parciales llegan a la ruta de renderizado de código. La tarea está terminada cuando el contenido de código cercado se renderiza incrementalmente durante el streaming sin hacer retroceder el renderizado de prosa.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Fenced code blocks don't render incrementally during streaming
Description
When streaming AI responses token-by-token, fenced code blocks (triple backtick) appear to render all at once rather than incrementally as the markdown string grows. Text outside code blocks streams smoothly with visible character-by-character rendering, but the moment a code fence opens, the content inside appears to buffer until the block is complete, then dumps visually.
Environment
streamdown: 2.5.0@streamdown/code: 1.1.1- React 18
- Using
<Streamdown animated isAnimating plugins={{ cjk, code, math, mermaid }}>
Reproduction
- Pass a growing markdown string to
<Streamdown>that includes a fenced code block - The string updates token-by-token (verified via
console.log— length increments: 4, 8, 24, 26, 34, 58... smoothly up to 860+) - Prose text before and after the code fence streams beautifully with the
animatedcaret - The code block content appears all at once or in large chunks
Minimal usage:
<Streamdown animated isAnimating plugins={{ cjk, code, math, mermaid }}>
{streamingMarkdownString}
</Streamdown>
Where streamingMarkdownString grows incrementally and contains:
Some text before...
```typescript
// This content appears to dump all at once
function example() {
return "hello";
}
```
What we investigated
1. Transport is not the issue
Console logging confirms the raw string updates smoothly, token-by-token. The buffering is in the rendering layer, not the data transport.
2. parseMarkdownIntoBlocks uses marked's Lexer.lex()
parseMarkdownIntoBlocks calls Lexer.lex(markdown, { gfm: true }) and iterates tokens. When the lexer encounters a fenced code block, it emits a single code token only after the closing fence is found. While the fence is open (no closing backticks yet), the raw content likely falls into a different token type or gets merged differently.
Key question: Does marked's Lexer.lex() emit a code-type token for a partial fence (no closing backticks), or does it fall back to a paragraph token? If it treats unclosed fences as paragraphs, the block would render as text during streaming and then suddenly swap to a code block when the closing fence arrives — which is exactly what we observe.
3. isIncomplete infrastructure exists
The Streamdown component checks for unclosed code fences (via hasIncompleteCodeFence) on the last block and passes isIncomplete={true} to the Block component. The useIsCodeFenceIncomplete hook exposes this. However, if marked's lexer doesn't produce a code-type token for the unclosed fence, the isIncomplete flag may not trigger the code block rendering path.
4. chatbot.ai-sdk.dev achieves smooth code block streaming
The reference chatbot at chatbot.ai-sdk.dev (which uses Streamdown via MessageResponse) renders code blocks incrementally during streaming. This suggests either a configuration difference or that AI SDK's useChat delivers data in a way that interacts with Streamdown differently than a custom WebSocket stream.
5. parseIncompleteMarkdown prop
We noticed this prop defaults to false. Does remend processing affect how unclosed code fences are rendered during streaming? Does enabling parseIncompleteMarkdown={true} change the behavior?
Our setup (not using AI SDK)
We're NOT using AI SDK's useChat. We have a custom WebSocket streaming pipeline that delivers text tokens from a container running Claude CLI. The growing markdown string is accumulated in a Zustand store and passed as children to Streamdown. The string IS raw markdown with fences.
Questions
- Does
parseMarkdownIntoBlocksproduce incremental block updates for unclosed code fences? - Is there a prop or configuration we're missing to enable incremental code block rendering?
- Does
chatbot.ai-sdk.devuse any additional configuration beyond what's in its open-sourceMessageResponsewrapper? - Could
remendpreprocessing be the key — does it close unclosed fences before the lexer sees them?
- Lenguaje dominante
- TypeScript
- Estrellas
- 5.7k
- Forks
- 313
- Merge medio
- 1 d 19 h
- PR fusionados (30 d)
- 24
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de vercel/streamdown
-
remend: unclosed `$$` in a table cell is closed on a new line when an earlier line has mathPosiblemente ocupada @orinnz la tomó hace 14 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
vercel/streamdown#652 ·
Los mantenedores suelen responder en 1 día
-
The word-fade engine ships with every app, whether or not it animatesPosiblemente ocupada @xyrolle la tomó hoy. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 1/100
vercel/streamdown#681 ·
Los mantenedores suelen responder en 1 día
-
Mermaid's UI ships in the main bundle although the chart is lazy-loadedPosiblemente ocupada @xyrolle la tomó hoy. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 20/100
vercel/streamdown#679 ·
Los mantenedores suelen responder en 1 día
-
CJK autolink boundary handling keeps `**` in the URL when a bold URL is followed by CJK punctuationPosiblemente ocupada @mikemikimike la tomó hace 2 días. Abiertobug
Dificultad 3/5 Medio día Aptitud para principiantes 50/100
vercel/streamdown#672 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Animation: CJK paragraphs only animate their first batch, and queued words pop in when isAnimating turns falsePosiblemente ocupada @sleitor la tomó hace 1 día. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
vercel/streamdown#669 ·
Los mantenedores suelen responder en 1 día
Todos los issues de vercel/streamdown
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
polarsource/polar#15580 ·
Los mantenedores suelen responder en 1 día
-
accepted bug via-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 69/100
pingdotgg/t3code#20931 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug/1-unconfirmed kind/bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 83/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
ranxianglei/billion-context#3583 ·
Los mantenedores suelen responder en 1 día