Skip to content

Make CSI 3 J discard the scrollback instead of erasing the screen - #15

Merged
tomlm merged 1 commit into
mainfrom
tomlm/csi-3j-erase-scrollback
Aug 22, 2026
Merged

Make CSI 3 J discard the scrollback instead of erasing the screen#15
tomlm merged 1 commit into
mainfrom
tomlm/csi-3j-erase-scrollback

Conversation

@tomlm

@tomlm tomlm commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Reported downstream as Iciclecreek.Avalonia.Terminal#28: cls in cmd.exe leaves the history scrollable with the mouse wheel, unlike conhost and Windows Terminal.

The bug

Mode 3 shared mode 2's body:

case 2: // Erase all
case 3: // Erase scrollback (extension)
    for (int i = 0; i < _terminal.Rows; i++)
        _buffer.Lines[_buffer.YBase + i]?.Fill(emptyCell);

That erases the visible screen and never touches the scrollback — the opposite of what mode 3 asks for. The comment said "erase scrollback"; the code did not. The two modes are complements rather than variations, so a caller wanting both sends 2 and 3.

Why that surfaced as a cls bug

Captured from ConPTY, this is what cls actually emits:

<ESC>[?25l <ESC>[H  ...25× <ESC>[K<CR><LF>...  <ESC>[3J  <ESC>[2;1H

It never sends CSI 2 J. It clears the screen itself, line by line with ESC[K, and then sends CSI 3 J for the history. With mode 3 doing nothing, the history survived.

That also explains the reporter's odd follow-up — that cls did clear a fresh terminal but not one that had run dir first. With no scrollback yet, clearing the screen looks exactly like clearing everything.

The fix

TerminalBuffer.ClearScrollback() trims the lines above the visible screen and moves _yBase and _yDisp with them.

Discarding rather than blanking is the point. Blanked lines are still present and still scrollable, so the history would remain reachable even after the terminal had been told to throw it away.

Moving the indices is load-bearing. _yBase and _yDisp are absolute indices into the line list, so trimming without adjusting them leaves the visible screen pointing at an offset that no longer exists — the next write walks off the end:

IndexOutOfRangeException at CircularList`1.get_Item
  at InputHandler.Print → Terminal.Write

I hit that while probing whether a downstream workaround was possible, which is also how I concluded the fix belonged here rather than in the consumer.

Tests

Four, alongside the existing EraseInDisplay cases. Three fail against the previous behaviour; the fourth, LeavesTheBufferWritable, passes either way and is deliberate — it guards this implementation's own risk, the stale-index crash, rather than reproducing the original defect.

607 tests green.

Version left at 1.0.15 — bumping looks like something you do deliberately at release time.

🤖 Generated with Claude Code

Mode 3 shared mode 2's body:

    case 2: // Erase all
    case 3: // Erase scrollback (extension)
        for (int i = 0; i < _terminal.Rows; i++)
            _buffer.Lines[_buffer.YBase + i]?.Fill(emptyCell);

That erases the VISIBLE screen and never touches the scrollback, which is the
opposite of what mode 3 asks for. The comment said "erase scrollback" and the
code did not. They are complements rather than variations: a caller wanting
both sends 2 and 3.

Reported downstream as Iciclecreek.Avalonia.Terminal#28 — `cls` in cmd.exe
left the history scrollable with the mouse wheel, unlike conhost and Windows
Terminal. Confirmed by capturing what ConPTY actually emits for `cls`:

    <ESC>[?25l <ESC>[H  ...25x <ESC>[K<CR><LF>...  <ESC>[3J  <ESC>[2;1H

It never sends CSI 2 J. It clears the screen itself, line by line, and then
sends CSI 3 J for the history — so with mode 3 doing nothing, the history
survived. That also explains the reporter's odd follow-up, that `cls` DID
clear a fresh terminal but not one that had run `dir` first: with no
scrollback yet, clearing the screen looks like clearing everything.

Discarding rather than blanking is the point. Blanked lines are still there
and still scrollable, so the history would remain reachable even after being
thrown away.

TerminalBuffer.ClearScrollback trims the lines above the screen and moves
_yBase and _yDisp with them. That second part is load-bearing: they are
absolute indices into the line list, so trimming without adjusting them leaves
the visible screen pointing at an offset that no longer exists and the next
write walks off the end with an IndexOutOfRangeException.

Four tests. Three fail against the previous behaviour; the fourth
(LeavesTheBufferWritable) passes either way and is deliberate — it guards the
new implementation's own risk, the stale-index crash, rather than reproducing
the original defect.

607 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132tgWvNwhLt1HprKFSTrsP
@tomlm
tomlm merged commit de28b7c into main Aug 22, 2026
1 check passed
@tomlm
tomlm deleted the tomlm/csi-3j-erase-scrollback branch August 22, 2026 16:02
@tomlm tomlm mentioned this pull request Aug 22, 2026
pull Bot referenced this pull request in IvanJosipovic/XTerm.NET Aug 22, 2026
1.0.15 is already published and predates the CSI 3 J fix in #15, so the fix
needs its own release to reach consumers.

That fix makes CSI 3 J discard the scrollback instead of erasing the visible
screen, which is what cmd.exe's `cls` relies on under ConPTY. Verified end to
end against a real shell: after `cls` the buffer goes from 1025 lines with
BaseY 1000 down to 25 lines with BaseY 0, where before the fix both were
unchanged.

Downstream, Iciclecreek.Avalonia.Terminal#28 is waiting on this — it pins
1.0.14, so it will move two versions when it picks this up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132tgWvNwhLt1HprKFSTrsP
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