Skip to content

🤖 fix: stop code-block highlight flashing while streaming#3480

Open
ammar-agent wants to merge 1 commit into
mainfrom
markdown-highlight-sx56
Open

🤖 fix: stop code-block highlight flashing while streaming#3480
ammar-agent wants to merge 1 commit into
mainfrom
markdown-highlight-sx56

Conversation

@ammar-agent
Copy link
Copy Markdown
Collaborator

Summary

Fixes a bug where syntax highlighting flashes (plain ↔ colored, repeatedly) while a markdown code block is still streaming. The renderer now shows correct partial highlighting that grows in place: lines that are already finalized stay colored while only the still-growing tail renders as plain text.

Background

CodeBlock highlights asynchronously via the Shiki worker, keyed on the full code string. During streaming the typewriter grows code on nearly every frame, so the async highlight is always a chunk behind. The previous getCurrentHighlightedCodeBlockLines gate did an exact highlighted.code === code match and returned null (→ plain text) whenever the highlight was stale. The all-or-nothing fallback dropped the whole block back to uncolored text on each chunk, producing a rapid plain → colored → plain flash. The gate existed to avoid showing stale text/height, but it threw away all coloring to do so.

Implementation

Replaced the exact-match gate with a streaming-aware per-line resolver, resolveCodeBlockLines, in MarkdownComponents.tsx:

  • Streaming only appends, so the previous highlight is almost always a prefix of the current code.
  • A line terminated by a newline is final — Shiki tokenizes left-to-right, so a completed line's colors never change based on later content. Those prefix lines are kept highlighted.
  • Only the still-growing tail (the unterminated last highlighted line and anything past the highlighted prefix) falls back to plain text until the next highlight lands.
  • Exact match → fully highlighted; language/theme change or a non-prefix code reset → plain (safe fallback, unchanged behavior).

CodeBlock now decides highlight-vs-plain per line. Height is always driven by the current plainLines, so there's no height jump. The SECURITY AUDIT invariant is preserved: dangerouslySetInnerHTML is only used for trusted Shiki HTML, plain text goes through a <code> child.

HighlightedCode.tsx (tool result panes) is intentionally left unchanged — those panes replace content in-place rather than streaming appends, so the prefix assumption does not apply there.

Validation

  • Rewrote the resolver unit tests around behavioral branches (finalized-prefix reuse, no-reuse of an incomplete last line, theme-change fallback, non-prefix fallback, exact match) rather than re-asserting copy.
  • make static-check, make typecheck, make lint, and bun test src/browser/features/Messages/MarkdownComponents.test.tsx (17 pass / 0 fail) all green.

Risks

Low and scoped to the streaming code-block render path. Worst case for a logic slip is a transient stale/plain line that self-corrects on the next highlight (the same failure mode the old gate guarded against). No change to highlighting output, security boundary, or non-streaming/settled rendering.


Generated with mux • Model: anthropic:claude-opus-4-8 • Thinking: xhigh • Cost: $3.28

@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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