fix(cli): translate LF→CRLF in streaming sink to stop rendered drift#125
Merged
emal-avala merged 1 commit intomainfrom Apr 15, 2026
Merged
fix(cli): translate LF→CRLF in streaming sink to stop rendered drift#125emal-avala merged 1 commit intomainfrom
emal-avala merged 1 commit intomainfrom
Conversation
Since #106 added the escape-key watcher, the terminal is held in raw mode for the entire duration of a streaming turn. In raw mode a bare `\n` moves the cursor down one row without returning to column 0, so every newline the LLM emits caused the next line to start at the column where the previous line ended. Over a multi-paragraph reply the text drifted off the right edge of the terminal. Route every print site inside the raw-mode window through two new helpers (`raw_print` / `raw_eprint`) that translate LF to CRLF before writing. The tui renderer (`render_tool_block`, `render_turn_summary`, etc.) already emits `\r\n` via `render_lines_to_ansi`, so it's untouched. The unaffected print paths outside the turn (setup wizard, shortcut panel, session summary) are also untouched. Eight unit tests cover the translation table, including the two edge cases that matter at runtime: existing `\r\n` must not become `\r\r\n`, and bare `\r` (used by the activity indicator to rewrite the status line) must pass through unchanged.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Merged
emal-avala
added a commit
that referenced
this pull request
Apr 15, 2026
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.
Diagnosis — print layer, not markdown
The drift you saw (every newline indented to the column where the previous line ended) is not a markdown rendering bug — markdown is never applied during streaming at all. It's a raw-mode terminal bug introduced by #106.
Since #106 landed `spawn_escape_watcher` to make Escape/Ctrl+C interrupt the agent, the watcher calls `crossterm::terminal::enable_raw_mode()` at the start of a turn and keeps it on until the turn completes. In raw mode a bare `\n` moves the cursor down one row without returning to column 0 — the tty layer no longer translates LF to CRLF for you. The streaming sink was still emitting `print!("{text}")` and `println!()` and `eprintln!(...)`, so every newline the LLM produced dropped the cursor onto the next row at the column where the previous line ended. Over a multi-paragraph reply the text walks off the right edge, which matches both examples you posted exactly.
The tui helpers (`render_tool_block`, `render_turn_summary`, `render_thinking_block`, `render_status_bar`) were already correct — they go through `render_lines_to_ansi` which writes `\r\n`. Only the raw `print!`/`println!`/`eprintln!` call sites inside `TerminalSink` and the turn-error branch in `run_repl` were broken.
Fix
Two private helpers at the top of `crates/cli/src/ui/repl.rs`:
Both normalize any existing `\r\n` first so translation is idempotent (no `\r\r\n`). Bare `\r` (used by `ActivityIndicator` to rewrite the status line) passes through unchanged.
All eight `print!`/`println!`/`eprintln!` sites inside the raw-mode window now route through them:
Print sites outside the turn (setup wizard, shortcut panel, session summary, rustyline output) are untouched — they run in cooked mode.
Tests
Added a `raw_print_tests` module with 8 unit tests covering the translation table, including the two edge cases that matter:
```
running 8 tests
test ui::repl::raw_print_tests::existing_crlf_is_preserved_not_doubled ... ok
test ui::repl::raw_print_tests::bare_lf_becomes_crlf ... ok
test ui::repl::raw_print_tests::cr_followed_by_text_then_lf ... ok
test ui::repl::raw_print_tests::empty_string ... ok
test ui::repl::raw_print_tests::lone_cr_is_not_touched ... ok
test ui::repl::raw_print_tests::mixed_lf_and_crlf ... ok
test ui::repl::raw_print_tests::text_without_newlines_is_unchanged ... ok
test ui::repl::raw_print_tests::multiple_newlines_all_translated ... ok
test result: ok. 8 passed
```
Manual test plan
Followup not in this PR
The other bug visible in your first paste — where three successive prompts `pwd` / `ls` / `fetch ...` got concatenated into `/doctoepwdlsfetch latest origin/main` and fed as a single input — looks like a separate rustyline / input-buffer issue, not a rendering issue. Worth filing separately; happy to pick it up next.