11import {
22 LogOutputChannel ,
33 ProgressLocation ,
4+ QuickPickItem ,
45 ShellExecution ,
56 Task ,
67 TaskPanelKind ,
@@ -16,11 +17,30 @@ import { EventNames } from '../../common/telemetry/constants';
1617import { sendTelemetryEvent } from '../../common/telemetry/sender' ;
1718import { createDeferred } from '../../common/utils/deferred' ;
1819import { isWindows } from '../../common/utils/platformUtils' ;
19- import { showErrorMessage , showInformationMessage , withProgress } from '../../common/window.apis' ;
20+ import { showErrorMessage , showInformationMessage , showQuickPick , withProgress } from '../../common/window.apis' ;
2021import { isUvInstalled , resetUvInstallationCache } from './helpers' ;
2122
2223const UV_INSTALL_PYTHON_DONT_ASK_KEY = 'python-envs:uv:UV_INSTALL_PYTHON_DONT_ASK' ;
2324
25+ /**
26+ * Represents a Python version from uv python list
27+ */
28+ export interface UvPythonVersion {
29+ key : string ;
30+ version : string ;
31+ version_parts : {
32+ major : number ;
33+ minor : number ;
34+ patch : number ;
35+ } ;
36+ path : string | null ;
37+ url : string | null ;
38+ os : string ;
39+ variant : string ;
40+ implementation : string ;
41+ arch : string ;
42+ }
43+
2444/**
2545 * Checks if a command is available on the system.
2646 */
@@ -142,12 +162,17 @@ export async function installUv(_log?: LogOutputChannel): Promise<boolean> {
142162
143163/**
144164 * Gets the path to the uv-managed Python installation.
165+ * @param version Optional Python version to find (e.g., "3.12")
145166 * @returns Promise that resolves to the Python path, or undefined if not found
146167 */
147- export async function getUvPythonPath ( ) : Promise < string | undefined > {
168+ export async function getUvPythonPath ( version ?: string ) : Promise < string | undefined > {
148169 return new Promise ( ( resolve ) => {
149170 const chunks : string [ ] = [ ] ;
150- const proc = spawnProcess ( 'uv' , [ 'python' , 'find' ] ) ;
171+ const args = [ 'python' , 'find' ] ;
172+ if ( version ) {
173+ args . push ( version ) ;
174+ }
175+ const proc = spawnProcess ( 'uv' , args ) ;
151176 proc . stdout ?. on ( 'data' , ( data ) => chunks . push ( data . toString ( ) ) ) ;
152177 proc . on ( 'error' , ( ) => resolve ( undefined ) ) ;
153178 proc . on ( 'exit' , ( code ) => {
@@ -161,6 +186,96 @@ export async function getUvPythonPath(): Promise<string | undefined> {
161186 } ) ;
162187}
163188
189+ /**
190+ * Gets available Python versions from uv.
191+ * @returns Promise that resolves to an array of Python versions
192+ */
193+ export async function getAvailablePythonVersions ( ) : Promise < UvPythonVersion [ ] > {
194+ return new Promise ( ( resolve ) => {
195+ const chunks : string [ ] = [ ] ;
196+ const proc = spawnProcess ( 'uv' , [ 'python' , 'list' , '--output-format' , 'json' ] ) ;
197+ proc . stdout ?. on ( 'data' , ( data ) => chunks . push ( data . toString ( ) ) ) ;
198+ proc . on ( 'error' , ( ) => resolve ( [ ] ) ) ;
199+ proc . on ( 'exit' , ( code ) => {
200+ if ( code === 0 && chunks . length > 0 ) {
201+ try {
202+ const versions = JSON . parse ( chunks . join ( '' ) ) as UvPythonVersion [ ] ;
203+ resolve ( versions ) ;
204+ } catch {
205+ traceError ( 'Failed to parse uv python list output' ) ;
206+ resolve ( [ ] ) ;
207+ }
208+ } else {
209+ resolve ( [ ] ) ;
210+ }
211+ } ) ;
212+ } ) ;
213+ }
214+
215+ interface PythonVersionQuickPickItem extends QuickPickItem {
216+ version : string ;
217+ isInstalled : boolean ;
218+ }
219+
220+ /**
221+ * Shows a QuickPick to select a Python version to install.
222+ * @returns Promise that resolves to the selected version string, or undefined if cancelled
223+ */
224+ export async function selectPythonVersionToInstall ( ) : Promise < string | undefined > {
225+ const versions = await withProgress (
226+ {
227+ location : ProgressLocation . Notification ,
228+ title : UvInstallStrings . fetchingVersions ,
229+ } ,
230+ async ( ) => getAvailablePythonVersions ( ) ,
231+ ) ;
232+
233+ if ( versions . length === 0 ) {
234+ showErrorMessage ( UvInstallStrings . failedToFetchVersions ) ;
235+ return undefined ;
236+ }
237+
238+ // Filter to only default variant (not freethreaded) and group by minor version
239+ const seenMinorVersions = new Set < string > ( ) ;
240+ const items : PythonVersionQuickPickItem [ ] = [ ] ;
241+
242+ for ( const v of versions ) {
243+ // Only include default variant CPython
244+ if ( v . variant !== 'default' || v . implementation !== 'cpython' ) {
245+ continue ;
246+ }
247+
248+ // Create a minor version key (e.g., "3.13")
249+ const minorKey = `${ v . version_parts . major } .${ v . version_parts . minor } ` ;
250+
251+ // Only show the latest patch for each minor version (they come sorted from uv)
252+ if ( seenMinorVersions . has ( minorKey ) ) {
253+ continue ;
254+ }
255+ seenMinorVersions . add ( minorKey ) ;
256+
257+ const isInstalled = v . path !== null ;
258+ items . push ( {
259+ label : `Python ${ v . version } ` ,
260+ description : isInstalled ? `$(check) ${ UvInstallStrings . installed } ` : undefined ,
261+ detail : isInstalled ? v . path ?? undefined : undefined ,
262+ version : v . version ,
263+ isInstalled,
264+ } ) ;
265+ }
266+
267+ const selected = await showQuickPick ( items , {
268+ placeHolder : UvInstallStrings . selectPythonVersion ,
269+ ignoreFocusOut : true ,
270+ } ) ;
271+
272+ if ( ! selected ) {
273+ return undefined ;
274+ }
275+
276+ return selected . version ;
277+ }
278+
164279/**
165280 * Installs Python using uv.
166281 * @param log Optional log output channel
@@ -238,9 +353,10 @@ export async function promptInstallPythonViaUv(
238353 * This is the main entry point for programmatic Python installation.
239354 *
240355 * @param log Optional log output channel
356+ * @param version Optional Python version to install (e.g., "3.12")
241357 * @returns Promise that resolves to the installed Python path, or undefined on failure
242358 */
243- export async function installPythonWithUv ( log ?: LogOutputChannel ) : Promise < string | undefined > {
359+ export async function installPythonWithUv ( log ?: LogOutputChannel , version ?: string ) : Promise < string | undefined > {
244360 const uvInstalled = await isUvInstalled ( log ) ;
245361
246362 sendTelemetryEvent ( EventNames . UV_PYTHON_INSTALL_STARTED , undefined , { uvAlreadyInstalled : uvInstalled } ) ;
@@ -265,15 +381,15 @@ export async function installPythonWithUv(log?: LogOutputChannel): Promise<strin
265381 }
266382
267383 // Step 2: Install Python via uv
268- const pythonSuccess = await installPythonViaUv ( log ) ;
384+ const pythonSuccess = await installPythonViaUv ( log , version ) ;
269385 if ( ! pythonSuccess ) {
270386 sendTelemetryEvent ( EventNames . UV_PYTHON_INSTALL_FAILED , undefined , { stage : 'pythonInstall' } ) ;
271387 showErrorMessage ( UvInstallStrings . installFailed ) ;
272388 return undefined ;
273389 }
274390
275391 // Step 3: Get the installed Python path using uv python find
276- const pythonPath = await getUvPythonPath ( ) ;
392+ const pythonPath = await getUvPythonPath ( version ) ;
277393 if ( ! pythonPath ) {
278394 traceError ( 'Python installed but could not find the path via uv python find' ) ;
279395 sendTelemetryEvent ( EventNames . UV_PYTHON_INSTALL_FAILED , undefined , { stage : 'findPath' } ) ;
0 commit comments