Skip to content

Keep transcript headings stable while the paragraph below them streams - #336

Merged
TheGreatAxios merged 1 commit into
release/tui-bugfixesfrom
cl-5559-fix-markdown-heading-shake-during-live-render
Aug 7, 2026
Merged

Keep transcript headings stable while the paragraph below them streams#336
TheGreatAxios merged 1 commit into
release/tui-bugfixesfrom
cl-5559-fix-markdown-heading-shake-during-live-render

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Renders a streaming markdown row's body as two stacked renderers once a heading has closed: a "frozen" one holding everything through that heading, and a "live" one holding the still-streaming tail
  • Most rows have no closed heading yet, so they keep painting through a single renderer, same as before this fix — the split only allocates a second renderer once a heading actually closes
  • The frozen half is non-streaming and is never handed new content while the tail keeps growing, so it is never asked to re-highlight once written
  • The boundary is derived from the row's own text, tracking fence state (both fence characters, matching-or-longer closers only, a closing line may carry no trailing text per CommonMark) so a # line inside a fenced code block — a shell or Python comment, for instance — is never read as a heading. Also recognizes an ATX heading indented up to 3 spaces, per CommonMark.

On using the library's own parser instead of a hand-rolled scan: checked this directly before writing the regex. MarkdownRenderable's block/token state (_parseState, _blockStates) is underscore-prefixed and not part of its declared public API, and the module that builds it (renderables/markdown-parser.js, which exports parseMarkdownIncremental) has no entry in the package's exports map — it cannot be imported through any supported path, only by reaching into an internal module the package does not expose. No usable public surface exists, so the boundary is derived locally instead.

Verification

  • bun run typecheck, bun run build, and bun test all exit 0 (4339 pass, 0 fail)
  • src/tui-opentui/markdown-rows.test.ts adds:
    • a mid-stream test that samples the heading's painted span after every delta with no settle wait (the settle-then-assert pattern the rest of the file uses cannot see a transient re-highlight). Against the unfixed renderer this test fails, reproducing the shake directly: the heading's span reverts to literal "### Title" on a delta. Against this fix it stays stable across every delta.
    • an object-identity test confirming the frozen renderer is the same instance, with unchanged content, before and after the tail grows
    • a row with no closed heading still paints through a single renderer (no wasted allocation)
    • fence/heading edge cases: a # comment inside a fence (with and without a heading before it), unmatched fence lengths and characters (4 not closed by 3, 3 closed by 4, backtick/tilde never cross-close), a closing-fence-shaped line carrying trailing text (does not close, per CommonMark), 3-space fence indent recognized and 4-space not, an unclosed fence at end of input, and an indented heading
    • paragraph-directly-before-list spacing and ten-item ordered-list marker width, both unchanged from the pre-fix renderer

Layout parity with the pre-fix renderer is verified for headings, lists (including a list directly under a heading or a paragraph with no blank line, and ten-item ordered lists), paragraphs, and tables.

Closes CL-5559

@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

CL-5559

@TheGreatAxios
TheGreatAxios changed the base branch from main to release/tui-bugfixes August 7, 2026 04:20
@TheGreatAxios
TheGreatAxios force-pushed the cl-5559-fix-markdown-heading-shake-during-live-render branch from fdb0a7f to 6f266c1 Compare August 7, 2026 04:56
The markdown renderer's default block mode merges a heading into
the same raw chunk as the paragraph that follows it. Every keystroke
of that paragraph changes the merged chunk's raw text, so the
heading's already-settled markup re-highlights too, flickering while
the rest of the message keeps streaming in.

Render a streaming row's markdown body as two stacked renderers when
a heading has closed: a "frozen" one holding everything through the
last such heading, marked non-streaming and never handed new content
again, and a "live" one holding the still-growing tail. Most rows
have no closed heading yet, so they keep painting through a single
renderer as before; the split only ever falls at a settled heading
boundary, never at a paragraph, list, or fence boundary, so
non-heading layout is unchanged.

Checked whether `@opentui/core` exposes a public surface for this
boundary instead of scanning lines by hand: `MarkdownRenderable`'s
own block/token state (`_parseState`, `_blockStates`) is
underscore-prefixed and not part of its declared public API, and the
module that builds it (`renderables/markdown-parser.js`) has no
subpath in the package's `exports` map, so it cannot be imported at
all through the supported entry points. No such surface exists, so
the boundary is derived locally, tracking fence state (both fence
characters, matching-or-longer closers only, a closing line may
carry no trailing text per CommonMark) so a `#` line inside a fenced
code block — a shell or Python comment, for instance — is never read
as a heading.

Adds a mid-stream span-sampling test with no settle wait that
reproduces the shake directly against the unfixed renderer, plus
regression tests pinning list spacing, ordered-list marker width,
and fence/heading edge cases (unmatched fence lengths and
characters, a closing fence with trailing text, indentation limits,
an indented heading) across the change.
@TheGreatAxios
TheGreatAxios force-pushed the cl-5559-fix-markdown-heading-shake-during-live-render branch from 6f266c1 to 90c696d Compare August 7, 2026 05:12
@TheGreatAxios
TheGreatAxios merged commit 95b4267 into release/tui-bugfixes Aug 7, 2026
2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-5559-fix-markdown-heading-shake-during-live-render branch August 7, 2026 06:35
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