Describe the bug
When streaming responses from an OpenAI-compatible API (such as Grok or custom proxy gateways), paragraphs and code blocks sometimes lose their leading and trailing newlines, causing them to merge directly into the previous output line without proper line breaks in the terminal.
For example, a response might render as:
最简单的Python入门例子:Hello World!1. 打开记事本 (instead of having double newlines before 1.)
And:
粘贴进去(全部内容就是它):╭─ python (instead of having double newlines before the code block)
Root Cause Analysis
The bug is located in TerminalRenderer::render_markdown inside rust/crates/rusty-claude-cli/src/render.rs:
#[must_use]
pub fn render_markdown(&self, markdown: &str) -> String {
let normalized = normalize_nested_fences(markdown);
let mut output = String::new();
// ...
for event in Parser::new_ext(&normalized, Options::all()) {
self.render_event(...);
}
output.trim_end().to_string() // <--- HERE
}
During streaming, MarkdownStreamState::push parses intermediate chunks (split at boundaries using find_stream_safe_boundary).
- The
ready chunk (e.g., up to an empty line representing a paragraph break) correctly ends with trailing newlines (e.g., \n\n generated by paragraph boundaries).
- However,
renderer.markdown_to_ansi(&ready) calls render_markdown(), which unconditionally performs .trim_end() on the rendered string.
- This strips all trailing newlines (including the structural
\n\n representing paragraph/block separators).
- Since these newlines are stripped from the output chunk and the original
ready text has been drained from self.pending, the newlines are completely lost from the terminal print.
- When the next chunk (e.g. list items or code block starts) is printed, it is outputted directly appended to the previous text without any line breaks.
Suggested Fix
Introduce a stream-friendly rendering method in TerminalRenderer that preserves trailing whitespace/newlines during stream chunk generation, and use it inside MarkdownStreamState::push.
For example:
- In
TerminalRenderer (inside rust/crates/rusty-claude-cli/src/render.rs):
#[must_use]
pub fn render_markdown_stream(&self, markdown: &str) -> String {
let normalized = normalize_nested_fences(markdown);
let mut output = String::new();
let mut state = RenderState::default();
let mut code_language = String::new();
let mut code_buffer = String::new();
let mut in_code_block = false;
for event in Parser::new_ext(&normalized, Options::all()) {
self.render_event(
event,
&mut state,
&mut output,
&mut code_buffer,
&mut code_language,
&mut in_code_block,
);
}
output // Do not call trim_end() here
}
#[must_use]
pub fn markdown_to_ansi_stream(&self, markdown: &str) -> String {
self.render_markdown_stream(markdown)
}
- In
MarkdownStreamState::push:
#[must_use]
pub fn push(&mut self, renderer: &TerminalRenderer, delta: &str) -> Option<String> {
self.pending.push_str(delta);
let split = find_stream_safe_boundary(&self.pending)?;
let ready = self.pending[..split].to_string();
self.pending.drain(..split);
Some(renderer.markdown_to_ansi_stream(&ready)) // Use the stream-specific method
}
This keeps MarkdownStreamState::flush using the standard markdown_to_ansi (which trims trailing newlines at the very end of the stream), while correctly outputting structural paragraph and code block newlines during streaming.
Describe the bug
When streaming responses from an OpenAI-compatible API (such as Grok or custom proxy gateways), paragraphs and code blocks sometimes lose their leading and trailing newlines, causing them to merge directly into the previous output line without proper line breaks in the terminal.
For example, a response might render as:
最简单的Python入门例子:Hello World!1. 打开记事本(instead of having double newlines before1.)And:
粘贴进去(全部内容就是它):╭─ python(instead of having double newlines before the code block)Root Cause Analysis
The bug is located in
TerminalRenderer::render_markdowninsiderust/crates/rusty-claude-cli/src/render.rs:During streaming,
MarkdownStreamState::pushparses intermediate chunks (split at boundaries usingfind_stream_safe_boundary).readychunk (e.g., up to an empty line representing a paragraph break) correctly ends with trailing newlines (e.g.,\n\ngenerated by paragraph boundaries).renderer.markdown_to_ansi(&ready)callsrender_markdown(), which unconditionally performs.trim_end()on the rendered string.\n\nrepresenting paragraph/block separators).readytext has been drained fromself.pending, the newlines are completely lost from the terminal print.Suggested Fix
Introduce a stream-friendly rendering method in
TerminalRendererthat preserves trailing whitespace/newlines during stream chunk generation, and use it insideMarkdownStreamState::push.For example:
TerminalRenderer(insiderust/crates/rusty-claude-cli/src/render.rs):MarkdownStreamState::push:This keeps
MarkdownStreamState::flushusing the standardmarkdown_to_ansi(which trims trailing newlines at the very end of the stream), while correctly outputting structural paragraph and code block newlines during streaming.