fn normalize_csi_overwrites(
chunk: &mut Vec<u8>,
still_on_first_line: &mut bool,
at_col0: &mut bool,
)Expand description
Normalize CSI cursor-repositioning idioms that map cleanly onto the
existing \r-based overwrite model, converting them to a literal \r
byte so collapse_cr (which already understands \r) collapses them
too, with no new cases needed there. Runs on the raw chunk BEFORE
strip_ansi, which otherwise deletes CSI sequences unconditionally with
no overwrite-awareness — the “Phase γ” gap strip_ansi’s own doc
comment flags (“Cursor positioning escapes within the same line”).
See docs/specs/SPEC_TOOL_LOG_UNIVERSAL_ANIMATION_COLLAPSE_2026_07_27.md §A2.
still_on_first_line and at_col0 are both caller-owned state that
persists across calls for the lifetime of one reader loop — this
function is invoked once per PTY read, and a chunk boundary is not a
terminal state boundary: a read ending mid-line (no trailing \n/\r)
must carry “cursor is NOT at column 0” into the next call, otherwise a
standalone EL split across two reads ("prefix" in one chunk,
"\x1b[2Ksuffix" in the next) would see a freshly-reset at_col0 == true and wrongly convert that EL to \r, reproducing the exact
mid-line truncation bug this function exists to prevent, just moved
from a mid-chunk split to a cross-chunk-read split (reagent P1, PR
#2330, caught after the first, chunk-internal-only version of this fix).
still_on_first_line: true until the first \n is
seen in this tool call’s output, false forever after. Needed because
Windows ConPTY re-serializes a literal \r as CUP-to-home
(\x1b[H) rather than passing it through unchanged — verified
empirically via a1_e2e_static_label_then_delayed_cr_overwrite_collapses_to_one_line
capturing the raw PTY bytes: a child process writing \rdone produces
\x1b[?25l\x1b[Hdone...\x1b[?25h on the wire, not a literal \r. CUP
“home” (row 1, col 1) is unambiguous evidence of an overwrite ONLY when
nothing has scrolled the visual line off row 1 yet; once real multi-line
output exists, \x1b[H genuinely means “go back to the first of several
lines” (a multi-line redraw, spec §A3, deliberately out of scope) and
must NOT be treated as “restart the current line”.
Cases handled, chosen because each means exactly “move to the start of the (only) line so far” without needing true per-column tracking:
- CUP to row 1 col 1 (
\x1b[H,\x1b[1H,\x1b[1;1H, and the omitted-field variants of each), gated onstill_on_first_lineper above — the ConPTY-observed idiom. - CHA to column 1 (
\x1b[1G/\x1b[G, “cursor to column 1”) is literally “move to start of line” — identical to\r. Unix PTYs (and any tool not going through ConPTY’s re-serialization) may use this form directly. - EL (
\x1b[2Kerase whole line,\x1b[K/\x1b[0Kerase to end of line) is treated as “reset to start of line” too, but ONLY when the cursor is already known to be at column 0 (*at_col0) — i.e. the dominant real-world\r+ EL idiom (\r\x1b[2K<text>), where it’s genuinely redundant with the\ralready just emitted. A STANDALONE EL, mid-line (cursor NOT at column 0 — e.g.prefix\x1b[Ksuffix), is a real, different case: EL only erases from the cursor onward and never moves it, so collapsing it to\rthere would makecollapse_crtruncateprefixeven though EL never asked for that. That standalone case is left unconverted (falls through to the “unrecognized” branch below, sostrip_ansiremoves the escape byte-for-byte without touchingprefix) — a bug reagent caught on this exact line (PR #2330): the prior, unconditional version silently discardedprefixinprefix\x1b[Ksuffix\n.
CUB (\x1b[<n>D, “cursor back n columns”) is intentionally NOT handled
here — a faithful implementation needs true per-column tracking (a raw
byte position isn’t a faithful proxy once n varies per frame), which
collapse_cr’s line-offset model doesn’t have. Left as a documented
follow-up alongside CUU/multi-line redraw (spec §A3) rather than
guessed at with an approximation that could truncate the wrong amount.