diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index e0e6253c..f816e114 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -521,7 +521,13 @@ export class InputHandler extends Disposable implements IInputHandler { const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._coreService.modes.insertMode; const curAttr = this._curAttrData; - let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); + + // Defensive check: bufferRow can be undefined if a resize occurred mid-write due to async + // scheduling gaps in WriteBuffer. See https://github.com/xtermjs/xterm.js/issues/5597 + if (!bufferRow) { + return; + } this._dirtyRowTracker.markDirty(this._activeBuffer.y); @@ -586,7 +592,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true; } // row changed, get it again - bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); + if (!bufferRow) { + return; + } if (oldWidth > 0 && bufferRow instanceof BufferLine) { // Combining character widens 1 column to 2. // Move old character to next line. diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 48236bc6..ee0374f7 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -264,6 +264,13 @@ export class Buffer implements IBuffer { this._cols = newCols; this._rows = newRows; + // Ensure the cursor position invariant: ybase + y must be within buffer bounds + // This can be violated during reflow or when shrinking rows + if (this.lines.length > 0) { + const maxY = Math.max(0, this.lines.length - this.ybase - 1); + this.y = Math.min(this.y, maxY); + } + this._memoryCleanupQueue.clear(); // schedule memory cleanup only, if more than 10% of the lines are affected if (dirtyMemoryLines > 0.1 * this.lines.length) { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index acd89f73..6aeb2d02 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -402,6 +402,16 @@ test.describe('API Integration Tests', () => { await pollFor(ctx.page, `window.calls`, [[10, 5], [20, 15]]); }); + test('resize during write should not throw', async () => { + await openTerminal(ctx, { rows: 50, cols: 80 }); + const largeData = 'x'.repeat(10000); + await ctx.proxy.write(largeData); + await ctx.proxy.resize(80, 10); + await ctx.proxy.write(largeData); + await ctx.proxy.resize(80, 5); + await ctx.proxy.write(largeData); + }); + test('onTitleChange', async () => { await openTerminal(ctx); await ctx.page.evaluate(`