Only a visible-frame capture positions its rows absolutely, so only that frame can be damaged by a terminal of the wrong size. A `full=1` body is linear scrollback closed by a relative cursor move, which is relative precisely so the browser's row count need not match the pane's, and a `history` body is the byte stream, which carries no row alignment to protect. The geometry comparison ran on all three, so it fired most often on the one response it cannot help: `_fullHistoryLoaded` is empty on the first select of every non-shell session per page, and a session whose pane a desktop tab holds too tall to ever fit then paid a second whole-scrollback capture, reset and replay on every page load and every first tab switch. `framePositionsRowsAbsolutely` gates both the captured-geometry comparison and `sizeMovedUnderLoad`. A size that moved under a byte-stream or scrollback replay is healed by xterm's own reflow plus the SIGWINCH the trailing `sendResize` already sends. A pane WIDER than the terminal damages the same frame a second way, so `captureCols` is now compared rather than only logged. `formatPaneSnapshot` paints each row out to the pane's own width, so a narrower browser wraps every painted row, and the wrap on the last one scrolls the whole frame up by a row. The terminal response no longer falls back to `session.ptyCols`/`ptyRows` when the capture reported no geometry. The cursor query is what produces the absolute addressing in the first place, so a capture that lost it returned a raw frame that was never positioned, and a byte-history response was never positioned either. Naming the session's own PTY size there described a frame that does not exist and invited a repair for damage that is not present. `_ptyCols` is also written only by `resize()` while the PTY is spawned at the size queried from tmux, so it can be wrong on its own terms. Both fields are now absent instead, and the `Session` getters added for that fallback go with it. Two browser cases cover the new behaviour and each fails without its fix: a `mux-full-history` response with both dimensions mismatched asserts one fetch (two without the gate), and a `mux-visible` response wider than the terminal but short enough to fit asserts two (one without the width comparison). Corrects a claim in the comment above `capturedGeometry` in tmux-manager.ts. Both replay paths do not address rows absolutely; the full-history one ends in a relative move, which is the whole reason the gate is right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2.3 KiB
aicodeman
| aicodeman |
|---|
| patch |
fix(terminal): replay a pane capture at the geometry it was taken at
A visible-frame capture repaints each row at an absolute position, counting up to the pane's height. A terminal shorter than that clamps every address past its own height onto its last line, so the overflow rows overwrite one another and the rows underneath are lost. Against a 50-row pane, a 30-row terminal rendered 28 of a 45-line command and drew the surviving frame twice.
A pane wider than the terminal damages the same frame a second way. Each row is painted out to the pane's own width, so a narrower terminal wraps every painted row, and the wrap on the last one scrolls the whole frame up by a row.
Nothing in the response said what geometry the frame was built for, so the client
could not detect either case. A capture now reports the geometry it was really
taken at through capturedGeometry on PaneCaptureOptions, and the terminal
response carries it as captureCols and captureRows. When the captured pane is
taller or wider than the terminal, or the size that produced the capture did not
survive the load, selectSession replays once at the size that stuck.
resizeRetry caps that at one attempt, so two competing fits cannot trade
replays forever.
That comparison runs on a visible-frame response only. A full-history response is linear scrollback closed by a relative cursor move, and a byte-history response carries no row alignment at all, so a size mismatch damages neither and a replay repairs neither. Gating on the source matters because the first load of every non-shell session per page takes the full-history path, where an ungated comparison would capture the whole tmux scrollback a second time. A response whose capture reported no geometry now omits both fields rather than naming the session's own PTY size, which describes no frame that was ever positioned.
That repairs the case where a capture won a race against the resize meant to
precede it. It does not repair a capture whose pane was too tall because
Session.resize declined the resize outright, which it does for a small
viewport while a desktop viewport's size claim is live: the retry re-sends the
same declined resize and captures the same pane. The reported geometry still
helps there, because the client can see the mismatch at all.