From c961408bf7ef733afff29c8de87d48bc72f31a64 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 20:31:29 -0700 Subject: [PATCH 1/9] fix(browser): use startY in horizontalDirection row helper call horizontalDirection called moveToRequestedRow with targetX as the first argument, but that parameter is the starting row (startY). resetStartingRow and moveToRequestedCol already pass startY correctly; only this copy-paste mistake passed a column index where a row was expected. That could miscompute wrapped-row offsets and emit the wrong horizontal arrow key sequence when building selection/cursor movement strings. Co-authored-by: Cursor --- src/browser/input/MoveToCell.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/input/MoveToCell.ts b/src/browser/input/MoveToCell.ts index b5ddc7ca2..9cc27088d 100644 --- a/src/browser/input/MoveToCell.ts +++ b/src/browser/input/MoveToCell.ts @@ -156,7 +156,7 @@ function wrappedRowsForRow(currentRow: number, bufferService: IBufferService): n */ function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction { let startRow; - if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) { + if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) { startRow = targetY - wrappedRowsForRow(targetY, bufferService); } else { startRow = startY; From 3f3f6abfb90ebd0367e0e894e16aa48fd6c62a39 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 20:33:19 -0700 Subject: [PATCH 2/9] fix(browser): parenthesize document fallback for CoreBrowserService Nullish coalescing binds tighter than the conditional operator, so the previous expression was parsed as (this._document ?? windowDefined) ? window.document : null. When this._document was set (e.g. parent ownerDocument or documentOverride), the ternary still returned window.document instead of the configured document. CoreBrowserService.mainDocument is used by AccessibilityManager, viewport styles, and decoration renderers; CharSizeService already received this._document directly. Add parentheses so the fallback applies only when _document is nullish. Co-authored-by: Cursor --- src/browser/CoreBrowserTerminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index f7e20b914..5b65a93db 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -505,7 +505,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this.textarea, parent.ownerDocument.defaultView ?? window, // Force unsafe null in node.js environment for tests - this._document ?? (typeof window !== 'undefined') ? window.document : null as any + this._document ?? ((typeof window !== 'undefined') ? window.document : null as any) )); this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService); From 536eeb4db09c4787e3197c76e08aef09cc12ce09 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 20:34:13 -0700 Subject: [PATCH 3/9] refactor(browser): remove unreachable coords guard in getCoords getCoordsRelativeToElement always returns a two-element tuple, so the array is always truthy and if (!coords) could never run. hasValidCharSize already handles the only early-exit path before coordinates are computed. Co-authored-by: Cursor --- src/browser/input/Mouse.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/browser/input/Mouse.ts b/src/browser/input/Mouse.ts index 5f73743fb..ba8bddaa2 100644 --- a/src/browser/input/Mouse.ts +++ b/src/browser/input/Mouse.ts @@ -37,10 +37,6 @@ export function getCoords(window: Pick, event: Pick< } const coords = getCoordsRelativeToElement(window, event, element); - if (!coords) { - return undefined; - } - coords[0] = Math.ceil((coords[0] + (isSelection ? cssCellWidth / 2 : 0)) / cssCellWidth); coords[1] = Math.ceil(coords[1] / cssCellHeight); From 1dd113b38a49dbb030ad52f8171254ba88ac5ff2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 20:38:07 -0700 Subject: [PATCH 4/9] fix(browser): reset TimeBasedDebouncer state on dispose dispose() cleared the pending timeout but left _refreshTimeoutID and _additionalRefreshRequested set. After dispose, a stale timeout id or trailing-refresh flag could confuse a new instance or tests. Match RenderDebouncer by clearing the timeout id and reset the trailing flag. Co-authored-by: Cursor --- src/browser/TimeBasedDebouncer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts index 0860e7927..959c77a4e 100644 --- a/src/browser/TimeBasedDebouncer.ts +++ b/src/browser/TimeBasedDebouncer.ts @@ -31,7 +31,9 @@ export class TimeBasedDebouncer implements IRenderDebouncer { public dispose(): void { if (this._refreshTimeoutID) { clearTimeout(this._refreshTimeoutID); + this._refreshTimeoutID = undefined; } + this._additionalRefreshRequested = false; } public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { From 98d7c25bd5476ae39773fee378ac48053fa98ceb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 21:44:30 -0700 Subject: [PATCH 5/9] fix(browser): cancel trailing debounce timeout on immediate refresh TimeBasedDebouncer can schedule a trailing setTimeout when refresh requests are throttled. If the debounce window later elapses and an immediate refresh runs, that pending timeout was left armed and could fire afterward, causing a duplicate _innerRefresh (e.g. extra live region updates for screen readers). Clear any pending timeout and reset _additionalRefreshRequested before running the immediate refresh path. Co-authored-by: Cursor --- src/browser/TimeBasedDebouncer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts index 959c77a4e..f0dd682e2 100644 --- a/src/browser/TimeBasedDebouncer.ts +++ b/src/browser/TimeBasedDebouncer.ts @@ -49,7 +49,12 @@ export class TimeBasedDebouncer implements IRenderDebouncer { // enough time to pass before refreshing again. const refreshRequestTime: number = performance.now(); if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) { - // Enough time has lapsed since the last refresh; refresh immediately + // Enough time has elapsed since the last refresh; refresh immediately + if (this._refreshTimeoutID !== undefined) { + clearTimeout(this._refreshTimeoutID); + this._refreshTimeoutID = undefined; + this._additionalRefreshRequested = false; + } this._lastRefreshMs = refreshRequestTime; this._innerRefresh(); } else if (!this._additionalRefreshRequested) { From 69b11a1a48f721c30ba9a79a324a6355212cf681 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 22:00:22 -0700 Subject: [PATCH 6/9] fix(browser): treat rAF id 0 as scheduled in RenderDebouncer dispose(), addRefreshCallback(), and refresh() used truthiness on _animationFrame, so a requestAnimationFrame handle of 0 would not be cancelled or deduplicated. Compare against undefined instead, matching other timer checks in the browser layer. Co-authored-by: Cursor --- src/browser/RenderDebouncer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index f6ea59bf6..fcfdaa5ec 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -23,7 +23,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { } public dispose(): void { - if (this._animationFrame) { + if (this._animationFrame !== undefined) { this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } @@ -31,7 +31,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { public addRefreshCallback(callback: FrameRequestCallback): number { this._refreshCallbacks.push(callback); - if (!this._animationFrame) { + if (this._animationFrame === undefined) { this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh()); } return this._animationFrame; @@ -46,7 +46,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; - if (this._animationFrame) { + if (this._animationFrame !== undefined) { return; } From 80468eeb4cbf1269c8c7fe0d858a9c9c4bfcfbbc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 22:04:23 -0700 Subject: [PATCH 7/9] fix(browser): clear idle blink timer when handle is 0 _clearIdleTimer() used truthiness on _idleTimeout, so a setTimeout handle of 0 would not be cleared. Compare against undefined, consistent with RenderDebouncer rAF handling. Co-authored-by: Cursor --- src/browser/renderer/dom/DomRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index de08ac0ac..383cf3025 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -694,7 +694,7 @@ class CursorBlinkStateManager { } private _clearIdleTimer(): void { - if (this._idleTimeout) { + if (this._idleTimeout !== undefined) { this._coreBrowserService.window.clearTimeout(this._idleTimeout); this._idleTimeout = undefined; } From bc26cc16c1ad1fe7f66db302b93008825262c550 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 May 2026 22:14:29 -0700 Subject: [PATCH 8/9] fix(browser): place imports before module const in TimeBasedDebouncer RENDER_DEBOUNCE_THRESHOLD_MS was declared above the import, which breaks ES module conventions and can confuse tooling. Move the import to the top of the module after the license header. Co-authored-by: Cursor --- src/browser/TimeBasedDebouncer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts index f0dd682e2..27b692bae 100644 --- a/src/browser/TimeBasedDebouncer.ts +++ b/src/browser/TimeBasedDebouncer.ts @@ -3,10 +3,10 @@ * @license MIT */ -const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second - import { IRenderDebouncer } from './Types'; +const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second + /** * Debounces calls to update screen readers to update at most once configurable interval of time. */ From 454e0749c267e88ace5c3604f513cff6d9ea1589 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 1 Jun 2026 16:15:01 +0000 Subject: [PATCH 9/9] fix(browser): use ??= in RenderDebouncer for lint Co-authored-by: Daniel Imms --- src/browser/RenderDebouncer.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index fcfdaa5ec..eeadbe018 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -31,9 +31,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { public addRefreshCallback(callback: FrameRequestCallback): number { this._refreshCallbacks.push(callback); - if (this._animationFrame === undefined) { - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh()); - } + this._animationFrame ??= this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh()); return this._animationFrame; }