Skip to content

fix(Messages): stabilize markdown rendering for streaming messages and code blocks#71

Merged
remarkablemark merged 4 commits into
masterfrom
fix/messages
May 15, 2026
Merged

fix(Messages): stabilize markdown rendering for streaming messages and code blocks#71
remarkablemark merged 4 commits into
masterfrom
fix/messages

Conversation

@remarkablemark
Copy link
Copy Markdown
Member

@remarkablemark remarkablemark commented May 15, 2026

What is the motivation for this pull request?

Bug fix for markdown rendering issues in chat messages, particularly around code blocks and streaming content.

What is the current behavior?

  • Streaming inline markdown was unstable
  • Fenced code block regex had issues with nested/ambiguous fences
  • Markdown rendering had edge cases with complex nested structures

What is the new behavior?

  • Stabilized streaming inline markdown rendering
  • Fixed fenced-block regex to properly handle edge cases
  • Added proper handling for nested ambiguous markdown fences (renders as raw)
  • Extracted message processing utilities with comprehensive tests

plan.md

Checklist:

Summary:

Implement a streaming-safe inline renderer for assistant output that stabilizes pending prose without changing the existing full markdown renderer for committed content. The first version only targets inline code, bold, italic, and inline LaTeX. While an inline construct is still open, show its text content immediately but suppress the opening delimiter(s); once the closing delimiter arrives, re-render that segment with normal markdown formatting.

Key Changes:

- Add a streaming-specific inline preprocessing step in the assistant message render path, used only for `streamingMessage` and not for committed history.
- Keep committed assistant messages rendering through the current markdown pipeline in `src/components/Messages/Messages.tsx` and `src/components/Markdown/Markdown.tsx`.
- For the pending assistant tail, derive a display string that:
  - Detects unmatched inline code delimiters using backticks.
  - Detects unmatched bold/italic delimiters for `*`, `**`, `_`, and `__`.
  - Detects unmatched inline LaTeX delimiters matching the existing inline math extension behavior.
  - Preserves all stable prefix content unchanged.
  - For an open inline segment, removes the opening delimiter(s) from display but keeps the segment text visible as plain text until the closing delimiter arrives.
- Do not change fenced code block streaming behavior in this pass.
- Do not add predictive or look-ahead rendering; base behavior only on currently received text.
- Keep this logic local to rendering. Do not mutate persisted session messages, stream payloads, or Ollama message contents.
- Add a small helper module for inline streaming normalization so the rule set is testable outside the React tree.

Rendering Rules:

- Inline code:
  - Closed `` `code` `` renders normally.
  - Open `` `code`` displays as `code` in plain text.
- Bold:
  - Closed `**bold**` or `__bold__` renders normally.
  - Open `**bold` or `__bold` displays `bold` in plain text.
- Italic:
  - Closed `*italic*` or `_italic_` renders normally.
  - Open `*italic` or `_italic` displays `italic` in plain text.
- Inline LaTeX:
  - Closed inline math renders through the existing markdown/math path.
  - Open inline math displays only the inner text in plain text until closed.
- If multiple inline constructs are open, normalize each open suffix region conservatively and avoid trying to nest or repair malformed markdown beyond the supported delimiters.
- If parsing is ambiguous, prefer plain visible text over speculative formatting.

Implementation Notes:

- Introduce a helper such as `normalizeStreamingInlineMarkdown(content: string): string` under the markdown/message area.
- Use that helper only when rendering the pending assistant message.
- Leave user and system message rendering unchanged.
- Keep the existing `parseContent` code-block split behavior intact; apply inline normalization only to text segments, not fenced code block segments.
- Reuse the current markdown renderer after normalization rather than creating a second markdown engine.

Test Plan:

- Unit tests for the normalization helper:
  - Complete inline code stays unchanged.
  - Open inline code hides the opening backtick and keeps text visible.
  - Complete bold/italic stays unchanged.
  - Open bold/italic hides the opener and keeps text visible.
  - Complete inline LaTeX stays unchanged.
  - Open inline LaTeX hides the opener and keeps text visible.
  - Mixed stable prefix plus open inline suffix preserves the prefix exactly.
  - Multiple open delimiters degrade conservatively to visible plain text.
- Component tests in `src/components/Messages/Messages.test.tsx`:
  - Pending assistant streaming output uses normalized display for open inline constructs.
  - Committed assistant messages still render full markdown normally.
  - User/system messages are unaffected.
- Regression tests to ensure fenced code blocks still stream as they do today.

Assumptions:

- “Inline LaTeX” means the same delimiter behavior supported by the existing inline math extension; this implementation should follow that extension’s accepted inline form rather than invent new math syntax.
- The first pass is intentionally limited to inline code, bold, italic, and inline LaTeX.
- This pass does not include incremental assistant commits, safe split-point flushing, or debounce; those remain separate follow-up work.
The change is in `src/components/Markdown/Markdown.tsx`: the renderer
now creates a width-aware `Marked` instance per render, enables
`marked-terminal` reflow, and overrides the `text` renderer so nested
inline tokens inside list items are parsed correctly.

That removes the raw `**...**` markers that were leaking through in
ordered lists.
The problem was in `src/components/CodeBlock/CodeBlock.tsx`: the
fence regex only matched code blocks that started at column 0, so
indented fences inside lists were left in the markdown text and
rendered literally.

Changed it to match fences with leading indentation and to strip
that shared indentation from the block body before rendering.

`src/components/Messages/Messages.tsx` now uses the same
normalization when splitting assistant messages into text and
code segments.
- unambiguous fences render as code blocks
- ambiguous nested same-length fences render as literal raw text instead of broken code UI

For raw segments in `src/components/Messages/Messages.tsx`, the renderer:

- checks for a complete outer ```markdown... ``` wrapper
- strips that wrapper
- renders the inner content in CodeBlock with language="markdown"

So ambiguous nested markdown examples now show as markdown source without the
outer ```markdown fence clutter, while still avoiding reparsing as live markdown.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/components/CodeBlock/CodeBlock.tsx 100.00% <100.00%> (ø)
src/components/Markdown/Markdown.tsx 100.00% <100.00%> (ø)
src/components/Messages/Messages.tsx 100.00% <100.00%> (ø)
src/components/Messages/utils.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@remarkablemark remarkablemark self-assigned this May 15, 2026
@remarkablemark remarkablemark added the bug Something isn't working label May 15, 2026
@remarkablemark remarkablemark changed the title fix/messages fix: stabilize markdown rendering for streaming messages and code blocks May 15, 2026
@remarkablemark remarkablemark changed the title fix: stabilize markdown rendering for streaming messages and code blocks fix(Messages): stabilize markdown rendering for streaming messages and code blocks May 15, 2026
@remarkablemark remarkablemark merged commit 2364293 into master May 15, 2026
16 checks passed
@remarkablemark remarkablemark deleted the fix/messages branch May 15, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant