From b50ca99c7e445a40ed094eb078b4b406ec35c5d5 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Wed, 1 Apr 2026 11:10:19 -0700 Subject: [PATCH 1/3] terminal: avoid cursor jump on reconnect --- .../contrib/terminal/browser/terminalProcessManager.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 72c54a5518df2..497036d0964c4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -568,7 +568,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // For normal terminals write a message indicating what happened and relaunch // using the previous shellLaunchConfig const message = localize('ptyHostRelaunch', "Restarting the terminal because the connection to the shell process was lost..."); - this._onProcessData.fire({ data: formatMessageForTerminal(message, { loudFormatting: true }), trackCommit: false }); + // Align with _reviveTerminalProcess to hedge against PSReadLine `GetConsoleCursorInfo` and cursor handling from conpty. + let postRestartMessage = ''; + if (this.os === OperatingSystem.Windows && this._dimensions.rows > 0) { + postRestartMessage = '\r\n'.repeat(this._dimensions.rows - 1) + `\x1b[H`; + } + this._onProcessData.fire({ data: formatMessageForTerminal(message, { loudFormatting: true }) + postRestartMessage, trackCommit: false }); await this.relaunch(this._shellLaunchConfig, this._dimensions.cols, this._dimensions.rows, false); } } From 0eb677015abf313e2bf7c0a10ac2d81283aff175 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:22:02 -0700 Subject: [PATCH 2/3] Update src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/terminal/browser/terminalProcessManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 497036d0964c4..274c4b481e5b9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -568,7 +568,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // For normal terminals write a message indicating what happened and relaunch // using the previous shellLaunchConfig const message = localize('ptyHostRelaunch', "Restarting the terminal because the connection to the shell process was lost..."); - // Align with _reviveTerminalProcess to hedge against PSReadLine `GetConsoleCursorInfo` and cursor handling from conpty. + // Align with the pty service's revive logic (_reviveTerminalProcess in src/vs/platform/terminal/node/ptyService.ts) + // to hedge against PSReadLine `GetConsoleCursorInfo` and cursor handling from conpty. let postRestartMessage = ''; if (this.os === OperatingSystem.Windows && this._dimensions.rows > 0) { postRestartMessage = '\r\n'.repeat(this._dimensions.rows - 1) + `\x1b[H`; From 1c0d90d2dd68d5addaf676649a0f3bbfdbfd66a0 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Wed, 1 Apr 2026 11:49:50 -0700 Subject: [PATCH 3/3] terminal: add reconnect restart tests --- .../browser/terminalProcessManager.test.ts | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index f029f371a9072..85738f7f59321 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { strictEqual } from 'assert'; -import { Event } from '../../../../../base/common/event.js'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { OperatingSystem } from '../../../../../base/common/platform.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { IConfigurationService, type IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js'; @@ -51,12 +52,13 @@ class TestTerminalChildProcess implements ITerminalChildProcess { } class TestTerminalInstanceService implements Partial { + readonly ptyHostRestartEmitter = new Emitter(); async getBackend() { return { onPtyHostExit: Event.None, onPtyHostUnresponsive: Event.None, onPtyHostResponsive: Event.None, - onPtyHostRestart: Event.None, + onPtyHostRestart: this.ptyHostRestartEmitter.event, onDidMoveWindowInstance: Event.None, onDidRequestDetach: Event.None, createProcess: ( @@ -77,6 +79,7 @@ class TestTerminalInstanceService implements Partial { suite('Workbench - TerminalProcessManager', () => { let manager: TerminalProcessManager; + let terminalInstanceService: TestTerminalInstanceService; const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -96,7 +99,9 @@ suite('Workbench - TerminalProcessManager', () => { configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true, } satisfies Partial as unknown as IConfigurationChangeEvent); - instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); + terminalInstanceService = new TestTerminalInstanceService(); + store.add(terminalInstanceService.ptyHostRestartEmitter); + instantiationService.stub(ITerminalInstanceService, terminalInstanceService); instantiationService.stub(ITerminalService, { setNextCommandId: async () => { } } as Partial); manager = store.add(instantiationService.createInstance(TerminalProcessManager, 1, undefined, undefined, undefined)); @@ -141,4 +146,39 @@ suite('Workbench - TerminalProcessManager', () => { }); }); }); + + suite('pty host restart', () => { + async function fireRestartAndCaptureData(os: OperatingSystem, rows: number): Promise { + await manager.createProcess({}, 80, rows, false); + manager.os = os; + let captured: string | undefined; + store.add(manager.onProcessData(e => captured = e.data)); + terminalInstanceService.ptyHostRestartEmitter.fire(); + return captured!; + } + + test('appends viewport-clearing newlines and ESC[H on Windows', async () => { + const data = await fireRestartAndCaptureData(OperatingSystem.Windows, 24); + deepStrictEqual( + { endsWithViewportClear: data.endsWith('\r\n'.repeat(23) + '\x1b[H') }, + { endsWithViewportClear: true } + ); + }); + + test('does not append viewport-clearing sequence on non-Windows', async () => { + const data = await fireRestartAndCaptureData(OperatingSystem.Linux, 24); + deepStrictEqual( + { containsCursorHome: data.includes('\x1b[H') }, + { containsCursorHome: false } + ); + }); + + test('does not append viewport-clearing sequence on Windows when rows is 0', async () => { + const data = await fireRestartAndCaptureData(OperatingSystem.Windows, 0); + deepStrictEqual( + { containsCursorHome: data.includes('\x1b[H') }, + { containsCursorHome: false } + ); + }); + }); });