Keep transcript headings stable while the paragraph below them streams - #336
Merged
TheGreatAxios merged 1 commit intoAug 7, 2026
Conversation
TheGreatAxios
force-pushed
the
cl-5559-fix-markdown-heading-shake-during-live-render
branch
from
August 7, 2026 04:56
fdb0a7f to
6f266c1
Compare
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
force-pushed
the
cl-5559-fix-markdown-heading-shake-during-live-render
branch
from
August 7, 2026 05:12
6f266c1 to
90c696d
Compare
TheGreatAxios
deleted the
cl-5559-fix-markdown-heading-shake-during-live-render
branch
August 7, 2026 06:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#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 exportsparseMarkdownIncremental) has no entry in the package'sexportsmap — 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, andbun testall exit 0 (4339 pass, 0 fail)src/tui-opentui/markdown-rows.test.tsadds:"### Title"on a delta. Against this fix it stays stable across every delta.#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 headingLayout 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