Skip to content

feat(terminal): replay full tmux scrollback on terminal reload + CRLF normalization#148

Open
aakhter wants to merge 2 commits into
Ark0N:masterfrom
aakhter:cod-164-scrollback-replay-crlf
Open

feat(terminal): replay full tmux scrollback on terminal reload + CRLF normalization#148
aakhter wants to merge 2 commits into
Ark0N:masterfrom
aakhter:cod-164-scrollback-replay-crlf

Conversation

@aakhter

@aakhter aakhter commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

A full page reload (GET /api/sessions/:id/terminal without ?tail=) previously only replayed Codeman's in-memory byte buffer — scrollback that had been pushed off the buffer was lost. This PR adds two complementary fixes:

COD-47: Full tmux scrollback replay

  • GET /api/sessions/:id/terminal (no ?tail=) now calls capture-pane -p -e -S -, capturing the entire tmux scrollback, not just the visible frame.
  • Tab switches (?tail=N) keep using the fast visible-frame path — no regression.
  • Shell-mode sessions apply a VPA-strip gate before returning the buffer (skips Ink redraw bloat).
  • MuxInterface.capturePaneBuffer gains an optional opts.fullHistory flag to request the deep capture.

COD-138: CRLF normalization of scrollback replay

Without this, full-history capture causes a "staircase" rendering bug: capture-pane -S - returns scrollback with bare \n line endings; xterm.js running with convertEol: false (correct for the live PTY stream, which carries real \r\n) treats each bare \n as "move down without carriage return," shifting each replayed line one column to the right.

Fix: new pure helper normalizeScrollbackEol() converts \r?\n\r\n (idempotent on CRLF; preserves lone \r overwrites; adds/removes no rows) applied at the raw-return seam. The absolute-cursor snapshot path (visible-frame / tab-switch) is unchanged.

Test plan

  • test/tmux-capture-full-history.test.ts (new, 5 tests) — full-history flag + VPA-strip gate
  • test/tmux-scrollback-eol.test.ts (new, 6 tests) — CRLF normalization invariants (no bare LF, CRLF idempotency, lone-CR preservation)
  • test/routes/session-routes.test.ts (13 new terminal-route tests added, 63 total pass)
  • tsc --noEmit: 0 errors
  • npm run build: clean
  • Prettier: clean

aakhter added 2 commits July 10, 2026 11:08
A full page reload (GET /api/sessions/:id/terminal with no ?tail=) now captures
the ENTIRE tmux scrollback via capture-pane -p -e -S -, so users get back history
that scrolled off Codeman's byte buffer. Tab switches (?tail=N) keep the fast
visible-frame capture.

- tmux-manager capturePaneBuffer/captureActivePaneBuffer take { fullHistory }:
  full-history returns raw linear scrollback (skips the single-screen
  formatPaneSnapshot repaint, which would clip multi-screen history).
- /terminal selects full-history on full reload, visible on tail; caps the
  payload at the configured terminalBufferMaxBytes (keeps most-recent bytes,
  line-aligned) and returns source/fullSize/truncated metadata.

Verified: tsc 0, tmux-capture-full-history 5/5, session-routes 68/68.
Caveat: lines tmux already evicted past its history-limit can't be recovered.
A shell terminal could render output diagonally (each line shifted one
column right) after a full page reload or a cursor-query-failure replay.

Root cause: capturePaneBuffer's full-history path (capture-pane -p -e -S -)
and its cursor-query-failure fallback returned raw scrollback, which tmux
joins with a BARE \n. The browser xterm uses convertEol:false (correct for
the live PTY stream, which carries real \r\n), so each bare \n dropped a
row without returning the cursor to column 0 -> staircase. The visible /
tab-switch path (formatPaneSnapshot) was immune because it repaints each
row with an absolute cursor CSI.

Fix: new pure helper normalizeScrollbackEol() (\r?\n -> \r\n, idempotent
on CRLF, leaves lone \r overwrites untouched, adds/removes no rows) applied
at both raw-return seams. The absolute-positioned snapshot path is unchanged.

Tests: test/tmux-scrollback-eol.test.ts pins the invariant (no LF without a
preceding CR) + CRLF idempotency + lone-CR preservation. 136/136 across
tmux-scrollback-eol + tmux-capture-full-history + tmux-manager +
routes/session-routes; build, tsc, prettier, frontend-syntax clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant