docs(tui): clarify resize reflow invariants

This commit is contained in:
Felipe Coury
2026-04-21 19:26:14 -03:00
parent c135a5a38d
commit 653b607002
4 changed files with 56 additions and 1 deletions
+11
View File
@@ -10759,6 +10759,11 @@ impl ChatWidget {
self.bottom_pane.is_task_running() || self.is_review_mode
}
/// Return the markdown body width available to an active stream.
///
/// Streaming controllers render only the message body, while history cells add bullets,
/// gutters, or plan padding around that body. Callers pass the reserved columns for that
/// wrapper so live output uses the same width that finalized cells will use during reflow.
fn current_stream_width(&self, reserved_cols: usize) -> Option<usize> {
self.last_rendered_width.get().and_then(|width| {
if width == 0 {
@@ -10769,6 +10774,11 @@ impl ChatWidget {
})
}
/// Update resize-sensitive chat widget state after the terminal width changes.
///
/// The app calls this even when terminal resize reflow is disabled so live stream wrapping
/// remains consistent with the current viewport. Finalized transcript rebuilding stays gated at
/// the app layer.
pub(crate) fn on_terminal_resize(&mut self, width: u16) {
let had_rendered_width = self.last_rendered_width.get().is_some();
self.last_rendered_width.set(Some(width as usize));
@@ -10790,6 +10800,7 @@ impl ChatWidget {
self.stream_controller.is_some()
}
/// Whether a proposed-plan stream is active.
pub(crate) fn has_active_plan_stream(&self) -> bool {
self.plan_stream_controller.is_some()
}
+8
View File
@@ -1,3 +1,11 @@
//! Inserts finalized history rows into terminal scrollback.
//!
//! Codex uses the terminal scrollback itself for finalized chat history, so inserting a history
//! cell is an escape-sequence operation rather than a normal ratatui render. The legacy insertion
//! path may move the inline viewport when new history arrives. The resize-reflow insertion path is
//! deliberately separate because it is replaying already-owned transcript rows after a resize and
//! must preserve the viewport position chosen by the reflow draw path.
use std::fmt;
use std::io;
use std::io::Write;
+16
View File
@@ -24,6 +24,13 @@ use std::time::Instant;
use super::StreamState;
/// Shared source-retaining stream state for assistant and plan output.
///
/// `raw_source` is the markdown source that has crossed a newline boundary and can be rendered
/// deterministically. `rendered_lines` is the current-width render of that source. `enqueued_len`
/// tracks how much of that render has been offered to the commit queue, while `emitted_len` tracks
/// how much has actually reached history cells. Keeping those counters separate lets width changes
/// rebuild pending output without duplicating lines that are already visible.
struct StreamCore {
state: StreamState,
width: Option<usize>,
@@ -166,6 +173,11 @@ impl StreamCore {
);
}
/// Append newly rendered lines to the live queue without replaying already queued rows.
///
/// Width changes can make the rendered line count smaller than the previous queue boundary; in
/// that case the only safe option is rebuilding the queue from `emitted_len`, because slicing
/// from the stale `enqueued_len` would skip pending source.
fn sync_queue_to_render(&mut self) -> bool {
let target_len = self.rendered_lines.len().max(self.emitted_len);
if target_len < self.enqueued_len {
@@ -183,6 +195,10 @@ impl StreamCore {
true
}
/// Rebuild the pending live queue from the current render and current emitted position.
///
/// This is used when resize invalidates queued wrapping. It must never enqueue rows before
/// `emitted_len`, because those rows have already been inserted into terminal history.
fn rebuild_queue_from_render(&mut self) {
self.state.clear_queue();
let target_len = self.rendered_lines.len().max(self.emitted_len);
+21 -1
View File
@@ -444,9 +444,16 @@ fn set_panic_hook() {
#[derive(Clone, Debug)]
pub enum TuiEvent {
/// A terminal key event after focus, paste, and protocol bookkeeping has been handled.
Key(KeyEvent),
/// A bracketed paste payload normalized by the app layer before it reaches the composer.
Paste(String),
/// A terminal size notification that should be handled as resize-sensitive draw work.
///
/// Resize is separate from `Draw` so the app can run feature-gated pre-render logic without
/// changing the default draw path for scheduled frames.
Resize,
/// A scheduled repaint that does not necessarily correspond to a terminal size change.
Draw,
}
@@ -456,6 +463,8 @@ pub struct Tui {
event_broker: Arc<EventBroker>,
pub(crate) terminal: Terminal,
pending_history_lines: Vec<Line<'static>>,
// Reflowed transcript rows are written through a separate queue so the feature-on draw path can
// preserve the inline viewport while the legacy queue keeps its pre-feature insertion behavior.
pending_reflow_history_lines: Vec<Line<'static>>,
alt_saved_viewport: Option<ratatui::layout::Rect>,
#[cfg(unix)]
@@ -742,7 +751,8 @@ impl Tui {
/// Resize the inline viewport for the resize-reflow path.
///
/// Unlike the legacy draw path, this path does not scroll rows above the viewport when the
/// terminal shrinks. Resize reflow owns rebuilding those rows from transcript source.
/// terminal shrinks. Resize reflow owns rebuilding those rows from transcript source, so
/// scrolling here would move the viewport once and then replay history into the wrong row.
fn update_inline_viewport_for_resize_reflow(
terminal: &mut Terminal,
height: u16,
@@ -807,6 +817,11 @@ impl Tui {
Ok(is_zellij)
}
/// Write reflowed transcript rows without moving the inline viewport.
///
/// These rows come from a source-backed rebuild after Codex has already decided which
/// transcript cells should be visible at the new width. Sending them through the legacy
/// insertion helper would preserve old scroll behavior and undo the resize-reflow invariant.
fn flush_pending_reflow_history_lines(
terminal: &mut Terminal,
pending_reflow_history_lines: &mut Vec<Line<'static>>,
@@ -885,6 +900,11 @@ impl Tui {
})?
}
/// Draw a frame using the resize-reflow viewport and history insertion rules.
///
/// This is the feature-gated counterpart to `draw`. It intentionally skips
/// `pending_viewport_area`, whose cursor-position heuristic is part of the legacy path, and
/// instead lets transcript reflow rebuild scrollback before the frame is rendered.
pub fn draw_with_resize_reflow(
&mut self,
height: u16,