From 7d18e5d2025943c7af35ac021b8adcd0c5c3e208 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 13 Jan 2026 18:54:59 -0800 Subject: [PATCH 1/7] Shell integration activate from Python extension --- package.json | 6 + package.nls.json | 1 + src/client/common/configSettings.ts | 1 + src/client/common/types.ts | 1 + .../envCollectionActivation/service.ts | 230 ++++++++++++++++++ 5 files changed, 239 insertions(+) diff --git a/package.json b/package.json index 544c72dba023..b6f8782076a0 100644 --- a/package.json +++ b/package.json @@ -622,6 +622,12 @@ "scope": "resource", "type": "boolean" }, + "python.terminal.shellIntegration.activate": { + "default": false, + "markdownDescription": "%python.terminal.shellIntegration.activate.description%", + "scope": "resource", + "type": "boolean" + }, "python.terminal.executeInFileDir": { "default": false, "description": "%python.terminal.executeInFileDir.description%", diff --git a/package.nls.json b/package.nls.json index 57f2ed95b2c0..aa9eff82d7e9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -76,6 +76,7 @@ "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things. Note: PyREPL (available in Python 3.13+) is automatically disabled when shell integration is enabled to avoid cursor indentation issues.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", + "python.terminal.shellIntegration.activate.description": "When enabled, activates Python environments by contributing shell-specific environment variables (e.g., `VSCODE_PYTHON_BASH_ACTIVATE`, `VSCODE_PYTHON_ZSH_ACTIVATE`, `VSCODE_PYTHON_FISH_ACTIVATE`, `VSCODE_PYTHON_PS1_ACTIVATE`, `VSCODE_PYTHON_CMD_ACTIVATE`) pointing to the activate script, instead of sending activation commands directly to the terminal. Supports bash, zsh, fish, PowerShell, and Command Prompt.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 91c06d9331fd..b77788d672da 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -379,6 +379,7 @@ export class PythonSettings implements IPythonSettings { activateEnvInCurrentTerminal: false, shellIntegration: { enabled: false, + activate: false, }, }; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index c30ad704b6c1..08a7dd406043 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -190,6 +190,7 @@ export interface ITerminalSettings { readonly activateEnvInCurrentTerminal: boolean; readonly shellIntegration: { enabled: boolean; + activate: boolean; }; } diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 2ce8d5d5d86a..5ed9dcb3ebcf 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -97,6 +97,14 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { try { + // Check shellIntegration.activate first - this should work regardless of + // env extension or terminalEnvVar experiment + const settings = this.configurationService.getSettings(resource); + if (settings.terminal.shellIntegration.activate) { + await this.activateUsingEnvVar(resource); + return; + } + if (useEnvExtension()) { traceVerbose('Ignoring environment variable experiment since env extension is being used'); this.context.environmentVariableCollection.clear(); @@ -170,6 +178,50 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } + /** + * Activates environments using shell-specific environment variables (e.g., VSCODE_PYTHON_BASH_ACTIVATE). + * This method works independently of the env extension or terminalEnvVar experiment. + */ + private async activateUsingEnvVar(resource: Resource): Promise { + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + const settings = this.configurationService.getSettings(r); + if (settings.terminal.shellIntegration.activate) { + await this.applyActivateEnvVarForResource(r).ignoreErrors(); + } + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + const settings = this.configurationService.getSettings(resource); + if (settings.terminal.shellIntegration.activate) { + await this.applyActivateEnvVarForResource(resource, shell).ignoreErrors(); + } + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + await this.applyActivateEnvVarForResource(resource); + await registerPythonStartup(this.context); + } + + /** + * Applies the shell-specific activate environment variable for a given resource. + */ + private async applyActivateEnvVarForResource( + resource: Resource, + shell = this.applicationEnvironment.shell, + ): Promise { + const workspaceFolder = this.getWorkspaceFolder(resource); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + await this.applyActivateEnvVar(resource, shell, envVarCollection); + } + public async _applyCollection(resource: Resource, shell?: string): Promise { this.progressService.showProgress({ location: ProgressLocation.Window, @@ -197,6 +249,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, @@ -308,6 +361,183 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ }); } + /** + * Applies the VSCODE_PYTHON_*_ACTIVATE environment variables to enable activation + * by contributing the full activation command via environment variable + * instead of sending activation commands directly to the terminal. + * Sets ALL shell-specific variables at once so any shell can be activated. + * Supports bash, zsh, fish, PowerShell, and Command Prompt. + */ + private async applyActivateEnvVar( + resource: Resource, + _shell: string, + envVarCollection: ReturnType, + ): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + traceVerbose('No interpreter found for shell integration activation'); + envVarCollection.clear(); + return; + } + + // For virtual environments, get the bin directory + if (interpreter.envType !== EnvironmentType.Venv && interpreter.type !== PythonEnvType.Virtual) { + traceVerbose('Shell integration activation only supports virtual environments'); + envVarCollection.clear(); + return; + } + + const binDir = path.dirname(interpreter.path); + + // Clear any previously set env vars + envVarCollection.clear(); + + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + + // Set ALL shell-specific environment variables at once + // Bash + const bashActivate = path.join(binDir, 'activate'); + if (await pathExists(bashActivate)) { + const bashCommand = `source ${bashActivate}`; + traceLog(`Setting VSCODE_PYTHON_BASH_ACTIVATE to ${bashCommand}`); + envVarCollection.replace('VSCODE_PYTHON_BASH_ACTIVATE', bashCommand, options); + + // ZSH uses the same activate script + traceLog(`Setting VSCODE_PYTHON_ZSH_ACTIVATE to ${bashCommand}`); + envVarCollection.replace('VSCODE_PYTHON_ZSH_ACTIVATE', bashCommand, options); + } + + // Fish + const fishActivate = path.join(binDir, 'activate.fish'); + if (await pathExists(fishActivate)) { + const fishCommand = `source ${fishActivate}`; + traceLog(`Setting VSCODE_PYTHON_FISH_ACTIVATE to ${fishCommand}`); + envVarCollection.replace('VSCODE_PYTHON_FISH_ACTIVATE', fishCommand, options); + } + + // PowerShell + const pwshActivate = path.join(binDir, 'Activate.ps1'); + if (await pathExists(pwshActivate)) { + const pwshCommand = `& ${pwshActivate}`; + traceLog(`Setting VSCODE_PYTHON_PWSH_ACTIVATE to ${pwshCommand}`); + envVarCollection.replace('VSCODE_PYTHON_PWSH_ACTIVATE', pwshCommand, options); + } + + // Command Prompt + const cmdActivate = path.join(binDir, 'activate.bat'); + if (await pathExists(cmdActivate)) { + traceLog(`Setting VSCODE_PYTHON_CMD_ACTIVATE to ${cmdActivate}`); + envVarCollection.replace('VSCODE_PYTHON_CMD_ACTIVATE', cmdActivate, options); + } + + const workspaceFolder = this.getWorkspaceFolder(resource); + const settings = this.configurationService.getSettings(resource); + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); + const description = new MarkdownString( + `${Interpreters.activateTerminalDescription} \`${displayPath}\` (via shell integration)`, + ); + envVarCollection.description = description; + } + + /** + * Builds the full activation command for the given shell type and script path. + */ + private buildActivateCommand(shellType: TerminalShellType, scriptPath: string): string { + switch (shellType) { + case TerminalShellType.bash: + case TerminalShellType.gitbash: + case TerminalShellType.wsl: + case TerminalShellType.zsh: + case TerminalShellType.fish: + return `source ${scriptPath}`; + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return `& ${scriptPath}`; + case TerminalShellType.commandPrompt: + return scriptPath; + default: + return `source ${scriptPath}`; + } + } + + /** + * Returns the environment variable name for shell integration activation based on shell type. + * Only supports bash, fish, PowerShell, and Command Prompt. + */ + private getShellActivateEnvVarName(shellType: TerminalShellType): string | undefined { + switch (shellType) { + case TerminalShellType.bash: + case TerminalShellType.gitbash: + case TerminalShellType.wsl: + return 'VSCODE_PYTHON_BASH_ACTIVATE'; + case TerminalShellType.zsh: + return 'VSCODE_PYTHON_ZSH_ACTIVATE'; + case TerminalShellType.fish: + return 'VSCODE_PYTHON_FISH_ACTIVATE'; + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'VSCODE_PYTHON_PS1_ACTIVATE'; + case TerminalShellType.commandPrompt: + return 'VSCODE_PYTHON_CMD_ACTIVATE'; + default: + return undefined; + } + } + + /** + * Gets the path to the activate script for the given interpreter. + */ + private async getActivateScriptPath(interpreter: PythonEnvironment, shell: string): Promise { + const shellType = identifyShellFromShellPath(shell); + + // For virtual environments, look for activate script in bin directory + if (interpreter.envType === EnvironmentType.Venv || interpreter.type === PythonEnvType.Virtual) { + const binDir = path.dirname(interpreter.path); + const activateScripts = this.getActivateScriptNames(shellType); + + if (!activateScripts) { + return undefined; + } + + for (const scriptName of activateScripts) { + const scriptPath = path.join(binDir, scriptName); + if (await pathExists(scriptPath)) { + return scriptPath; + } + } + } + + // For conda environments, we would need a different approach + // For now, return undefined for unsupported environment types + return undefined; + } + + /** + * Returns the activate script names to look for based on shell type. + * Only supports bash, fish, PowerShell, and Command Prompt. + */ + private getActivateScriptNames(shellType: TerminalShellType): string[] | undefined { + switch (shellType) { + case TerminalShellType.bash: + case TerminalShellType.gitbash: + case TerminalShellType.zsh: + case TerminalShellType.wsl: + return ['activate', 'activate.sh']; + case TerminalShellType.fish: + return ['activate.fish']; + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return ['Activate.ps1']; + case TerminalShellType.commandPrompt: + return ['activate.bat']; + default: + return undefined; + } + } + private isPromptSet = new Map(); // eslint-disable-next-line class-methods-use-this From 52ec9cfbc849422a3dbdd3daed5279e4d638a159 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 14 Jan 2026 09:32:08 -0800 Subject: [PATCH 2/7] TODO --- src/client/terminals/envCollectionActivation/service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 5ed9dcb3ebcf..d50174977915 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -97,6 +97,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { try { + // TODO: Consider activating from somewhere else since we are removing `terminalEnvVar` experiment // Check shellIntegration.activate first - this should work regardless of // env extension or terminalEnvVar experiment const settings = this.configurationService.getSettings(resource); From 95fbc10b3f7acb5cd46ec9a2c52a47da6ff4935c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 14 Jan 2026 09:33:54 -0800 Subject: [PATCH 3/7] Rename --- src/client/terminals/envCollectionActivation/service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index d50174977915..20e28ec0328a 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -102,7 +102,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // env extension or terminalEnvVar experiment const settings = this.configurationService.getSettings(resource); if (settings.terminal.shellIntegration.activate) { - await this.activateUsingEnvVar(resource); + await this.activateUsingShellIntegrationEnvVar(resource); return; } @@ -181,9 +181,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ /** * Activates environments using shell-specific environment variables (e.g., VSCODE_PYTHON_BASH_ACTIVATE). - * This method works independently of the env extension or terminalEnvVar experiment. */ - private async activateUsingEnvVar(resource: Resource): Promise { + private async activateUsingShellIntegrationEnvVar(resource: Resource): Promise { if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { From 22729bd0dbe733f45390883ad44f6ac4bc1826b2 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 14 Jan 2026 09:34:48 -0800 Subject: [PATCH 4/7] Remove unnnessary --- .../envCollectionActivation/service.ts | 96 ------------------- 1 file changed, 96 deletions(-) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 20e28ec0328a..c7964f967403 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -442,102 +442,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.description = description; } - /** - * Builds the full activation command for the given shell type and script path. - */ - private buildActivateCommand(shellType: TerminalShellType, scriptPath: string): string { - switch (shellType) { - case TerminalShellType.bash: - case TerminalShellType.gitbash: - case TerminalShellType.wsl: - case TerminalShellType.zsh: - case TerminalShellType.fish: - return `source ${scriptPath}`; - case TerminalShellType.powershell: - case TerminalShellType.powershellCore: - return `& ${scriptPath}`; - case TerminalShellType.commandPrompt: - return scriptPath; - default: - return `source ${scriptPath}`; - } - } - - /** - * Returns the environment variable name for shell integration activation based on shell type. - * Only supports bash, fish, PowerShell, and Command Prompt. - */ - private getShellActivateEnvVarName(shellType: TerminalShellType): string | undefined { - switch (shellType) { - case TerminalShellType.bash: - case TerminalShellType.gitbash: - case TerminalShellType.wsl: - return 'VSCODE_PYTHON_BASH_ACTIVATE'; - case TerminalShellType.zsh: - return 'VSCODE_PYTHON_ZSH_ACTIVATE'; - case TerminalShellType.fish: - return 'VSCODE_PYTHON_FISH_ACTIVATE'; - case TerminalShellType.powershell: - case TerminalShellType.powershellCore: - return 'VSCODE_PYTHON_PS1_ACTIVATE'; - case TerminalShellType.commandPrompt: - return 'VSCODE_PYTHON_CMD_ACTIVATE'; - default: - return undefined; - } - } - - /** - * Gets the path to the activate script for the given interpreter. - */ - private async getActivateScriptPath(interpreter: PythonEnvironment, shell: string): Promise { - const shellType = identifyShellFromShellPath(shell); - - // For virtual environments, look for activate script in bin directory - if (interpreter.envType === EnvironmentType.Venv || interpreter.type === PythonEnvType.Virtual) { - const binDir = path.dirname(interpreter.path); - const activateScripts = this.getActivateScriptNames(shellType); - - if (!activateScripts) { - return undefined; - } - - for (const scriptName of activateScripts) { - const scriptPath = path.join(binDir, scriptName); - if (await pathExists(scriptPath)) { - return scriptPath; - } - } - } - - // For conda environments, we would need a different approach - // For now, return undefined for unsupported environment types - return undefined; - } - - /** - * Returns the activate script names to look for based on shell type. - * Only supports bash, fish, PowerShell, and Command Prompt. - */ - private getActivateScriptNames(shellType: TerminalShellType): string[] | undefined { - switch (shellType) { - case TerminalShellType.bash: - case TerminalShellType.gitbash: - case TerminalShellType.zsh: - case TerminalShellType.wsl: - return ['activate', 'activate.sh']; - case TerminalShellType.fish: - return ['activate.fish']; - case TerminalShellType.powershell: - case TerminalShellType.powershellCore: - return ['Activate.ps1']; - case TerminalShellType.commandPrompt: - return ['activate.bat']; - default: - return undefined; - } - } - private isPromptSet = new Map(); // eslint-disable-next-line class-methods-use-this From c0805fda48eda5ba36c63a828f43f51be880a6bd Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 14 Jan 2026 09:48:24 -0800 Subject: [PATCH 5/7] setting descr --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index aa9eff82d7e9..be68ad9a8fef 100644 --- a/package.nls.json +++ b/package.nls.json @@ -76,7 +76,7 @@ "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things. Note: PyREPL (available in Python 3.13+) is automatically disabled when shell integration is enabled to avoid cursor indentation issues.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", - "python.terminal.shellIntegration.activate.description": "When enabled, activates Python environments by contributing shell-specific environment variables (e.g., `VSCODE_PYTHON_BASH_ACTIVATE`, `VSCODE_PYTHON_ZSH_ACTIVATE`, `VSCODE_PYTHON_FISH_ACTIVATE`, `VSCODE_PYTHON_PS1_ACTIVATE`, `VSCODE_PYTHON_CMD_ACTIVATE`) pointing to the activate script, instead of sending activation commands directly to the terminal. Supports bash, zsh, fish, PowerShell, and Command Prompt.", + "python.terminal.shellIntegration.activate.description": "Activate Python environment using shell integration. This means activating without sending activate script in your terminal, preserving shell integration capabilities and activated Copilot terminals.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", From 96d8f8f6e10923598c89da7d052171a5583e3a03 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 14 Jan 2026 10:29:48 -0800 Subject: [PATCH 6/7] dqwdwqd --- src/client/terminals/envCollectionActivation/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index c7964f967403..3d891c67eb11 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -381,8 +381,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } // For virtual environments, get the bin directory + // TODO: Make sure we test+get it working for conda, etc as well. if (interpreter.envType !== EnvironmentType.Venv && interpreter.type !== PythonEnvType.Virtual) { - traceVerbose('Shell integration activation only supports virtual environments'); envVarCollection.clear(); return; } From d747391ca0063f88e585229dea355933c9b50e94 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 14 Jan 2026 10:32:09 -0800 Subject: [PATCH 7/7] more todo --- src/client/terminals/envCollectionActivation/service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 3d891c67eb11..bba8f6471d62 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -380,8 +380,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } - // For virtual environments, get the bin directory - // TODO: Make sure we test+get it working for conda, etc as well. + // TODO, important: Make sure we test+get it working for conda, etc as well. if (interpreter.envType !== EnvironmentType.Venv && interpreter.type !== PythonEnvType.Virtual) { envVarCollection.clear(); return; @@ -427,6 +426,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } // Command Prompt + // TODO: We need to modify user's shell init for cmd, shell integration doesnt work for cmd const cmdActivate = path.join(binDir, 'activate.bat'); if (await pathExists(cmdActivate)) { traceLog(`Setting VSCODE_PYTHON_CMD_ACTIVATE to ${cmdActivate}`);