fix(tui): flush logical lines to scrollback so resize reflow stops splitting words and dropping indent#665
Draft
griffinwork40 wants to merge 2 commits into
Draft
fix(tui): flush logical lines to scrollback so resize reflow stops splitting words and dropping indent#665griffinwork40 wants to merge 2 commits into
griffinwork40 wants to merge 2 commits into
Conversation
…uards (#540, #541) Extend the node-pty scrollback harness (issue #541) with a mid-stream width resize so the width-resize scrollback-fragmentation class (#540 axis-2) is reproducible in CI, and add guards that lock the bug until the Stage-3 logical-line flush fixes it. Read-only diagnosis only; no src/ changes. Harness (tests/pty/): - constants.ts: PTY_RESIZE marker protocol (buildResizeMarker/findResizeMarker). An APC marker a scenario emits at the point it wants the geometry changed, carrying <cols>x<rows>; xterm ignores APC so it never perturbs the buffer. - harness.ts: IPtyProcess.resize(); runScenarioInPty detects the first marker, calls child.resize (SIGWINCH), records its byte offset, and on replay splits the captured stream: construct emulator @ old geometry, write pre-bytes, emulator.resize() to reflow, write post-bytes -- faithful to a real terminal reflowing scrollback on resize. Also exposes per-line isWrapped. - scenarios.ts: requestResize() driver helper (emit marker, await own 'resize'); PtyExpect.logicalSpan asserts soft-wrap rejoinability (tmux -J equivalent) via isWrapped counts, plus minSpanRows to prove a resize actually fired. Scenarios (existing 5 unchanged -- the resize is marker-driven, no API change): - width-resize-reflow-sanity (GREEN, stable): a line that fits wide re-wraps to 2 rows when narrowed -- certifies the resize handshake actually fires. - width-resize-fragment-narrow / -widen (GREEN now = RED guards): a logical line committed at one width, flushed to scrollback as hard rows, shows >=2 non-wrapped rows after a resize (not tmux -J-rejoinable). When PR 2 (#540 Stage 3 logical flush) lands, this drops to 1 and the assertion FAILS -- the signal to flip minNonWrappedRows -> maxNonWrappedRows: 1. Gates: pnpm test:pty 8/8 green; pnpm lint (tsc --noEmit) clean.
…ize fragmentation (#540) Axis 2 of the resize-wrapping bug: committed content was hard-wrapped to the width-at-commit and flushed into native scrollback as hard-newline rows, so a later width change re-fragmented scrolled-off content (the reported screenshot). Terminals only reflow their own soft-wraps, never app hard-newlines. Fix: the end-of-turn / disarm flush now archives the pending committed band as soft-wrappable LOGICAL lines instead of pre-hard-wrapped physical rows, so the terminal owns wrapping and reflows scrollback cleanly on any width change (docs/tui-resize-reflow.md "sound pattern (a)"). Mechanism -- retain, do NOT reconstruct (the flat committedBand: string[] loses logical boundaries, and reflowBandSplit re-wraps each row independently, so they cannot be recovered by re-joining rows): - Add committedBandMeta: BandRowMeta[], per-physical-row logical provenance index-aligned 1:1 with committedBand, maintained across commit / reflow / eviction / repin. - flushPendingCommittedBand: scrollbackFlushLines maps the pending physical rows back to logical lines (reading the full band+meta so a logical line STRADDLING the pending/painted boundary emits only its pending rows -- no scrollback duplication); buildScrollbackArchiveEscape writes each line at the bottom margin with autowrap ON + trailing newline, so the terminal wraps it and scrolls it into history. - The LIVE band keeps hard-wrapped physical rows for exact row math (unchanged); only the scrollback archival path emits logical lines. Flips PR 1's two RED guards (width-resize-fragment-narrow/-widen) to assert the rejoined property (maxNonWrappedRows: 1) -- they pass because the behavior changed, not because the assertions were weakened. Stacked on #621 (base afk/tmux-test-wrapping-fix). Gates: pnpm lint clean; pnpm test:pty 8/8; headless compositor 363/363; pnpm test:coverage green. NOT verified: the Stage-4 human real-terminal matrix (iTerm2 / Terminal.app / Ghostty) -- REQUIRED before merge; prior fixes in this code shipped green and broke in reality.
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.
Problem
Narrowing the terminal pane after long output has scrolled into native scrollback makes the terminal re-wrap that stored scrollback at the new width — splitting words mid-character (
missing→miss+ing) and dropping continuation-line indentation to column 0.Root cause
AFK hard-wraps committed output to physical rows for exact live-cursor frame math, then archives those pre-wrapped physical rows into native scrollback with a raw newline (
terminal-compositor.committed-band-commit.ts). Once bytes are in native scrollback the terminal owns them, so the in-memory reflow fix (band-reflow.ts, PR #386 /f75d8a4) cannot reach them. On a narrowing resize, the terminal's DECAWM autowrap hard-wraps each stored row again → mid-word splits + lost indent.band-reflow.ts:10-22anddocs/tui-resize-reflow.md:129-142document this as a known out-of-scope gap.Fix
Retain per-row logical provenance (
committedBandMeta/BandRowMeta) and, at the band-hold archive site, emit whole soft-wrappable logical lines viabuildScrollbackArchiveEscapeinstead of pre-wrapped physical rows. Native scrollback then holds one reflow-clean logical line per logical line, which any emulator — and tmux copy-mode (-J) — rewraps cleanly on resize.Gates (rebased onto current main, zero conflicts)
pnpm lint— 0 errorspnpm test:pty) — 8/8 (incl. the 3 axis-2 resize guards)MERGE GATE — human real-terminal validation required (do NOT merge until checked)
PTY tests replay an xterm emulator and cannot reproduce terminal-only DECAWM deferred-wrap artifacts; prior fixes in this path shipped green and broke on real terminals.
-J): unbroken logical lineDraft opened by afk. Branch rebases cleanly onto main (verified); rebase or "Update branch" before merge.