StreamingMd previously split in-flight text into one memoized
stable-prefix <Md> plus a re-parsed tail. Every time the stable boundary
advanced, the prefix string changed, its memo key missed, and the entire
prefix re-tokenized — O(blocks^2) parse work across a long reply — and
finding the boundary rescanned fence state from position 0 on every
delta.
Replace the monolithic prefix with an append-only array of settled
top-level blocks, each rendered as its own <Md> memoized on text that
never changes once committed (every block parses exactly once for the
life of the stream), plus a persistent scanner that keeps fence/math
open-state and scan position across deltas so each delta only scans the
newly arrived complete lines. Boundaries stay at "\n\n" outside
code/math fences; partial trailing lines stay in the tail until their
newline arrives so a growing "```" can't be misjudged.
Replaying a newline-terminated block-heavy stream at width 80 through a
real Ink render (one process per strategy so the parse LRU and GC
pressure can't cross-contaminate):
| Blocks | Appends | naive full Md | monolithic prefix | per-block |
|--------|---------|---------------|-------------------|-----------|
| 32 | 135 | 279.7 ms | 221.1 ms | 104.7 ms |
| 128 | 543 | 3.03 s | 2.39 s | 317.8 ms |
| 512 | 2175 | 56.28 s | 35.14 s | 3.05 s |
Remaining per-block cost is Ink layout of the growing tree, not parsing.
Bench: ui-tui/scripts/bench-streaming-md.tsx.