forge_markdown_stream: The Rust Renderer That Makes LLM Markdown Feel Instant

A streaming terminal renderer that repairs broken fences, finalizes immutable blocks, and keeps complex markdown usable while tokens are still arriving.

8 min read • View on GitHub • More from antinomyhq

A split terminal screen shows chaotic markdown on the left and stabilized output on the right. The left side flickers with a broken fence, shifting table edges, and unfinished lines, while the right side locks completed blocks into a calm, readable layout. It explains the article’s central idea: streaming output becomes usable when only the unfinished part stays mutable.
The project’s core move is simple and strange at the same time. Freeze what is done, keep only the last block alive, and the terminal stops fighting the stream.
Key Takeaways

Streaming markdown is easy until the first broken fence lands in the middle of a table. Then the whole terminal starts wobbling. forge_markdown_stream is interesting because it refuses to treat that wobble as normal.

The problem is not markdown. It is markdown that is still arriving

Traditional renderers assume the document exists first and the screen comes second. That works for blog posts. It breaks for LLM output, where text arrives in fragments, lists grow line by line, and code fences often close late or not at all.

The obvious fix, reparsing the whole document on every token, is the wrong one. It feels simple, but it gets slower as the response gets longer, which is exactly the direction AI transcripts tend to go.

A wide scene shows two terminal rendering approaches side by side. One side keeps rebuilding the entire document as new text arrives, causing the layout to jitter. The other side keeps a stable stack of completed blocks while only the last block changes. It explains why streaming-first rendering avoids repeated full-document work.
The library’s advantage is not a prettier parser. It is the decision to stop touching finished blocks.

The trick: only the pending block stays mutable

That is the whole mental model. Incoming text is buffered, repaired if needed, parsed into blocks, and then split into two states: finalized blocks and one pending block. The finalized part is immutable. Only the pending block remains exposed to change.

The renderer behaves like a transaction log. New input can change the current entry, but committed blocks do not get rewritten.

That is why this project feels less like a parser and more like a small state machine with discipline. Once a block is safe, it gets locked away. The terminal stops repainting the past.

```text
> a code fence that is still attached to the last line of code ```
```

Repair is the quiet hero

The repair layer exists because LLMs do not reliably respect markdown boundaries. A closing fence can arrive glued to code, or a block can end in a way that looks complete to a human and malformed to a parser.

`repair_line` fixes that before parsing gets involved. The point is not cleverness. The point is forgiveness, which is exactly what streaming AI output needs.

A broken code fence is shown as a torn strip of paper being cleanly separated into two lines. On one side the fence token is attached to code, and on the other side the fence is split off so the block can close properly. It explains how the repair step restores structure before parsing.
A small repair step prevents a large visual mess later. That is a good trade in a streaming terminal.

Tables are where the terminal gets serious

This is the part that tells you the project is not a toy. Tables force the renderer to measure visible width, ignore ANSI escape sequences, shrink columns when space gets tight, and keep box-drawing characters aligned even after wrapping.

ApproachParsing modelStreaming behaviorFailure modeWhy it matters for LLM output
Traditional markdown parserWhole documentReparse everything on every updateJitter and O(n²) costFine for static docs, poor for live streams
Naive streaming loopRepeated full re-renderStill rewrites too much stateSlow as the response growsLooks responsive at first, then degrades
forge_markdown_streamBlock-aware incremental stateOnly the pending block changesRequires repair and careful boundariesKeeps long LLM replies stable in the terminal

The neat part is that the table logic cares about what the user can actually see, not the raw string length. That is the right mental model for terminal UI, and it is why ANSI-aware wrapping is so easy to get wrong.

A wide table is squeezed into a narrow terminal while its columns shrink proportionally and wrap cleanly. The visible borders stay aligned even as the content reflows. It explains why terminal tables need width measurement that ignores hidden styling codes.
Readable tables are a layout problem first and a formatting problem second.

ANSI-aware wrapping is harder than it looks

Colored terminal text is sneaky. The escape sequences should not count toward width, but they still have to survive the wrap. If the renderer gets that wrong, colors bleed, lines misalign, or the output turns visually noisy.

That is where `unicode-width`, `colored`, and the wrapping logic pull their weight. The implementation is not flashy. It just respects the fact that terminals are visual systems, not plain text boxes.

A single terminal sentence wraps across two lines while preserving its color state, with hidden ANSI styling implied by the structure rather than shown literally. The two lines remain visually continuous, showing that wrapped styling can survive line breaks without bleeding.
The hard part is not adding color. It is making color survive layout changes cleanly.

Why Rust fits this job

Rust is a natural fit here because the problem is full of places where correctness matters more than convenience. Width calculation, block state, repair rules, and incremental rendering all need explicit control.

The stack reflects that choice. `unicode-width` helps measure what the terminal actually displays. `syntect` handles syntax highlighting. `insta` gives the project a way to lock down edge cases with snapshots.

pub fn push(&mut self, chunk: &str) {
    self.buffer.push_str(chunk);
    while let Some(pos) = self.buffer.find('\n') {
        let line = self.buffer.drain(..=pos).collect::<String>();
        let repaired = repair_line(&line, &self.state);
        self.parser.push(&repaired);
    }
}

How it compares to static markdown libraries

Static libraries like `markdown-it` and `marked` are optimized for finished documents. That makes them great general-purpose tools. It also makes them a mismatch for a live stream that keeps changing under your cursor.

Library styleBest atStreaming fitTypical trade-off
Static parserCompleted documentsWeakEasy to use, but too much work repeats
Naive live re-renderSimple demosOkay at small scaleGets slower as content grows
forge_markdown_streamIncremental terminal outputStrongMore stateful, but far more stable

The architectural point is simple: if the user sees a completed block, the program should stop pretending that block is still provisional. That is the difference between a live system and a perpetual draft.

Forge needed a renderer, and the renderer reveals the product

Forge is a broader CLI for working across Git forges, but the renderer tells you how the project thinks about humans and agents. Output has to stay readable while it is still forming, because the CLI is meant to serve both people and software that think out loud.

I wanted one interface that worked the same way everywhere, for humans on the command line and for AI coding agents that need to interact with forges programmatically.

Andrew Nesbitt, Creator of Forge · Forge | Andrew Nesbitt
A hedcut-style portrait of Andrew Nesbitt rendered in black ink on a white background. It provides a human anchor for the Forge project and connects the renderer to its creator.

That makes `forge_markdown_stream` feel like infrastructure, not ornament. It exists so the rest of the CLI can stay calm while the stream is still in flight.

The bigger lesson

The real lesson here is about state management under uncertainty. If something is done, lock it. If something is not done, keep it honest. That idea is useful far beyond markdown.

Terminal UX for AI does not improve just by rendering faster. It improves when the renderer understands the difference between mutable and immutable output, and acts on that difference with discipline.