Skip to content

Ability to track the user selected Environment #25090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2025
Merged
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
2 changes: 0 additions & 2 deletions src/client/chat/installPackagesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { resolveFilePath } from './utils';
import { IModuleInstaller } from '../common/installer/types';
import { ModuleInstallerType } from '../pythonEnvironments/info';
import { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
import { trackEnvUsedByTool } from './lastUsedEnvs';

export interface IInstallPackageArgs {
resourcePath?: string;
Expand Down Expand Up @@ -67,7 +66,6 @@ export class InstallPackagesTool implements LanguageModelTool<IInstallPackageArg
for (const packageName of options.input.packageList) {
await installer.installModule(packageName, resourcePath, token, undefined, { installAsProcess: true });
}
trackEnvUsedByTool(resourcePath, environment);
Copy link
Author

Choose a reason for hiding this comment

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

Old code no longer required

// format and return
const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join(', ')}`;
return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]);
Expand Down
81 changes: 0 additions & 81 deletions src/client/chat/lastUsedEnvs.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/client/chat/listPackagesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { parsePipList } from './pipListUtils';
import { Conda } from '../pythonEnvironments/common/environmentManagers/conda';
import { traceError } from '../logging';
import { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
import { trackEnvUsedByTool } from './lastUsedEnvs';

export interface IResourceReference {
resourcePath?: string;
Expand Down Expand Up @@ -109,7 +108,6 @@ export async function getPythonPackagesResponse(
if (!packages.length) {
return 'No packages found';
}
trackEnvUsedByTool(resourcePath, environment);
// Installed Python packages, each in the format <name> or <name> (<version>). The version may be omitted if unknown. Returns an empty array if no packages are installed.
const response = [
'Below is a list of the Python packages, each in the format <name> or <name> (<version>). The version may be omitted if unknown: ',
Expand Down
2 changes: 0 additions & 2 deletions src/client/chat/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { PythonExtension, ResolvedEnvironment } from '../api/types';
import { ITerminalHelper, TerminalShellType } from '../common/terminal/types';
import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution';
import { Conda } from '../pythonEnvironments/common/environmentManagers/conda';
import { trackEnvUsedByTool } from './lastUsedEnvs';

export function resolveFilePath(filepath?: string): Uri | undefined {
if (!filepath) {
Expand Down Expand Up @@ -71,7 +70,6 @@ export async function getEnvironmentDetails(
getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper),
token,
);
trackEnvUsedByTool(resourcePath, environment);
const message = [
`Following is the information about the Python environment:`,
`1. Environment Type: ${environment.environment?.type || 'unknown'}`,
Expand Down
23 changes: 23 additions & 0 deletions src/client/common/utils/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,26 @@ export async function waitForCondition(
}, 10);
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isPromiseLike<T>(v: any): v is PromiseLike<T> {
return typeof v?.then === 'function';
}

export function raceTimeout<T>(timeout: number, ...promises: Promise<T>[]): Promise<T | undefined>;
export function raceTimeout<T>(timeout: number, defaultValue: T, ...promises: Promise<T>[]): Promise<T>;
export function raceTimeout<T>(timeout: number, defaultValue: T, ...promises: Promise<T>[]): Promise<T> {
const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue;
if (isPromiseLike(defaultValue)) {
promises.push((defaultValue as unknown) as Promise<T>);
}

let promiseResolve: ((value: T) => void) | undefined = undefined;

const timer = setTimeout(() => promiseResolve?.((resolveValue as unknown) as T), timeout);

return Promise.race([
Promise.race(promises).finally(() => clearTimeout(timer)),
new Promise<T>((resolve) => (promiseResolve = resolve)),
]);
}
4 changes: 4 additions & 0 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { ProposedExtensionAPI } from './proposedApiTypes';
import { buildProposedApi } from './proposedApi';
import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState';
import { registerTools } from './chat';
import { IRecommendedEnvironmentService } from './interpreter/configuration/types';

durations.codeLoadingTime = stopWatch.elapsedTime;

Expand Down Expand Up @@ -164,6 +165,9 @@ async function activateUnsafe(
);
const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer);
registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer);
ext.legacyIOC.serviceContainer
.get<IRecommendedEnvironmentService>(IRecommendedEnvironmentService)
.registerEnvApi(api.environments);
return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,8 +565,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
return Promise.resolve();
}

/**
* @returns true when an interpreter was set, undefined if the user cancelled the quickpick.
*/
@captureTelemetry(EventName.SELECT_INTERPRETER)
public async setInterpreter(): Promise<void> {
public async setInterpreter(): Promise<true | undefined> {
const targetConfig = await this.getConfigTargets();
if (!targetConfig) {
return;
Expand All @@ -588,6 +591,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
if (useEnvExtension()) {
await setInterpreterLegacy(interpreterState.path, wkspace);
}
return true;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import { PythonInterpreterTelemetry } from '../../telemetry/types';
import { IComponentAdapter } from '../contracts';
import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types';
import {
IRecommendedEnvironmentService,
IPythonPathUpdaterServiceFactory,
IPythonPathUpdaterServiceManager,
} from './types';

@injectable()
export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager {
constructor(
@inject(IPythonPathUpdaterServiceFactory)
private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory,
@inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter,
@inject(IRecommendedEnvironmentService) private readonly preferredEnvService: IRecommendedEnvironmentService,
) {}

public async updatePythonPath(
Expand All @@ -28,6 +33,9 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage
let failed = false;
try {
await pythonPathUpdater.updatePythonPath(pythonPath);
if (trigger === 'ui') {
Copy link
Author

Choose a reason for hiding this comment

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

I believe this is the right spot for tracking what env is selected by the user.

this.preferredEnvService.trackUserSelectedEnvironment(pythonPath, wkspace);
}
} catch (err) {
failed = true;
const reason = err as Error;
Expand Down
102 changes: 102 additions & 0 deletions src/client/interpreter/configuration/recommededEnvironmentService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { IRecommendedEnvironmentService } from './types';
import { PythonExtension } from '../../api/types';
import { IExtensionContext, Resource } from '../../common/types';
import { Uri, workspace } from 'vscode';
import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState';
import { traceError } from '../../logging';

const MEMENTO_KEY = 'userSelectedEnvPath';

@injectable()
export class RecommendedEnvironmentService implements IRecommendedEnvironmentService {
private api?: PythonExtension['environments'];
constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {}

registerEnvApi(api: PythonExtension['environments']) {
this.api = api;
}

trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined) {
if (workspace.workspaceFolders?.length) {
try {
void updateWorkspaceStateValue(MEMENTO_KEY, getDataToStore(environmentPath, uri));
} catch (ex) {
traceError('Failed to update workspace state for preferred environment', ex);
}
} else {
void this.extensionContext.globalState.update(MEMENTO_KEY, environmentPath);
}
}

getRecommededEnvironment(
resource: Resource,
):
| { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' }
| undefined {
let workspaceState: string | undefined = undefined;
try {
workspaceState = getWorkspaceStateValue<string>(MEMENTO_KEY);
} catch (ex) {
traceError('Failed to get workspace state for preferred environment', ex);
}

if (workspace.workspaceFolders?.length && workspaceState) {
const workspaceUri = (
(resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) ||
workspace.workspaceFolders[0].uri
).toString();

try {
const existingJson: Record<string, string> = JSON.parse(workspaceState);
const selectedEnvPath = existingJson[workspaceUri];
if (selectedEnvPath) {
return { environmentPath: selectedEnvPath, reason: 'workspaceUserSelected' };
}
} catch (ex) {
traceError('Failed to parse existing workspace state value for preferred environment', ex);
}
}

const globalSelectedEnvPath = this.extensionContext.globalState.get<string | undefined>(MEMENTO_KEY);
if (globalSelectedEnvPath) {
return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' };
}
return this.api && workspace.isTrusted
? {
environmentPath: this.api.getActiveEnvironmentPath(resource).path,
reason: 'defaultRecommended',
}
: undefined;
}
}

function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined {
if (!workspace.workspaceFolders?.length) {
return environmentPath;
}
const workspaceUri = (
(uri ? workspace.getWorkspaceFolder(uri)?.uri : undefined) || workspace.workspaceFolders[0].uri
).toString();
const existingData = getWorkspaceStateValue<string>(MEMENTO_KEY);
if (!existingData) {
return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {});
}
try {
const existingJson: Record<string, string> = JSON.parse(existingData);
if (environmentPath) {
existingJson[workspaceUri] = environmentPath;
} else {
delete existingJson[workspaceUri];
}
return JSON.stringify(existingJson);
} catch (ex) {
traceError('Failed to parse existing workspace state value for preferred environment', ex);
return JSON.stringify({
[workspaceUri]: environmentPath,
});
}
}
12 changes: 12 additions & 0 deletions src/client/interpreter/configuration/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode';
import { Resource } from '../../common/types';
import { PythonEnvironment } from '../../pythonEnvironments/info';
import { PythonExtension } from '../../api/types';

export interface IPythonPathUpdaterService {
updatePythonPath(pythonPath: string | undefined): Promise<void>;
Expand Down Expand Up @@ -96,3 +97,14 @@ export interface IInterpreterQuickPick {
params?: InterpreterQuickPickParams,
): Promise<string | undefined>;
}

export const IRecommendedEnvironmentService = Symbol('IRecommendedEnvironmentService');
export interface IRecommendedEnvironmentService {
registerEnvApi(api: PythonExtension['environments']): void;
trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void;
getRecommededEnvironment(
resource: Resource,
):
| { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' }
| undefined;
}
6 changes: 6 additions & 0 deletions src/client/interpreter/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import { InstallPythonViaTerminal } from './configuration/interpreterSelector/co
import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter';
import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter';
import { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector';
import { RecommendedEnvironmentService } from './configuration/recommededEnvironmentService';
import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService';
import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory';
import {
IInterpreterComparer,
IInterpreterQuickPick,
IInterpreterSelector,
IRecommendedEnvironmentService,
IPythonPathUpdaterServiceFactory,
IPythonPathUpdaterServiceManager,
} from './configuration/types';
Expand Down Expand Up @@ -59,6 +61,10 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void
IExtensionSingleActivationService,
ResetInterpreterCommand,
);
serviceManager.addSingleton<IRecommendedEnvironmentService>(
IRecommendedEnvironmentService,
RecommendedEnvironmentService,
);
serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand);

serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, VirtualEnvironmentPrompt);
Expand Down
Loading