Skip to content

fix(render): restore trailing newlines stripped by markdown_to_ansi in stream#3043

Open
np6126 wants to merge 1 commit into
ultraworkers:mainfrom
np6126:fix/render-stream-trailing-newlines
Open

fix(render): restore trailing newlines stripped by markdown_to_ansi in stream#3043
np6126 wants to merge 1 commit into
ultraworkers:mainfrom
np6126:fix/render-stream-trailing-newlines

Conversation

@np6126
Copy link
Copy Markdown

@np6126 np6126 commented May 17, 2026

Summary

MarkdownStreamState::push calls markdown_to_ansi on each ready chunk, but render_markdown strips trailing newlines via trim_end() (see render.rs line 270). When the next streamed chunk arrives, its rendered output runs into the tail of the previous chunk in the terminal — adjacent paragraphs / list items / code blocks visibly merge into each other.

This PR restores the trailing newline(s) on the rendered output when the input chunk had them, matching the input's level of trailing whitespace (\n vs \n\n). flush() is unaffected since it returns the final chunk where trailing whitespace is irrelevant.

Diff

 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(&ready))
+    let rendered = renderer.markdown_to_ansi(&ready);
+    // markdown_to_ansi strips trailing newlines via trim_end(); restore them
+    // so consecutive streamed chunks are separated in the terminal output.
+    let trailing = if ready.ends_with("\n\n") { "\n\n" } else { "\n" };
+    Some(if rendered.ends_with('\n') {
+        rendered
+    } else {
+        rendered + trailing
+    })
 }

Repro

Any OpenAI-compatible streaming endpoint whose deltas are separated by \n reproduces this — most providers do. Visual repro: stream a multi-line response with numbered lists or fenced code blocks and observe items concatenating without a visible separator.

Test plan

  • cargo check --workspace clean
  • Existing render tests: cargo test -p rusty-claude-cli
  • Manual: stream a multi-line response through the CLI, observe newlines preserved between rendered chunks

…n stream

MarkdownStreamState::push calls markdown_to_ansi on each ready chunk, but
markdown_to_ansi (via the underlying markdown renderer) strips trailing
newlines with trim_end(). When the next streamed chunk arrives, its rendered
output runs into the tail of the previous chunk in the terminal, producing
visibly merged lines on streamed output.

Restore the trailing newline(s) on the rendered output when the input chunk
had them and the renderer ate them, matching the input's level of trailing
whitespace ('\n' vs '\n\n'). flush() is unaffected since it returns the final
chunk where trailing whitespace is irrelevant.

Repro: any OpenAI-compatible streaming endpoint where deltas arrive separated
by '\n' (most providers).
np6126 pushed a commit to np6126/tank-claw-os that referenced this pull request May 17, 2026
…am-drop

The two patches we ship in bootc/patches/ each contained an experimental
mix of changes that aren't all needed in this setup. Splitting them into
intent-aligned files makes it possible to drop them individually as
upstream merges happen, without having to surgically extract pieces.

- claw-fix-stream-newlines.patch: now contains only the trailing-newline
  restoration in MarkdownStreamState::push. Matches the change in
  ultraworkers/claw-code#3043 1:1, so the file drops out the moment
  that PR lands and CLAW_CODE_REF is bumped.

- claw-fix-openai-prefix-strip.patch: now contains only the "local/"
  routing-prefix additions in metadata_for_model and
  wire_model_for_base_url. The dead-code edit to strip_routing_prefix and
  the OpenRouter slug-preservation removal are gone — both were
  experimental and unneeded for this setup (we use "local/" prefix, not
  "openai/"). Matches ultraworkers/claw-code#3044 1:1, drops out when
  that PR lands.

- claw-add-think-block-filter.patch (new): isolates the <think>...</think>
  block filtering for thinking models (Qwen3 et al). Applied on top of the
  newlines patch since both touch MarkdownStreamState::push. Stays as a
  local patch until ultraworkers/claw-code#3045 resolves; the filter
  remains usable as a standalone patch against post-#3043 upstream
  because its context lines reference the newlines-applied state.

Containerfile applies the three patches in order: newlines → think → local-prefix.

Verified: all three apply cleanly to the pinned CLAW_CODE_REF and the
resulting tree compiles (cargo check --workspace).
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