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');