From 66c3e3e18c296c88d86d9f1db0c568afb9237022 Mon Sep 17 00:00:00 2001 From: "Marcel G." Date: Fri, 25 Sep 2026 21:23:57 +0200 Subject: [PATCH 1/2] fix: preserve transcript lines with CRLF output --- static/js/terminal-manager.js | 3 +++ tests/js/terminal-transcript.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/js/terminal-transcript.test.js diff --git a/static/js/terminal-manager.js b/static/js/terminal-manager.js index d4584a4..c5c0971 100644 --- a/static/js/terminal-manager.js +++ b/static/js/terminal-manager.js @@ -903,6 +903,9 @@ const TerminalManager = { continue; } if (ch === '\r') { + if (text[i + 1] === '\n') { + continue; + } output.splice(lineStart); continue; } diff --git a/tests/js/terminal-transcript.test.js b/tests/js/terminal-transcript.test.js new file mode 100644 index 0000000..31aec00 --- /dev/null +++ b/tests/js/terminal-transcript.test.js @@ -0,0 +1,26 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +global.window = {addEventListener() {}, visualViewport: null}; +global.document = {}; +global.navigator = {}; + +require('../../static/js/terminal-manager.js'); +const TerminalManager = global.window.TerminalManager; + +test('transcript export preserves text before CRLF line endings', () => { + TerminalManager.transcripts = { + session: ['user@host:~$ echo hello\r', '\n\x1b[32mhello\x1b[0m\r', '\nuser@host:~$ '], + }; + + assert.equal( + TerminalManager.getCleanTranscript('session'), + 'user@host:~$ echo hello\nhello\nuser@host:~$ ', + ); +}); + +test('transcript export still handles a lone carriage return as an overwrite', () => { + TerminalManager.transcripts = {session: ['Downloading 10%\rDownloading 90%\n']}; + + assert.equal(TerminalManager.getCleanTranscript('session'), 'Downloading 90%\n'); +}); From b1b3611fa29ce270901d42b16e11f61c391a0c20 Mon Sep 17 00:00:00 2001 From: "Marcel G." Date: Fri, 25 Sep 2026 21:39:48 +0200 Subject: [PATCH 2/2] fix: classify transcript CRLF before ANSI cleanup --- static/js/terminal-manager.js | 5 +---- tests/js/terminal-transcript.test.js | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/static/js/terminal-manager.js b/static/js/terminal-manager.js index c5c0971..9753233 100644 --- a/static/js/terminal-manager.js +++ b/static/js/terminal-manager.js @@ -878,7 +878,7 @@ const TerminalManager = { if (!raw) { return ''; } - const stripped = this.stripAnsiSequences(raw); + const stripped = this.stripAnsiSequences(raw.replace(/\r\n/g, '\n')); return this.normalizeControlChars(stripped); }, @@ -903,9 +903,6 @@ const TerminalManager = { continue; } if (ch === '\r') { - if (text[i + 1] === '\n') { - continue; - } output.splice(lineStart); continue; } diff --git a/tests/js/terminal-transcript.test.js b/tests/js/terminal-transcript.test.js index 31aec00..e122b88 100644 --- a/tests/js/terminal-transcript.test.js +++ b/tests/js/terminal-transcript.test.js @@ -24,3 +24,9 @@ test('transcript export still handles a lone carriage return as an overwrite', ( assert.equal(TerminalManager.getCleanTranscript('session'), 'Downloading 90%\n'); }); + +test('ANSI erase between CR and LF does not turn a cleared line into a saved line', () => { + TerminalManager.transcripts = {session: ['text\r', '\x1b[2K\n']}; + + assert.equal(TerminalManager.getCleanTranscript('session'), '\n'); +});