tui2: stop baking streaming wraps; reflow agent markdown (#8761)

Background
Streaming assistant prose in tui2 was being rendered with viewport-width
wrapping during streaming, then stored in history cells as already split
`Line`s. Those width-derived breaks became indistinguishable from hard
newlines, so the transcript could not "un-split" on resize. This also
degraded copy/paste, since soft wraps looked like hard breaks.

What changed
- Introduce width-agnostic `MarkdownLogicalLine` output in
`tui2/src/markdown_render.rs`, preserving markdown wrap semantics:
initial/subsequent indents, per-line style, and a preformatted flag.
- Update the streaming collector (`tui2/src/markdown_stream.rs`) to emit
logical lines (newline-gated) and remove any captured viewport width.
- Update streaming orchestration (`tui2/src/streaming/*`) to queue and
emit logical lines, producing `AgentMessageCell::new_logical(...)`.
- Make `AgentMessageCell` store logical lines and wrap at render time in
`HistoryCell::transcript_lines_with_joiners(width)`, emitting joiners so
copy/paste can join soft-wrap continuations correctly.

Overlay deferral
When an overlay is active, defer *cells* (not rendered `Vec<Line>`) and
render them at overlay close time. This avoids baking width-derived
wraps based on a stale width.

Tests + docs
- Add resize/reflow regression tests + snapshots for streamed agent
output.
- Expand module/API docs for the new logical-line streaming pipeline and
clarify joiner semantics.
- Align scrollback-related docs/comments with current tui2 behavior
(main draw loop does not flush queued "history lines" to the terminal).

More details
See `codex-rs/tui2/docs/streaming_wrapping_design.md` for the full
problem statement and solution approach, and
`codex-rs/tui2/docs/tui_viewport_and_history.md` for viewport vs printed
output behavior.
This commit is contained in:
Josh McKinney authored and GitHub committed 2026-01-05 18:37:58 -08:00
1 parent 771f1ca6ab
commit c92dbea7c1
13 files changed
+868 -325

No files matched your search

+33 -3
View File
@@ -123,12 +123,42 @@ impl App {
}
/// Close transcript overlay and restore normal UI.
///
/// Any history emitted while the overlay was open is flushed to the normal-buffer queue here.
///
/// Importantly, we defer *cells* (not rendered lines) so we can render them against the current
/// width on close and avoid baking width-derived wrapping based on an earlier viewport size.
/// (This matters if/when scrollback printing is enabled; `Tui::insert_history_lines` currently
/// queues lines without printing them during the main draw loop.)
pub(crate) fn close_transcript_overlay(&mut self, tui: &mut tui::Tui) {
let _ = tui.leave_alt_screen();
let was_backtrack = self.backtrack.overlay_preview_active;
if !self.deferred_history_lines.is_empty() {
let lines = std::mem::take(&mut self.deferred_history_lines);
tui.insert_history_lines(lines);
if !self.deferred_history_cells.is_empty() {
let cells = std::mem::take(&mut self.deferred_history_cells);
let width = tui.terminal.last_known_screen_size.width;
let mut lines: Vec<ratatui::text::Line<'static>> = Vec::new();
for cell in cells {
let mut display = cell.display_lines(width);
if display.is_empty() {
continue;
}
// Only insert a separating blank line for new cells that are not part of an
// ongoing stream. Streaming continuations should not accrue extra blank lines
// between chunks.
if !cell.is_stream_continuation() {
if self.has_emitted_history_lines {
display.insert(0, ratatui::text::Line::from(""));
} else {
self.has_emitted_history_lines = true;
}
}
lines.extend(display);
}
if !lines.is_empty() {
tui.insert_history_lines(lines);
}
}
self.overlay = None;
self.backtrack.overlay_preview_active = false;