Skip to content

P3: VirtualTerminal's remaining CSI handlers differ from a real terminal, none reachable mid-turn #668

Description

@RonenMars

services/virtual-terminal.ts has a family of real defects that #654 deliberately left alone, each a case where the emulator's handler differs from a real terminal.
Filing them as one issue rather than seven, because a capture shows none of them is reachable mid-turn, so they share a single priority and a single unblocking condition.

Verified state

Checked 2026-08-12 against origin/main @ 86eeb2a1, and against a capture of Claude Code v2.1.228 spawned at the streamer's real 120x40 (threadbase-streamer docs/2026-08-12-claude-pty-capture.md).

The defects

  • CSI 2J and CSI 3J are treated identically as total grid annihilation (:249-260, this.grid = [[]]). A real terminal clears only the viewport on 2J; 3J clears scrollback. Two existing tests assert the destructive behaviour (__tests__/unit/services/virtual-terminal.test.ts:490, :556), so changing it is a test rewrite as well as a behaviour change.
  • CSI 1J (erase from start of screen to cursor) falls through the if/else if silently, with no counter increment.
  • CSI 0J truncates the grid to row + 1, permanently deleting scrollback below the cursor.
  • CSI S (:285-288) splices the grid's oldest rows instead of appending blanks at the viewport bottom, destroying scrollback. Not a cursor bug — the splice's implicit shift compensates exactly, so do not carry that claim forward. The loss is also invisible: shrinking grid.length shifts the derived viewport origin by the same amount, so the viewport looks unchanged.
  • CSI L / M / T splice absolute scrollback with no viewport bottom.
  • CSI r (DECSTBM) is parsed and discarded (:295-296); no scroll region is modelled.
  • ESC 7 / ESC 8 / ESC M (DECSC/DECRC/RI) are silently dropped at :193-194, and CSI s / u sit in IGNORED_CSI — there is no saved-cursor state in the class at all.

Why they are all one priority

The capture's CSI histogram, read for presence and absence only:

during the turn:   G  B  C  m  H  K
startup only (1x): r  J  c  ESC7  ESC8
never emitted:     S  T  L  M  A  D  f

S, T, L, M never appear. r, J (all 2J), ESC7 and ESC8 are a single terminal-init sequence — ESC7 ESC[r ESC8, contiguous at offsets 0, 2 and 5 — and a startup 2J annihilates an empty grid, which is harmless. Bare ESC[r resets margins to the full screen rather than setting a region, so ignoring it reaches the correct end state; no parameterised r appears anywhere.

Nothing here is reachable mid-turn, so the queue is uniformly downgraded rather than reordered.

Three separate attempts were made to promote one of these items on the strength of a count of ~30. All three were the same artifact: turn.raw concatenates 36 snapshots, each a full ring-buffer dump, so every snapshot replays the startup bytes. Verified per-snapshot — each contains exactly one 2J at the same offset, and the first gap between occurrences is exactly snap-000.raw's length. Read that histogram for presence and absence; never for magnitude.

What would change this

One capture of a tool-heavy turn — reading files, triggering a hook, producing scrolling output. The existing capture is a single output-dominated turn with no tool use, so anything tool-triggered is unsampled, and L/M in particular are most plausible around file diffs.
Absence in one capture is not absence in general.

If any of these turns out to fire mid-session, it comes back out of this issue as its own P2 with the measured evidence.

Not included

CSI A/B/H/f were fixed in #654 and are not part of this. Note that A is never emitted either — its clamp is defensive rather than reachable, in the same category as the above, except that it rode along free.

Plan and full rationale: threadbase-streamer#537docs/plans/2026-08-12-viewport-relative-cursor-positioning.md, "Explicitly OUT of scope".

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Deferredtech-debtCleanup with no user-visible change

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions