/** * PyCharm's "Add Python Interpreter" (local): generate a new environment with a * tool (virtualenv / uv / Poetry) or select an existing interpreter. */ import { isCancelled } from "@/platform/ipc"; import { useId, useMemo, useState } from "react"; import { Dialog, IconButton, Input, Select, Spinner, type SelectOption, T } from "@/ui"; import { choosePath, createVenv, pythonInterpreter, rememberSdk, type Sdk, type VenvTool } from "./api"; import { tildify, useDetected } from "./components"; import styles from "./Sdk.module.css"; import { tx } from "@/platform/i18n"; type Mode = "new" | "existing"; /** Environment types of the left column, in PyCharm's order. */ const KINDS: { tool: VenvTool; label: string; missing: string }[] = [ { tool: "venv", label: "Virtualenv Environment", missing: "" }, { tool: "conda", label: "Conda Environment", missing: "conda is not installed" }, { tool: "uv", label: "uv Environment", missing: "uv is not installed" }, { tool: "poetry", label: "Poetry Environment", missing: "Poetry is not installed" }, ]; export function AddInterpreterDialog({ root, onClose, onAdded, }: { root: string; onClose: () => void; onAdded: (python: string) => void; }) { const { list, loading } = useDetected("python"); const [kind, setKind] = useState("venv"); const [mode, setMode] = useState("new"); const bases = useMemo(() => list.filter((s) => s.source !== "venv"), [list]); const [base, setBase] = useState(null); const [location, setLocation] = useState(`${root}/.venv`); const [existing, setExisting] = useState(null); const [browsed, setBrowsed] = useState([]); const [busy, setBusy] = useState<{ cancel: () => void } | null>(null); const [log, setLog] = useState([]); const [error, setError] = useState(null); const ids = { base: useId(), location: useId(), existing: useId() }; const baseValue = base ?? bases[0]?.path ?? null; const toOption = (s: { path: string; name: string }): SelectOption => ({ value: s.path, label: s.name, detail: tildify(s.path), }); const baseOptions = bases.map(toOption); const existingOptions = [...browsed, ...list.filter((s) => !browsed.some((b) => b.path === s.path))].map((s) => ({ value: s.path, label: `${s.name} — ${tildify(s.path)}`, })); const checkExisting = (path: string, onResolved: (sdk: Sdk) => void) => { setError(null); let cancelled = false; setBusy({ cancel: () => { cancelled = true; setBusy(null); }, }); void pythonInterpreter(path) .then((sdk) => { if (cancelled) return; if (!sdk) { setError(tx("Cannot run this Python interpreter. Select a working Python executable or virtualenv folder.")); return; } onResolved(sdk); }) .catch((e: unknown) => { if (!cancelled) setError(String((e as Error)?.message ?? e)); }) .finally(() => { if (!cancelled) setBusy(null); }); }; const ok = () => { if (busy) return; setError(null); if (mode === "existing") { if (existing) checkExisting(existing, (sdk) => { rememberSdk("python", sdk); onAdded(sdk.path); }); return; } if (!baseValue || !location.trim()) return; setLog([]); const job = createVenv(kind, baseValue, location.trim(), (line) => setLog((l) => [...l.slice(-200), line])); setBusy({ cancel: job.cancel }); job.promise .then((sdk) => { rememberSdk("python", sdk); onAdded(sdk.path); }) .catch((e: unknown) => setError(isCancelled(e) ? null : String((e as Error)?.message ?? e))) .finally(() => setBusy(null)); }; const canOk = !busy && (mode === "new" ? !!baseValue && !!location.trim() : !!existing?.trim()); const note = { venv: tx("Runs “ -m venv ”; the new interpreter becomes the project interpreter."), conda: tx("Runs “conda create -p ”; the new interpreter becomes the project interpreter."), uv: tx("Runs “uv venv --python ”; the new interpreter becomes the project interpreter."), poetry: tx( "Runs “poetry env use ” in the project; poetry keeps the environment under its own cache directory.", ), }[kind]; return ( (busy ? busy.cancel() : onClose())} title="Add Python Interpreter" width={680} aria-label="Add Python Interpreter" footerStart={ busy && ( ) } buttons={[ { label: "OK", role: "default", disabled: !canOk, onClick: ok }, { label: "Cancel", role: "cancel" }, ]} >
    {KINDS.map((k) => (
  • ))}
{mode === "new" ? ( <>
setLocation(e.target.value)} disabled={kind === "poetry"} /> { const p = await choosePath({ directory: true, title: "Virtualenv Location", defaultPath: root }); if (p) setLocation(p); }} />
{ const p = await choosePath({ directory: true, title: "Select Python Environment Folder", defaultPath: root, }); if (p) checkExisting(p, (sdk) => { setBrowsed((current) => [sdk, ...current.filter((s) => s.path !== sdk.path)]); setExisting(sdk.path); }); }} />
)} {error && (
{error}
)} {log.length > 0 &&
{log.join("\n")}
}
); }