Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4e9792f
Split runtimePath (absolute) from runtimeDisplayPath (UI-only ~-short…
samclark2015 Jul 9, 2026
43d4296
Add tests for getRuntimeDisplayPath and metadata path-splitting logic
samclark2015 Jul 9, 2026
6d8f501
Fix stale ~-prefixed runtimePath in affiliation storage
samclark2015 Jul 10, 2026
af3d426
Move runtimeDisplayPath computation from extensions to LanguageRuntim…
samclark2015 Jul 10, 2026
0a5ecf5
Remove obsolete runtimeDisplayPath extension tests, add affiliation-h…
samclark2015 Jul 13, 2026
c681632
Fix runtime metadata test assertions and lint headers
samclark2015 Jul 13, 2026
c31d9a6
Remove leftover runtimeDisplayPath test missed by prior cleanup commit
samclark2015 Jul 13, 2026
328e747
Add LanguageRuntimeStartupBehavior and LanguageRuntimeSessionLocation…
samclark2015 Jul 13, 2026
703c018
Remove createPythonRuntimeMetadata path tests; clean up now-unused ps…
samclark2015 Jul 14, 2026
2021268
Make registerRuntime async; use remote userHome for runtimeDisplayPat…
samclark2015 Jul 14, 2026
98f7f4d
Revert registerRuntime to synchronous; cache userHome eagerly in cons…
samclark2015 Jul 14, 2026
d965d84
Don't fall back to local-only home on userHome cache miss; soften con…
samclark2015 Jul 16, 2026
432bbcc
Use getRuntimeDisplayPath in clearAffiliatedRuntime quick pick
samclark2015 Jul 16, 2026
6d6f266
Flush userHome microtask before registerRuntime in tildify test
samclark2015 Jul 16, 2026
e8fe5a2
Merge remote-tracking branch 'origin/main' into issue-12942
samclark2015 Jul 16, 2026
2d90403
Merge remote-tracking branch 'origin/main' into issue-12942
samclark2015 Jul 17, 2026
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
10 changes: 1 addition & 9 deletions extensions/positron-python/src/client/positron/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

// eslint-disable-next-line import/no-unresolved
import * as positron from 'positron';
import * as vscode from 'vscode';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as crypto from 'crypto';

Expand Down Expand Up @@ -250,13 +248,7 @@ export async function createPythonRuntimeMetadata(
digest.update(pythonVersion);
const runtimeId = digest.digest('hex').substring(0, 32);

// Create the runtime path.
// TODO@softwarenerd - We will need to update this for Windows.
const homedir = os.homedir();
const runtimePath =
os.platform() !== 'win32' && interpreter.path.startsWith(homedir)
? path.join('~', interpreter.path.substring(homedir.length))
: interpreter.path;
const runtimePath = interpreter.path;

// Save the ID of the Python environment for use when creating the language
// runtime session.
Expand Down
6 changes: 1 addition & 5 deletions extensions/positron-r/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,11 +642,7 @@ export async function makeMetadata(
const homedir = os.homedir();
const isUserInstallation = rInst.binpath.startsWith(homedir);

// Create the runtime path.
// TODO@softwarenerd - We will need to update this for Windows.
const runtimePath = os.platform() !== 'win32' && isUserInstallation ?
path.join('~', rInst.binpath.substring(homedir.length)) :
rInst.binpath;
const runtimePath = rInst.binpath;

// Create the Rscript path.
// The Rscript path is the same as the R binary path, but with the 'R' or 'R.exe' executable
Expand Down
8 changes: 7 additions & 1 deletion extensions/positron-r/src/runtime-quickpick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as os from 'os';
import * as path from 'path';
import * as positron from 'positron';
import * as vscode from 'vscode';
import { rRuntimeDiscoverer, RRuntimeSource } from './provider';
Expand All @@ -18,7 +20,11 @@ class RuntimeQuickPickItem implements vscode.QuickPickItem {
public runtimeMetadata: positron.LanguageRuntimeMetadata,
) {
this.label = runtimeMetadata.runtimeName;
this.description = runtimeMetadata.runtimePath;
const homedir = os.homedir();
this.description =
os.platform() !== 'win32' && runtimeMetadata.runtimePath.startsWith(homedir)
? path.join('~', runtimeMetadata.runtimePath.substring(homedir.length))
: runtimeMetadata.runtimePath;
this.runtime = runtimeMetadata;
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,11 @@ declare module 'positron' {
* before the runtime is started.
*/
export interface LanguageRuntimeMetadata {
/** The path to the runtime. */
/**
* The absolute path to the runtime executable. Always a fully-expanded
* path suitable for use with execFile or equivalent; never contains ~
* or other shorthand.
*/
runtimePath: string;

/** A unique identifier for this runtime; takes the form of a GUID */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { DropDownListBoxEntry } from '../../positronComponents/dropDownListBox/dropDownListBox.js';
import { DropDownListBoxItem } from '../../positronComponents/dropDownListBox/dropDownListBoxItem.js';
import { DropDownListBoxSeparator } from '../../positronComponents/dropDownListBox/dropDownListBoxSeparator.js';
import { ILanguageRuntimeMetadata } from '../../../services/languageRuntime/common/languageRuntimeService.js';
import { getRuntimeDisplayPath, ILanguageRuntimeMetadata } from '../../../services/languageRuntime/common/languageRuntimeService.js';

/**
* InterpreterInfo interface.
Expand Down Expand Up @@ -52,7 +52,7 @@ export const interpretersToDropdownItems = (
runtimeId: runtime.runtimeId,
languageName: runtime.languageName,
languageVersion: runtime.languageVersion,
runtimePath: runtime.runtimePath,
runtimePath: getRuntimeDisplayPath(runtime),
runtimeSource: runtime.runtimeSource,
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { URI } from '../../../../../base/common/uri.js';
import { localize } from '../../../../../nls.js';
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js';
import { getRuntimeDisplayPath, RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js';
import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js';
import { IChatWidget } from '../chat.js';

Expand Down Expand Up @@ -77,7 +77,7 @@ export async function showRuntimeSessionsPick(accessor: ServicesAccessor, _widge
return {
id: session.sessionId,
label,
detail: session.runtimeMetadata.runtimePath,
detail: getRuntimeDisplayPath(session.runtimeMetadata),
iconPath: {
dark: URI.parse(`data:image/svg+xml;base64, ${session.runtimeMetadata.base64EncodedIconSvg}`),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../p
import { IKeybindingRule, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { LANGUAGE_RUNTIME_ACTION_CATEGORY } from '../common/languageRuntime.js';
import { IPositronConsoleService, POSITRON_CONSOLE_VIEW_ID } from '../../../services/positronConsole/browser/interfaces/positronConsoleService.js';
import { ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimePickerContribution, IRuntimePickerItem, LanguageRuntimeSessionMode, RuntimeCodeExecutionMode, RuntimeErrorBehavior, RuntimeStartupPhase, RuntimeState } from '../../../services/languageRuntime/common/languageRuntimeService.js';
import { getRuntimeDisplayPath, ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimePickerContribution, IRuntimePickerItem, LanguageRuntimeSessionMode, RuntimeCodeExecutionMode, RuntimeErrorBehavior, RuntimeStartupPhase, RuntimeState } from '../../../services/languageRuntime/common/languageRuntimeService.js';
import { ILanguageRuntimeSession, IRuntimeClientInstance, IRuntimeSessionService, RuntimeClientType, RuntimeStartMode } from '../../../services/runtimeSession/common/runtimeSessionService.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
Expand Down Expand Up @@ -213,7 +213,7 @@ export const selectLanguageRuntimeSession = async (
.map(session => ({
id: session.sessionId,
label: session.dynState.sessionName,
detail: session.runtimeMetadata.runtimePath,
detail: getRuntimeDisplayPath(session.runtimeMetadata),
description: session.sessionId === currentForegroundSession?.sessionId
? localize('positron.languageRuntime.currentlySelected', 'Currently Selected')
: undefined,
Expand Down Expand Up @@ -242,7 +242,7 @@ export const selectLanguageRuntimeSession = async (
.map(session => ({
id: session.sessionId,
label: getSessionDisplayNameWithRuntime(session),
detail: session.runtimeMetadata.runtimePath,
detail: getRuntimeDisplayPath(session.runtimeMetadata),
description: session.sessionId === currentForegroundSession?.sessionId
? localize('positron.languageRuntime.currentlySelected', 'Currently Selected')
: undefined,
Expand All @@ -264,7 +264,7 @@ export const selectLanguageRuntimeSession = async (
.map(session => ({
id: session.sessionId,
label: getSessionDisplayNameWithRuntime(session),
detail: session.runtimeMetadata.runtimePath,
detail: getRuntimeDisplayPath(session.runtimeMetadata),
description: session.sessionId === currentForegroundSession?.sessionId
? localize('positron.languageRuntime.currentlySelected', 'Currently Selected')
: undefined,
Expand Down Expand Up @@ -474,7 +474,7 @@ export const selectNewLanguageRuntime = async (
items.push({
id: runtime.runtimeId,
label: runtime.runtimeName,
detail: runtime.runtimePath,
detail: getRuntimeDisplayPath(runtime),
iconPath: {
dark: URI.parse(`data:image/svg+xml;base64, ${runtime.base64EncodedIconSvg}`),
},
Expand Down Expand Up @@ -540,7 +540,7 @@ export const selectNewLanguageRuntime = async (
items.push({
id: runtime.runtimeId,
label: runtime.runtimeName,
detail: runtime.runtimePath,
detail: getRuntimeDisplayPath(runtime),
iconPath: {
dark: URI.parse(`data:image/svg+xml;base64, ${runtime.base64EncodedIconSvg}`),
},
Expand Down Expand Up @@ -899,7 +899,7 @@ export function registerLanguageRuntimeActions() {
const runtimeQuickPickItems = runtimes.map<LanguageRuntimeQuickPickItem>(runtime => ({
id: runtime.runtimeId,
label: `${runtime.languageName}: ${runtime.runtimeName}`,
description: runtime.runtimePath,
description: getRuntimeDisplayPath(runtime),
runtime
} satisfies LanguageRuntimeQuickPickItem));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,15 @@ describe('selectNewLanguageRuntime', () => {
await vi.waitFor(() => expect(pick.show).toHaveBeenCalled());
}

function registerRuntime(metadata: ILanguageRuntimeMetadata): ILanguageRuntimeMetadata {
ctx.disposables.add(ctx.get(ILanguageRuntimeService).registerRuntime(metadata));
async function registerRuntime(metadata: ILanguageRuntimeMetadata): Promise<ILanguageRuntimeMetadata> {
const runtimeService = ctx.get(ILanguageRuntimeService);
ctx.disposables.add(runtimeService.registerRuntime(metadata));
if (!preferredByLanguage.has(metadata.languageId)) {
preferredByLanguage.set(metadata.languageId, metadata);
}
return metadata;
// registerRuntime enriches the metadata into a new object (e.g. adds
// runtimeDisplayPath); return the stored instance the picker resolves to.
return runtimeService.getRegisteredRuntime(metadata.runtimeId) ?? metadata;
}

function pickItemById(id: string): IQuickPickItem | undefined {
Expand Down Expand Up @@ -124,7 +127,7 @@ describe('selectNewLanguageRuntime', () => {
});

it('resolves to the selected runtime metadata', async () => {
const py = registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const py = await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const promise = runPicker();
await waitUntilOpened();
const item = pickItemById('py-1')!;
Expand All @@ -150,8 +153,8 @@ describe('selectNewLanguageRuntime', () => {

describe('options.languageId', () => {
it('filters runtimes to the given languageId', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1', languageId: 'python', languageName: 'Python' }));
registerRuntime(makeRuntime({ runtimeId: 'r-1', languageId: 'r', languageName: 'R', runtimeName: 'R 4.4' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1', languageId: 'python', languageName: 'Python' }));
await registerRuntime(makeRuntime({ runtimeId: 'r-1', languageId: 'r', languageName: 'R', runtimeName: 'R 4.4' }));

const promise = runPicker({ languageId: 'python' });
await waitUntilOpened();
Expand All @@ -165,7 +168,7 @@ describe('selectNewLanguageRuntime', () => {
});

it('passes languageId through to getPickerContributions', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const spy = vi.spyOn(ctx.get(ILanguageRuntimeService), 'getPickerContributions');
const promise = runPicker({ languageId: 'python' });
await waitUntilOpened();
Expand All @@ -177,8 +180,8 @@ describe('selectNewLanguageRuntime', () => {

describe('options.currentRuntimeId', () => {
it('pre-focuses the matching item via activeItems', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0', runtimeName: 'Python 3.10' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0', runtimeName: 'Python 3.10' }));

const promise = runPicker({ currentRuntimeId: 'py-2' });
await waitUntilOpened();
Expand All @@ -189,7 +192,7 @@ describe('selectNewLanguageRuntime', () => {
});

it('leaves activeItems untouched when no item matches the id', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const promise = runPicker({ currentRuntimeId: 'unknown-id' });
await waitUntilOpened();
expect(pick.activeItems).toEqual([]);
Expand All @@ -200,8 +203,8 @@ describe('selectNewLanguageRuntime', () => {

describe('item structure', () => {
it('groups Suggested + per-environment-type runtimes with separators', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-system', runtimeSource: 'System', runtimeName: 'Python (System)' }));
registerRuntime(makeRuntime({ runtimeId: 'py-conda', runtimeSource: 'Conda', runtimeName: 'Python (Conda)' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-system', runtimeSource: 'System', runtimeName: 'Python (System)' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-conda', runtimeSource: 'Conda', runtimeName: 'Python (Conda)' }));

const promise = runPicker();
await waitUntilOpened();
Expand All @@ -223,9 +226,9 @@ describe('selectNewLanguageRuntime', () => {
});

it('sorts within an env type by version descending, unsupported runtimes last', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-310', languageVersion: '3.10.0', runtimeName: 'Python 3.10' }));
registerRuntime(makeRuntime({ runtimeId: 'py-312', languageVersion: '3.12.0', runtimeName: 'Python 3.12' }));
registerRuntime(makeRuntime({
await registerRuntime(makeRuntime({ runtimeId: 'py-310', languageVersion: '3.10.0', runtimeName: 'Python 3.10' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-312', languageVersion: '3.12.0', runtimeName: 'Python 3.12' }));
await registerRuntime(makeRuntime({
runtimeId: 'py-old', languageVersion: '3.8.0', runtimeName: 'Python 3.8 (unsupported)',
extraRuntimeData: { supported: false },
}));
Expand All @@ -246,21 +249,21 @@ describe('selectNewLanguageRuntime', () => {

describe('reactive rebuild', () => {
it('rebuilds when onDidRegisterRuntime fires mid-pick', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const promise = runPicker();
await waitUntilOpened();
expect(pickItemById('py-1')).toBeDefined();
expect(pickItemById('py-2')).toBeUndefined();

registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' }));
expect(pickItemById('py-2')).toBeDefined();
pick.cancel(QuickInputHideReason.Gesture);
await promise;
});

it('rebuilds when onDidUnregisterRuntime fires mid-pick', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' }));
const promise = runPicker();
await waitUntilOpened();
expect(pickItemById('py-1')).toBeDefined();
Expand All @@ -276,13 +279,13 @@ describe('selectNewLanguageRuntime', () => {
});

it('preserves the previously focused item across rebuilds', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-2', languageVersion: '3.10.0' }));
const promise = runPicker({ currentRuntimeId: 'py-2' });
await waitUntilOpened();
expect(pick.activeItems[0].id).toBe('py-2');

registerRuntime(makeRuntime({ runtimeId: 'py-3', languageVersion: '3.13.0' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-3', languageVersion: '3.13.0' }));
expect(pick.activeItems[0].id).toBe('py-2');
pick.cancel(QuickInputHideReason.Gesture);
await promise;
Expand All @@ -291,7 +294,7 @@ describe('selectNewLanguageRuntime', () => {

describe('startup phase', () => {
it('re-fetches contributions when phase transitions to Complete', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const runtimeService = ctx.get(ILanguageRuntimeService);
runtimeService.setStartupPhase(RuntimeStartupPhase.Discovering);

Expand Down Expand Up @@ -344,7 +347,7 @@ describe('selectNewLanguageRuntime', () => {
it('shows a busy spinner and discovering placeholder while phase is not Complete', async () => {
const runtimeService = ctx.get(ILanguageRuntimeService);
runtimeService.setStartupPhase(RuntimeStartupPhase.Discovering);
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));

const promise = runPicker();
await waitUntilOpened();
Expand Down Expand Up @@ -372,7 +375,7 @@ describe('selectNewLanguageRuntime', () => {
});

it('does not show a spinner when discovery is already complete on open', async () => {
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
const promise = runPicker();
await waitUntilOpened();
expect(pick.busy).toBe(false);
Expand All @@ -384,7 +387,7 @@ describe('selectNewLanguageRuntime', () => {

it('toggles the spinner back on when phase leaves Complete while the picker is open', async () => {
const runtimeService = ctx.get(ILanguageRuntimeService);
registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
await registerRuntime(makeRuntime({ runtimeId: 'py-1' }));
// beforeEach leaves the phase at Complete.
const promise = runPicker();
await waitUntilOpened();
Expand Down Expand Up @@ -487,7 +490,9 @@ describe('selectNewLanguageRuntime', () => {
const installItem = await waitForItemByLabel('Install Python via uv');
pick.accept(installItem);

await expect(promise).resolves.toEqual(installedRuntime);
// The picker resolves to the enriched, registered instance.
const result = await promise;
expect(result).toEqual(runtimeService.getRegisteredRuntime(installedRuntime.runtimeId));
expect(contribution.onSelect).toHaveBeenCalledWith('install-uv');
expect(rediscoverAllRuntimes).toHaveBeenCalledWith(/* quiet */ true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('PositronAssistantService', () => {
ctx.instantiationService.stub(IPositronPlotsService, ctx.disposables.add(createTestPlotsServiceWithPlots()));

// Create test runtime sessions
const runtime = createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables);
const runtime = await createTestLanguageRuntimeMetadata(ctx.instantiationService, ctx.disposables);
testConsoleSession = await startTestLanguageRuntimeSession(
ctx.instantiationService,
ctx.disposables,
Expand Down
Loading
Loading