From 323347f50e6d5479406918ca76bd9cc0c7602f68 Mon Sep 17 00:00:00 2001 From: Isaac Sanchez-Hawkins <266845420+isanchez404@users.noreply.github.com> Date: Tue, 12 May 2026 04:05:03 -0400 Subject: [PATCH] fix(terminal): preserve UTF-8 replay chunks (#1181) * fix(terminal): preserve UTF-8 replay chunks * fix(terminal): avoid replay trim prepends --------- Co-authored-by: Isaac Sanchez --- .../server/lib/terminal/output-replay-buffer.js | 16 ++++++++++++++-- .../lib/terminal/output-replay-buffer.test.js | 9 +++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/web/server/lib/terminal/output-replay-buffer.js b/packages/web/server/lib/terminal/output-replay-buffer.js index d7c64bf0..458920fa 100644 --- a/packages/web/server/lib/terminal/output-replay-buffer.js +++ b/packages/web/server/lib/terminal/output-replay-buffer.js @@ -10,8 +10,20 @@ const trimTerminalOutputChunkToMaxBytes = (data, maxBytes) => { return data; } - const trimmedBuffer = Buffer.from(data, 'utf8').subarray(-maxBytes); - return trimmedBuffer.toString('utf8'); + const kept = []; + let trimmedBytes = 0; + const characters = Array.from(data); + for (let index = characters.length - 1; index >= 0; index -= 1) { + const character = characters[index]; + const characterBytes = Buffer.byteLength(character, 'utf8'); + if (trimmedBytes + characterBytes > maxBytes) { + break; + } + kept.push(character); + trimmedBytes += characterBytes; + } + + return kept.reverse().join(''); }; export const createTerminalOutputReplayBuffer = () => ({ diff --git a/packages/web/server/lib/terminal/output-replay-buffer.test.js b/packages/web/server/lib/terminal/output-replay-buffer.test.js index 19b5785e..a2349a2d 100644 --- a/packages/web/server/lib/terminal/output-replay-buffer.test.js +++ b/packages/web/server/lib/terminal/output-replay-buffer.test.js @@ -56,6 +56,15 @@ describe('terminal output replay buffer', () => { expect(bufferState.totalBytes).toBe(4); }); + it('does not split multibyte characters when trimming oversized chunks', () => { + const bufferState = createTerminalOutputReplayBuffer(); + const chunk = appendTerminalOutputReplayChunk(bufferState, '🙂x', 2); + + expect(chunk?.data).toBe('x'); + expect(chunk?.bytes).toBe(1); + expect(bufferState.totalBytes).toBe(1); + }); + it('uses the default max bytes when not provided', () => { const bufferState = createTerminalOutputReplayBuffer(); const chunk = appendTerminalOutputReplayChunk(bufferState, 'ok');