From 72f2edfa713114a8b122defdef802595ec031fd8 Mon Sep 17 00:00:00 2001 From: HarshaNaidu11 Date: Fri, 3 Jul 2026 23:20:46 +0530 Subject: [PATCH 1/2] fix(core): adjust wide-character rendering boundaries in differential renderer --- packages/core/src/terminal/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/terminal/Renderer.ts b/packages/core/src/terminal/Renderer.ts index 41404e21..8dde62bf 100644 --- a/packages/core/src/terminal/Renderer.ts +++ b/packages/core/src/terminal/Renderer.ts @@ -278,7 +278,7 @@ export class Renderer { // flush the span const adjustedStart = Renderer._adjustSpanStart(spanStart, back[row]); output += moveTo(adjustedStart, row); - for (let sc = spanStart; sc < c; sc++) { + for (let sc = adjustedStart; sc < c; sc++) { const cell = back[row][sc]; if (cell.width === 0) continue; output += this._renderCell(cell); @@ -291,7 +291,7 @@ export class Renderer { if (spanStart !== -1) { const adjustedStart = Renderer._adjustSpanStart(spanStart, back[row]); output += moveTo(adjustedStart, row); - for (let sc = spanStart; sc < cols; sc++) { + for (let sc = adjustedStart; sc < cols; sc++) { const cell = back[row][sc]; if (cell.width === 0) continue; output += this._renderCell(cell); From 8e843412a459f5a292719f2250919123461c5021 Mon Sep 17 00:00:00 2001 From: HarshaNaidu11 Date: Sun, 5 Jul 2026 12:03:27 +0530 Subject: [PATCH 2/2] fix(core): resolve wide-character layout corruption in differential renderer --- packages/core/src/terminal/Renderer.test.ts | 28 +++++++++++++++++++++ packages/core/src/terminal/Renderer.ts | 7 +++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/core/src/terminal/Renderer.test.ts b/packages/core/src/terminal/Renderer.test.ts index 4f9fb1c0..3ab64f7f 100644 --- a/packages/core/src/terminal/Renderer.test.ts +++ b/packages/core/src/terminal/Renderer.test.ts @@ -262,4 +262,32 @@ describe('Renderer profiling hooks', () => { const resetCount = (output.match(/\x1b\[0m/g) || []).length; expect(resetCount).toBeGreaterThanOrEqual(2); }); + + it('correctly adjusts span start backwards and renders from adjustedStart', () => { + const narrowScreen = new Screen(10, 2); + const renderer = new Renderer(terminal, narrowScreen); + + // First frame: write a wide character at col 0 and 'A' at col 2 + narrowScreen.setCell(0, 0, { char: '中', width: 2 }); + narrowScreen.setCell(1, 0, { char: '', width: 0 }); + narrowScreen.setCell(2, 0, { char: 'A', width: 1 }); + renderer.renderNow(); + + // Second frame: Keep col 0 unchanged, change col 1 style (continuation) and col 2 char + narrowScreen.setCell(0, 0, { char: '中', width: 2 }); + narrowScreen.setCell(1, 0, { char: '', width: 0, fg: { type: 'named', name: 'red' } }); + narrowScreen.setCell(2, 0, { char: 'B', width: 1 }); + + fakeStdout.writes = ''; + renderer.renderNow(); + + // Verify that the output has redrawn the wide character at col 0 + const outputStr = fakeStdout.writes; + // It should move to col 0, not col 1 or 2, to start rendering + expect(outputStr).toContain('\x1b[1;1H'); + // It should render '中' + expect(outputStr).toContain('中'); + // It should render 'B' + expect(outputStr).toContain('B'); + }); }); diff --git a/packages/core/src/terminal/Renderer.ts b/packages/core/src/terminal/Renderer.ts index 8dde62bf..3368e743 100644 --- a/packages/core/src/terminal/Renderer.ts +++ b/packages/core/src/terminal/Renderer.ts @@ -267,11 +267,10 @@ export class Renderer { let spanStart = -1; for (let c = 0; c < cols; c++) { - // Skip continuation cells (right half of wide chars) - they are not - // independently renderable and their primary cell handles the output. - if (back[row][c].width === 0) continue; - const changed = !cellsEqual(front[row][c], back[row][c]); + // Skip continuation cells (right half of wide chars) if they haven't changed. + // If they have changed, we must process them so we can adjust the span start back to the primary cell. + if (back[row][c].width === 0 && !changed) continue; if (changed && spanStart === -1) { spanStart = c; // start a new changed span } else if (!changed && spanStart !== -1) {