Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/common/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export namespace VenvManagerStrings {
export const venvErrorNoBasePython = l10n.t('No base Python found');
export const venvErrorNoPython3 = l10n.t('Did not find any base Python 3');

export const noEnvClickToCreate = l10n.t('No environment found, click to create');
export const noEnvFound = l10n.t('No Python environments found.');
export const createEnvironment = l10n.t('Create Environment');

export const venvName = l10n.t('Enter a name for the virtual environment');
export const venvNameErrorEmpty = l10n.t('Name cannot be empty');
export const venvNameErrorExists = l10n.t('A folder with the same name already exists');
Expand Down Expand Up @@ -206,3 +210,32 @@ export namespace ActivationStrings {
);
export const activatingEnvironment = l10n.t('Activating environment');
}

export namespace UvInstallStrings {
export const noPythonFound = l10n.t('No Python installation found');
export const installPythonPrompt = l10n.t('No Python found. Would you like to install Python using uv?');
export const installPythonAndUvPrompt = l10n.t(
'No Python found. Would you like to install uv and use it to install Python?',
);
export const installPython = l10n.t('Install Python');
export const installingUv = l10n.t('Installing uv...');
export const installingPython = l10n.t('Installing Python via uv...');
export const installComplete = l10n.t('Python installed successfully');
export function installCompleteWithDetails(version: string, path: string): string {
return l10n.t('Python {0} installed successfully at {1}', version, path);
}
export function installCompleteWithPath(path: string): string {
return l10n.t('Python installed successfully at {0}', path);
}
export const installFailed = l10n.t('Failed to install Python');
export const uvInstallFailed = l10n.t('Failed to install uv');
export const uvInstallRestartRequired = l10n.t(
'uv was installed but may not be available in the current terminal. Please restart VS Code or open a new terminal and try again.',
);
export const dontAskAgain = l10n.t("Don't ask again");
export const clickToInstallPython = l10n.t('No Python found, click to install');
export const selectPythonVersion = l10n.t('Select Python version to install');
export const installed = l10n.t('installed');
export const fetchingVersions = l10n.t('Fetching available Python versions...');
export const failedToFetchVersions = l10n.t('Failed to fetch available Python versions');
}
10 changes: 9 additions & 1 deletion src/common/tasks.apis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Task, TaskExecution, tasks } from 'vscode';
import { Disposable, Task, TaskExecution, TaskProcessEndEvent, tasks } from 'vscode';

export async function executeTask(task: Task): Promise<TaskExecution> {
return tasks.executeTask(task);
}

export function onDidEndTaskProcess(
listener: (e: TaskProcessEndEvent) => unknown,
thisArgs?: unknown,
disposables?: Disposable[],
): Disposable {
return tasks.onDidEndTaskProcess(listener, thisArgs, disposables);
}
43 changes: 41 additions & 2 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export enum EventNames {
VENV_USING_UV = 'VENV.USING_UV',
VENV_CREATION = 'VENV.CREATION',

UV_PYTHON_INSTALL_PROMPTED = 'UV.PYTHON_INSTALL_PROMPTED',
UV_PYTHON_INSTALL_STARTED = 'UV.PYTHON_INSTALL_STARTED',
UV_PYTHON_INSTALL_COMPLETED = 'UV.PYTHON_INSTALL_COMPLETED',
UV_PYTHON_INSTALL_FAILED = 'UV.PYTHON_INSTALL_FAILED',

PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT',
ADD_PROJECT = 'ADD_PROJECT',
/**
Expand Down Expand Up @@ -83,15 +88,49 @@ export interface IEventNamePropertyMapping {
/* __GDPR__
"venv.using_uv": {"owner": "eleanorjboyd" }
*/
[EventNames.VENV_USING_UV]: never | undefined /* __GDPR__
[EventNames.VENV_USING_UV]: never | undefined;

/* __GDPR__
"venv.creation": {
"creationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
}
*/;
*/
[EventNames.VENV_CREATION]: {
creationType: 'quick' | 'custom';
};

/* __GDPR__
"uv.python_install_prompted": {
"trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
}
*/
[EventNames.UV_PYTHON_INSTALL_PROMPTED]: {
trigger: 'activation' | 'createEnvironment';
};

/* __GDPR__
"uv.python_install_started": {
"uvAlreadyInstalled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
}
*/
[EventNames.UV_PYTHON_INSTALL_STARTED]: {
uvAlreadyInstalled: boolean;
};

/* __GDPR__
"uv.python_install_completed": {"owner": "karthiknadig" }
*/
[EventNames.UV_PYTHON_INSTALL_COMPLETED]: never | undefined;

/* __GDPR__
"uv.python_install_failed": {
"stage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
}
*/
[EventNames.UV_PYTHON_INSTALL_FAILED]: {
stage: 'uvInstall' | 'uvNotOnPath' | 'pythonInstall' | 'findPath';
};

/* __GDPR__
"package_management": {
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
Expand Down
20 changes: 12 additions & 8 deletions src/features/views/treeViewItems.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { EnvironmentGroupInfo, IconPath, Package, PythonEnvironment, PythonProject } from '../../api';
import { EnvViewStrings } from '../../common/localize';
import { EnvViewStrings, UvInstallStrings, VenvManagerStrings } from '../../common/localize';
import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api';
import { isActivatableEnvironment } from '../common/activation';
import { removable } from './utils';
Expand Down Expand Up @@ -115,20 +115,24 @@ export class NoPythonEnvTreeItem implements EnvTreeItem {
private readonly tooltip?: string | MarkdownString,
private readonly iconPath?: string | IconPath,
) {
const item = new TreeItem(
this.parent.manager.supportsCreate
? 'No environment found, click to create'
: 'No python environments found.',
TreeItemCollapsibleState.None,
);
// Use special message for system manager (Python installation)
const isSystemManager = this.parent.manager.name === 'system';
let label: string;
if (this.parent.manager.supportsCreate) {
label = isSystemManager ? UvInstallStrings.clickToInstallPython : VenvManagerStrings.noEnvClickToCreate;
} else {
label = VenvManagerStrings.noEnvFound;
}

const item = new TreeItem(label, TreeItemCollapsibleState.None);
item.contextValue = 'python-no-environment';
item.description = this.description;
item.tooltip = this.tooltip;
item.iconPath = this.iconPath ?? new ThemeIcon('circle-slash');
if (this.parent.manager.supportsCreate) {
item.command = {
command: 'python-envs.create',
title: 'Create Environment',
title: isSystemManager ? UvInstallStrings.installPython : VenvManagerStrings.createEnvironment,
arguments: [this.parent],
};
}
Comment on lines +118 to 138
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NoPythonEnvTreeItem now has conditional labeling/command title for the system manager (install Python via uv) vs other managers (create env). There are existing unit tests for this file, but none cover this new branch. Please add tests that assert the label and command title for both manager.name === 'system' and non-system managers when supportsCreate is true/false.

Copilot uses AI. Check for mistakes.
Expand Down
51 changes: 51 additions & 0 deletions src/managers/builtin/sysPythonManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as path from 'path';
import { EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, ThemeIcon, Uri, window } from 'vscode';
import {
CreateEnvironmentOptions,
CreateEnvironmentScope,
DidChangeEnvironmentEventArgs,
DidChangeEnvironmentsEventArgs,
EnvironmentChangeKind,
Expand Down Expand Up @@ -28,6 +30,7 @@ import {
setSystemEnvForWorkspaces,
} from './cache';
import { refreshPythons, resolveSystemPythonEnvironmentPath } from './utils';
import { installPythonWithUv, promptInstallPythonViaUv, selectPythonVersionToInstall } from './uvPythonInstaller';

export class SysPythonManager implements EnvironmentManager {
private collection: PythonEnvironment[] = [];
Expand Down Expand Up @@ -70,6 +73,23 @@ export class SysPythonManager implements EnvironmentManager {

await this.internalRefresh(false, SysManagerStrings.sysManagerDiscovering);

// If no Python environments were found, offer to install via uv
if (this.collection.length === 0) {
const pythonPath = await promptInstallPythonViaUv('activation', this.log);
if (pythonPath) {
const resolved = await resolveSystemPythonEnvironmentPath(
pythonPath,
this.nativeFinder,
this.api,
this,
);
if (resolved) {
this.collection.push(resolved);
this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]);
}
}
}
Comment on lines +76 to +91
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After installing Python via uv during initialize(), the resolved environment is pushed into collection and an environments-changed event is fired, but globalEnv/fsPathToEnv are not updated. This can leave get(undefined) returning undefined even though a system Python now exists. Consider setting this.globalEnv (and persisting via setSystemEnvForGlobal or calling this.set(undefined, resolved)) and/or re-running loadEnvMap() after adding the environment.

This issue also appears on line 256 of the same file.

Copilot uses AI. Check for mistakes.

this._initialized.resolve();
}

Expand Down Expand Up @@ -218,6 +238,37 @@ export class SysPythonManager implements EnvironmentManager {
return resolved;
}

/**
* Installs a global Python using uv.
* This method shows a QuickPick to select the Python version, then installs it.
*/
async create(
_scope: CreateEnvironmentScope,
_options?: CreateEnvironmentOptions,
): Promise<PythonEnvironment | undefined> {
// Show QuickPick to select Python version
const selectedVersion = await selectPythonVersionToInstall();
if (!selectedVersion) {
// User cancelled
return undefined;
}

const pythonPath = await installPythonWithUv(this.log, selectedVersion);

if (pythonPath) {
// Resolve the installed Python using NativePythonFinder instead of full refresh
const resolved = await resolveSystemPythonEnvironmentPath(pythonPath, this.nativeFinder, this.api, this);
if (resolved) {
// Add to collection and fire change event
this.collection.push(resolved);
this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]);
return resolved;
}
}

return undefined;
}

async clearCache(): Promise<void> {
await clearSystemEnvCache();
}
Expand Down
Loading
Loading