From 204c05ab832051b776b774224c49f4386b11e23b Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 10:18:33 -0800 Subject: [PATCH 01/16] Ensure terminal to prevent cannot read undefined error --- src/client/common/terminal/service.ts | 40 ++++++++++++--------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 54c1fd1f795e..0539914e3d32 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -20,8 +20,6 @@ import { TerminalShellType, } from './types'; import { traceVerbose } from '../../logging'; -import { useEnvExtension } from '../../envExt/api.internal'; -import { ensureTerminalLegacy } from '../../envExt/api.legacy'; import { sleep } from '../utils/async'; @injectable() @@ -81,6 +79,7 @@ export class TerminalService implements ITerminalService, Disposable { commandLine: string, isPythonShell: boolean, ): Promise { + await this.ensureTerminal(); const terminal = this.terminal!; if (!this.options?.hideFromUser) { terminal.show(true); @@ -128,33 +127,30 @@ export class TerminalService implements ITerminalService, Disposable { } } // TODO: Debt switch to Promise ---> breaks 20 tests + // TODO: Properly migrate all creation, ensureTerminal to environment extension. public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; } - if (useEnvExtension()) { - this.terminal = await ensureTerminalLegacy(this.options?.resource, { - name: this.options?.title || 'Python', - hideFromUser: this.options?.hideFromUser, - }); - } else { - this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); - this.terminal = this.terminalManager.createTerminal({ - name: this.options?.title || 'Python', - hideFromUser: this.options?.hideFromUser, - }); - this.terminalAutoActivator.disableAutoActivation(this.terminal); + // Always use the legacy terminal creation method for now + // The environment extension path can create duplicate terminals + // or show `Cannot read properties of undefined (reading 'show')` error: https://github.com/microsoft/vscode-python-environments/issues/958 + this.terminal = this.terminalManager.createTerminal({ + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); + this.terminalAutoActivator.disableAutoActivation(this.terminal); - await sleep(100); + await sleep(100); - await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, { - resource: this.options?.resource, - preserveFocus, - interpreter: this.options?.interpreter, - hideFromUser: this.options?.hideFromUser, - }); - } + await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, { + resource: this.options?.resource, + preserveFocus, + interpreter: this.options?.interpreter, + hideFromUser: this.options?.hideFromUser, + }); if (!this.options?.hideFromUser) { this.terminal.show(preserveFocus); From f01b51b6574dbc5d5eccf6c76a8377a3b65aabd6 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 10:27:12 -0800 Subject: [PATCH 02/16] Return early if going to useEnvExtension route --- src/client/common/terminal/service.ts | 41 +++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 0539914e3d32..45b4ac5ed198 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -20,6 +20,8 @@ import { TerminalShellType, } from './types'; import { traceVerbose } from '../../logging'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { ensureTerminalLegacy } from '../../envExt/api.legacy'; import { sleep } from '../utils/async'; @injectable() @@ -79,7 +81,6 @@ export class TerminalService implements ITerminalService, Disposable { commandLine: string, isPythonShell: boolean, ): Promise { - await this.ensureTerminal(); const terminal = this.terminal!; if (!this.options?.hideFromUser) { terminal.show(true); @@ -127,30 +128,34 @@ export class TerminalService implements ITerminalService, Disposable { } } // TODO: Debt switch to Promise ---> breaks 20 tests - // TODO: Properly migrate all creation, ensureTerminal to environment extension. public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; } - // Always use the legacy terminal creation method for now - // The environment extension path can create duplicate terminals - // or show `Cannot read properties of undefined (reading 'show')` error: https://github.com/microsoft/vscode-python-environments/issues/958 - this.terminal = this.terminalManager.createTerminal({ - name: this.options?.title || 'Python', - hideFromUser: this.options?.hideFromUser, - }); - this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); - this.terminalAutoActivator.disableAutoActivation(this.terminal); + if (useEnvExtension()) { + this.terminal = await ensureTerminalLegacy(this.options?.resource, { + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + return; + } else { + this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); + this.terminal = this.terminalManager.createTerminal({ + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + this.terminalAutoActivator.disableAutoActivation(this.terminal); - await sleep(100); + await sleep(100); - await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, { - resource: this.options?.resource, - preserveFocus, - interpreter: this.options?.interpreter, - hideFromUser: this.options?.hideFromUser, - }); + await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, { + resource: this.options?.resource, + preserveFocus, + interpreter: this.options?.interpreter, + hideFromUser: this.options?.hideFromUser, + }); + } if (!this.options?.hideFromUser) { this.terminal.show(preserveFocus); From f4cc2dd1de330cf2a8cd52a985e15e484e07cb1b Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 10:27:42 -0800 Subject: [PATCH 03/16] Better comments --- src/client/common/terminal/service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 45b4ac5ed198..03e246bef6e3 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -138,6 +138,7 @@ export class TerminalService implements ITerminalService, Disposable { name: this.options?.title || 'Python', hideFromUser: this.options?.hideFromUser, }); + // Return early to prevent duplicate creation of terminal when using env extension. return; } else { this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); From 700715e0950008126a13f6b17cb30c4fd5676788 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 11:46:11 -0800 Subject: [PATCH 04/16] Do not execute when terminal is not ready --- src/client/common/terminal/service.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 03e246bef6e3..864842d1d613 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -20,9 +20,9 @@ import { TerminalShellType, } from './types'; import { traceVerbose } from '../../logging'; +import { sleep } from '../utils/async'; import { useEnvExtension } from '../../envExt/api.internal'; import { ensureTerminalLegacy } from '../../envExt/api.legacy'; -import { sleep } from '../utils/async'; @injectable() export class TerminalService implements ITerminalService, Disposable { @@ -81,7 +81,14 @@ export class TerminalService implements ITerminalService, Disposable { commandLine: string, isPythonShell: boolean, ): Promise { - const terminal = this.terminal!; + // TODO: First execution of shift+enter may get ignored when using sendText. + // Prevent Cannot read properties of undefined: https://github.com/microsoft/vscode-python-environments/issues/958 + if (!this.terminal) { + traceVerbose('Terminal not available yet, cannot execute command'); + return undefined; + } + + const terminal = this.terminal; if (!this.options?.hideFromUser) { terminal.show(true); } @@ -128,6 +135,7 @@ export class TerminalService implements ITerminalService, Disposable { } } // TODO: Debt switch to Promise ---> breaks 20 tests + // TODO: Properly migrate all creation, ensureTerminal to environment extension. public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; @@ -138,7 +146,6 @@ export class TerminalService implements ITerminalService, Disposable { name: this.options?.title || 'Python', hideFromUser: this.options?.hideFromUser, }); - // Return early to prevent duplicate creation of terminal when using env extension. return; } else { this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); From 5e0035a3052ace099e19d29bba04df7663ae106f Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 11:52:51 -0800 Subject: [PATCH 05/16] better --- src/client/common/terminal/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 864842d1d613..250bab138065 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -135,7 +135,6 @@ export class TerminalService implements ITerminalService, Disposable { } } // TODO: Debt switch to Promise ---> breaks 20 tests - // TODO: Properly migrate all creation, ensureTerminal to environment extension. public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; From 46a3b9173bf2c80a08aca7a250e85b0fcfcb2a27 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 15:01:53 -0800 Subject: [PATCH 06/16] execute REPL command with queue! --- src/client/common/terminal/service.ts | 93 +++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 250bab138065..e82a007128f3 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -9,7 +9,7 @@ import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ITerminalAutoActivation } from '../../terminals/types'; -import { ITerminalManager } from '../application/types'; +import { IApplicationShell, ITerminalManager } from '../application/types'; import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; import { IConfigurationService, IDisposableRegistry } from '../types'; import { @@ -24,6 +24,11 @@ import { sleep } from '../utils/async'; import { useEnvExtension } from '../../envExt/api.internal'; import { ensureTerminalLegacy } from '../../envExt/api.legacy'; +interface QueuedCommand { + commandLine: string; + resolve: (value: TerminalShellExecution | undefined) => void; +} + @injectable() export class TerminalService implements ITerminalService, Disposable { private terminal?: Terminal; @@ -33,8 +38,12 @@ export class TerminalService implements ITerminalService, Disposable { private terminalHelper: ITerminalHelper; private terminalActivator: ITerminalActivator; private terminalAutoActivator: ITerminalAutoActivation; + private applicationShell: IApplicationShell; private readonly executeCommandListeners: Set = new Set(); private _terminalFirstLaunched: boolean = true; + private pythonReplCommandQueue: QueuedCommand[] = []; + private isReplReady: boolean = false; + private replDataListener?: Disposable; public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } @@ -48,11 +57,13 @@ export class TerminalService implements ITerminalService, Disposable { this.terminalHelper = this.serviceContainer.get(ITerminalHelper); this.terminalManager = this.serviceContainer.get(ITerminalManager); this.terminalAutoActivator = this.serviceContainer.get(ITerminalAutoActivation); + this.applicationShell = this.serviceContainer.get(IApplicationShell); this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); this.terminalActivator = this.serviceContainer.get(ITerminalActivator); } public dispose() { this.terminal?.dispose(); + this.disposeReplListener(); if (this.executeCommandListeners && this.executeCommandListeners.size > 0) { this.executeCommandListeners.forEach((d) => { @@ -81,14 +92,76 @@ export class TerminalService implements ITerminalService, Disposable { commandLine: string, isPythonShell: boolean, ): Promise { - // TODO: First execution of shift+enter may get ignored when using sendText. - // Prevent Cannot read properties of undefined: https://github.com/microsoft/vscode-python-environments/issues/958 - if (!this.terminal) { - traceVerbose('Terminal not available yet, cannot execute command'); - return undefined; + if (isPythonShell) { + if (this.isReplReady) { + return this.executeCommandInternal(commandLine, true); + } + + // Queue command and start listening for REPL prompt if not already + return new Promise((resolve) => { + this.pythonReplCommandQueue.push({ commandLine, resolve }); + traceVerbose(`Queued Python REPL command: ${commandLine}`); + this.startReplListener(); + }); + } + + // For non-Python shell commands, execute directly + return this.executeCommandInternal(commandLine, isPythonShell); + } + + /** + * Starts listening for the Python REPL prompt (>>>). + * When detected, processes all queued commands. + */ + private startReplListener(): void { + if (this.replDataListener) { + return; } + let terminalData = ''; + + this.replDataListener = this.applicationShell.onDidWriteTerminalData((e) => { + if (this.terminal && e.terminal === this.terminal) { + terminalData += e.data; + // Check for Python REPL prompt (>>>) + if (/>>>\s*$/.test(terminalData)) { + traceVerbose('Python REPL ready, detected >>> prompt'); + this.isReplReady = true; + this.disposeReplListener(); + this.flushReplQueue(); + } + } + }); + } + + private disposeReplListener(): void { + if (this.replDataListener) { + this.replDataListener.dispose(); + this.replDataListener = undefined; + } + } + + private async flushReplQueue(): Promise { + while (this.pythonReplCommandQueue.length > 0) { + const cmd = this.pythonReplCommandQueue.shift(); + if (cmd) { + traceVerbose(`Executing queued REPL command: ${cmd.commandLine}`); + const result = await this.executeCommandInternal(cmd.commandLine, true); + cmd.resolve(result); + } + } + } + + private async executeCommandInternal( + commandLine: string, + isPythonShell: boolean, + ): Promise { const terminal = this.terminal; + if (!terminal) { + traceVerbose('Terminal not available, cannot execute command'); + return undefined; + } + if (!this.options?.hideFromUser) { terminal.show(true); } @@ -175,6 +248,14 @@ export class TerminalService implements ITerminalService, Disposable { if (terminal === this.terminal) { this.terminalClosed.fire(); this.terminal = undefined; + // Reset REPL state when terminal closes + this.isReplReady = false; + this.disposeReplListener(); + // Clear any pending commands + while (this.pythonReplCommandQueue.length > 0) { + const cmd = this.pythonReplCommandQueue.shift(); + cmd?.resolve(undefined); + } } } From 9f1d1793735635c512117da5c284a607be79fbd3 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 15:15:31 -0800 Subject: [PATCH 07/16] Try to fix tests --- .../common/terminals/service.unit.test.ts | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 246a599f17d6..61828b9d9a55 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -14,8 +14,9 @@ import { Uri, Terminal as VSCodeTerminal, WorkspaceConfiguration, + TerminalDataWriteEvent, } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IApplicationShell, ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; @@ -56,6 +57,8 @@ suite('Terminal Service', () => { let useEnvExtensionStub: sinon.SinonStub; let interpreterService: TypeMoq.IMock; let options: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + let onDidWriteTerminalDataEmitter: EventEmitter; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); @@ -118,6 +121,15 @@ suite('Terminal Service', () => { mockServiceContainer.setup((c) => c.get(ITerminalActivator)).returns(() => terminalActivator.object); mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object); mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + + // Setup IApplicationShell mock with onDidWriteTerminalData + applicationShell = TypeMoq.Mock.ofType(); + onDidWriteTerminalDataEmitter = new EventEmitter(); + applicationShell + .setup((a) => a.onDidWriteTerminalData) + .returns(() => onDidWriteTerminalDataEmitter.event); + mockServiceContainer.setup((c) => c.get(IApplicationShell)).returns(() => applicationShell.object); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); isWindowsStub = sinon.stub(platform, 'isWindows'); pythonConfig = TypeMoq.Mock.ofType(); @@ -230,10 +242,12 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + // Simulate REPL ready by firing the >>> prompt + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await service.executeCommand(textToSend, true); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); @@ -251,10 +265,12 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + // Simulate REPL ready by firing the >>> prompt + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await service.executeCommand(textToSend, true); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); @@ -273,10 +289,12 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + // Simulate REPL ready by firing the >>> prompt + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await service.executeCommand(textToSend, true); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); @@ -305,6 +323,8 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); + // Simulate REPL ready by firing the >>> prompt + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await service.executeCommand(textToSend, true); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); @@ -325,10 +345,12 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + // Simulate REPL ready by firing the >>> prompt + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await service.executeCommand(textToSend, true); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); From d79acf3288e47015113e2753d968c1a91c62863f Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 15:16:33 -0800 Subject: [PATCH 08/16] Better tests --- src/test/common/terminals/service.unit.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 61828b9d9a55..e1a67ad3fbab 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -125,9 +125,7 @@ suite('Terminal Service', () => { // Setup IApplicationShell mock with onDidWriteTerminalData applicationShell = TypeMoq.Mock.ofType(); onDidWriteTerminalDataEmitter = new EventEmitter(); - applicationShell - .setup((a) => a.onDidWriteTerminalData) - .returns(() => onDidWriteTerminalDataEmitter.event); + applicationShell.setup((a) => a.onDidWriteTerminalData).returns(() => onDidWriteTerminalDataEmitter.event); mockServiceContainer.setup((c) => c.get(IApplicationShell)).returns(() => applicationShell.object); getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); From c36320578da8c2893d83eb5cdd95a66aca3ed970 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 15:26:23 -0800 Subject: [PATCH 09/16] Pretend its real scenario --- .../common/terminals/service.unit.test.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index e1a67ad3fbab..e60f82e1a4fe 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -241,9 +241,10 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Simulate REPL ready by firing the >>> prompt + // Start executeCommand (sets up listener), then fire >>> prompt, then await + const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); - await service.executeCommand(textToSend, true); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); @@ -264,9 +265,10 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Simulate REPL ready by firing the >>> prompt + // Start executeCommand (sets up listener), then fire >>> prompt, then await + const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); - await service.executeCommand(textToSend, true); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); @@ -288,9 +290,10 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Simulate REPL ready by firing the >>> prompt + // Start executeCommand (sets up listener), then fire >>> prompt, then await + const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); - await service.executeCommand(textToSend, true); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); @@ -321,9 +324,10 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Simulate REPL ready by firing the >>> prompt + // Start executeCommand (sets up listener), then fire >>> prompt, then await + const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); - await service.executeCommand(textToSend, true); + await executePromise; terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); }); @@ -344,9 +348,10 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Simulate REPL ready by firing the >>> prompt + // Start executeCommand (sets up listener), then fire >>> prompt, then await + const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); - await service.executeCommand(textToSend, true); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); From 8fc330a7c4ae173e4d11dcf9b7923f7ef456b8ae Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 15:33:01 -0800 Subject: [PATCH 10/16] Better comments --- src/client/common/terminal/service.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index e82a007128f3..f5d0a70425af 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -105,14 +105,11 @@ export class TerminalService implements ITerminalService, Disposable { }); } - // For non-Python shell commands, execute directly + // Non-REPL code execution return this.executeCommandInternal(commandLine, isPythonShell); } - /** - * Starts listening for the Python REPL prompt (>>>). - * When detected, processes all queued commands. - */ + // Process Python code execution once REPL is detected. private startReplListener(): void { if (this.replDataListener) { return; From 1a1c2e70e89a115ba97717f0f46a956fa58fe5be Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 15:45:58 -0800 Subject: [PATCH 11/16] use string for queue type [] --- src/client/common/terminal/service.ts | 53 +++++++++------------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index f5d0a70425af..a9603fdb96ae 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -24,11 +24,6 @@ import { sleep } from '../utils/async'; import { useEnvExtension } from '../../envExt/api.internal'; import { ensureTerminalLegacy } from '../../envExt/api.legacy'; -interface QueuedCommand { - commandLine: string; - resolve: (value: TerminalShellExecution | undefined) => void; -} - @injectable() export class TerminalService implements ITerminalService, Disposable { private terminal?: Terminal; @@ -41,7 +36,7 @@ export class TerminalService implements ITerminalService, Disposable { private applicationShell: IApplicationShell; private readonly executeCommandListeners: Set = new Set(); private _terminalFirstLaunched: boolean = true; - private pythonReplCommandQueue: QueuedCommand[] = []; + private pythonReplCommandQueue: string[] = []; private isReplReady: boolean = false; private replDataListener?: Disposable; public get onDidCloseTerminal(): Event { @@ -94,19 +89,19 @@ export class TerminalService implements ITerminalService, Disposable { ): Promise { if (isPythonShell) { if (this.isReplReady) { - return this.executeCommandInternal(commandLine, true); - } - - // Queue command and start listening for REPL prompt if not already - return new Promise((resolve) => { - this.pythonReplCommandQueue.push({ commandLine, resolve }); - traceVerbose(`Queued Python REPL command: ${commandLine}`); + this.terminal?.sendText(commandLine); + traceVerbose(`Python REPL sendText: ${commandLine}`); + } else { + // Queue command to run once REPL is ready. + this.pythonReplCommandQueue.push(commandLine); + traceVerbose(`Python REPL queued command: ${commandLine}`); this.startReplListener(); - }); + } + return undefined; } // Non-REPL code execution - return this.executeCommandInternal(commandLine, isPythonShell); + return this.executeCommandInternal(commandLine); } // Process Python code execution once REPL is detected. @@ -138,21 +133,17 @@ export class TerminalService implements ITerminalService, Disposable { } } - private async flushReplQueue(): Promise { + private flushReplQueue(): void { while (this.pythonReplCommandQueue.length > 0) { - const cmd = this.pythonReplCommandQueue.shift(); - if (cmd) { - traceVerbose(`Executing queued REPL command: ${cmd.commandLine}`); - const result = await this.executeCommandInternal(cmd.commandLine, true); - cmd.resolve(result); + const commandLine = this.pythonReplCommandQueue.shift(); + if (commandLine) { + traceVerbose(`Executing queued REPL command: ${commandLine}`); + this.terminal?.sendText(commandLine); } } } - private async executeCommandInternal( - commandLine: string, - isPythonShell: boolean, - ): Promise { + private async executeCommandInternal(commandLine: string): Promise { const terminal = this.terminal; if (!terminal) { traceVerbose('Terminal not available, cannot execute command'); @@ -182,11 +173,7 @@ export class TerminalService implements ITerminalService, Disposable { await promise; } - if (isPythonShell) { - // Prevent KeyboardInterrupt in Python REPL: https://github.com/microsoft/vscode-python/issues/25468 - terminal.sendText(commandLine); - traceVerbose(`Python REPL detected, sendText: ${commandLine}`); - } else if (terminal.shellIntegration) { + if (terminal.shellIntegration) { const execution = terminal.shellIntegration.executeCommand(commandLine); traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`); return execution; @@ -248,11 +235,7 @@ export class TerminalService implements ITerminalService, Disposable { // Reset REPL state when terminal closes this.isReplReady = false; this.disposeReplListener(); - // Clear any pending commands - while (this.pythonReplCommandQueue.length > 0) { - const cmd = this.pythonReplCommandQueue.shift(); - cmd?.resolve(undefined); - } + this.pythonReplCommandQueue = []; } } From 57e9e98a015a3a5a80cce43a091d1faacb8e32d3 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 19:35:10 -0800 Subject: [PATCH 12/16] skip flaky tests --- src/test/smoke/smartSend.smoke.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts index 80eabf356330..cae41cc094d5 100644 --- a/src/test/smoke/smartSend.smoke.test.ts +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -19,11 +19,8 @@ suite('Smoke Test: Run Smart Selection and Advance Cursor', async () => { suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - // TODO: Re-enable this test once the flakiness on Windows is resolved - test('Smart Send', async function () { - if (process.platform === 'win32') { - return this.skip(); - } + // TODO: Re-enable this test once the flakiness on Windows, linux are resolved + test.skip('Smart Send', async function () { const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', From 48e75c3165c084e76439a9f35141eb06eb6b7dcf Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 23:25:01 -0800 Subject: [PATCH 13/16] Polish comments before merge --- src/client/common/terminal/service.ts | 4 ---- src/test/common/terminals/service.unit.test.ts | 5 ----- 2 files changed, 9 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index a9603fdb96ae..8e3c7370773b 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -104,18 +104,15 @@ export class TerminalService implements ITerminalService, Disposable { return this.executeCommandInternal(commandLine); } - // Process Python code execution once REPL is detected. private startReplListener(): void { if (this.replDataListener) { return; } let terminalData = ''; - this.replDataListener = this.applicationShell.onDidWriteTerminalData((e) => { if (this.terminal && e.terminal === this.terminal) { terminalData += e.data; - // Check for Python REPL prompt (>>>) if (/>>>\s*$/.test(terminalData)) { traceVerbose('Python REPL ready, detected >>> prompt'); this.isReplReady = true; @@ -232,7 +229,6 @@ export class TerminalService implements ITerminalService, Disposable { if (terminal === this.terminal) { this.terminalClosed.fire(); this.terminal = undefined; - // Reset REPL state when terminal closes this.isReplReady = false; this.disposeReplListener(); this.pythonReplCommandQueue = []; diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index e60f82e1a4fe..3f2581d43dfd 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -241,7 +241,6 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Start executeCommand (sets up listener), then fire >>> prompt, then await const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; @@ -265,7 +264,6 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Start executeCommand (sets up listener), then fire >>> prompt, then await const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; @@ -290,7 +288,6 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Start executeCommand (sets up listener), then fire >>> prompt, then await const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; @@ -324,7 +321,6 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Start executeCommand (sets up listener), then fire >>> prompt, then await const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; @@ -348,7 +344,6 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - // Start executeCommand (sets up listener), then fire >>> prompt, then await const executePromise = service.executeCommand(textToSend, true); onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; From ca8000a6c765a2cd5c1a68569a7ecd5deaa686c6 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 23:27:09 -0800 Subject: [PATCH 14/16] tests --- src/test/common/terminals/service.unit.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 3f2581d43dfd..1cb64374ea37 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -245,7 +245,7 @@ suite('Terminal Service', () => { onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); @@ -268,7 +268,7 @@ suite('Terminal Service', () => { onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); @@ -292,7 +292,7 @@ suite('Terminal Service', () => { onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); From e346387fd2bef7a81fd584ab109f42a673f9119b Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 15 Dec 2025 23:28:50 -0800 Subject: [PATCH 15/16] final --- src/test/common/terminals/service.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 1cb64374ea37..ea05b5194e88 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -348,7 +348,7 @@ suite('Terminal Service', () => { onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); await executePromise; - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); From 6ea7f82e5d1225a1093c2690aa40d27602dc2809 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Tue, 16 Dec 2025 09:31:27 -0800 Subject: [PATCH 16/16] Add more approach, to make experience faster --- .../common/application/terminalManager.ts | 3 ++ src/client/common/application/types.ts | 2 + src/client/common/terminal/service.ts | 41 ++++++++++++++----- src/client/common/vscodeApis/windowApis.ts | 5 +++ .../common/terminals/service.unit.test.ts | 31 +++++++++++++- 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/client/common/application/terminalManager.ts b/src/client/common/application/terminalManager.ts index 9d0536e85243..dc2603e84a56 100644 --- a/src/client/common/application/terminalManager.ts +++ b/src/client/common/application/terminalManager.ts @@ -38,6 +38,9 @@ export class TerminalManager implements ITerminalManager { public onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable { return window.onDidEndTerminalShellExecution(handler); } + public onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); + } } /** diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 65a8833a691c..34a95fb604f0 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -939,6 +939,8 @@ export interface ITerminalManager { onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable; onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable; + + onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable; } export const IDebugService = Symbol('IDebugManager'); diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 8e3c7370773b..0dffd5615ae1 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -38,7 +38,8 @@ export class TerminalService implements ITerminalService, Disposable { private _terminalFirstLaunched: boolean = true; private pythonReplCommandQueue: string[] = []; private isReplReady: boolean = false; - private replDataListener?: Disposable; + private replPromptListener?: Disposable; + private replShellTypeListener?: Disposable; public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } @@ -105,28 +106,48 @@ export class TerminalService implements ITerminalService, Disposable { } private startReplListener(): void { - if (this.replDataListener) { + if (this.replPromptListener || this.replShellTypeListener) { return; } + this.replShellTypeListener = this.terminalManager.onDidChangeTerminalState((terminal) => { + if (this.terminal && terminal === this.terminal) { + if (terminal.state.shell == 'python') { + traceVerbose('Python REPL ready from terminal shell api'); + this.onReplReady(); + } + } + }); + let terminalData = ''; - this.replDataListener = this.applicationShell.onDidWriteTerminalData((e) => { + this.replPromptListener = this.applicationShell.onDidWriteTerminalData((e) => { if (this.terminal && e.terminal === this.terminal) { terminalData += e.data; if (/>>>\s*$/.test(terminalData)) { - traceVerbose('Python REPL ready, detected >>> prompt'); - this.isReplReady = true; - this.disposeReplListener(); - this.flushReplQueue(); + traceVerbose('Python REPL ready, from >>> prompt detection'); + this.onReplReady(); } } }); } + private onReplReady(): void { + if (this.isReplReady) { + return; + } + this.isReplReady = true; + this.flushReplQueue(); + this.disposeReplListener(); + } + private disposeReplListener(): void { - if (this.replDataListener) { - this.replDataListener.dispose(); - this.replDataListener = undefined; + if (this.replPromptListener) { + this.replPromptListener.dispose(); + this.replPromptListener = undefined; + } + if (this.replShellTypeListener) { + this.replShellTypeListener.dispose(); + this.replShellTypeListener = undefined; } } diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index fade0a028487..90a06e7ed75a 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -25,6 +25,7 @@ import { NotebookDocument, NotebookEditor, NotebookDocumentShowOptions, + Terminal, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; import { Resource } from '../types'; @@ -124,6 +125,10 @@ export function onDidStartTerminalShellExecution(handler: (e: TerminalShellExecu return window.onDidStartTerminalShellExecution(handler); } +export function onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); +} + export enum MultiStepAction { Back = 'Back', Cancel = 'Cancel', diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index ea05b5194e88..3a6d54c9390b 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -59,6 +59,7 @@ suite('Terminal Service', () => { let options: TypeMoq.IMock; let applicationShell: TypeMoq.IMock; let onDidWriteTerminalDataEmitter: EventEmitter; + let onDidChangeTerminalStateEmitter: EventEmitter; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); @@ -122,12 +123,16 @@ suite('Terminal Service', () => { mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object); mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); - // Setup IApplicationShell mock with onDidWriteTerminalData applicationShell = TypeMoq.Mock.ofType(); onDidWriteTerminalDataEmitter = new EventEmitter(); applicationShell.setup((a) => a.onDidWriteTerminalData).returns(() => onDidWriteTerminalDataEmitter.event); mockServiceContainer.setup((c) => c.get(IApplicationShell)).returns(() => applicationShell.object); + onDidChangeTerminalStateEmitter = new EventEmitter(); + terminalManager + .setup((t) => t.onDidChangeTerminalState(TypeMoq.It.isAny())) + .returns((handler) => onDidChangeTerminalStateEmitter.event(handler)); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); isWindowsStub = sinon.stub(platform, 'isWindows'); pythonConfig = TypeMoq.Mock.ofType(); @@ -352,6 +357,30 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); + test('Ensure REPL ready when onDidChangeTerminalState fires with python shell', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + + terminal.setup((t) => t.state).returns(() => ({ isInteractedWith: true, shell: 'python' })); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidChangeTerminalStateEmitter.fire(terminal.object); + await executePromise; + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + test('Ensure terminal is not shown if `hideFromUser` option is set to `true`', async () => { terminalHelper .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()))