From 1111c231a7311ca3bfa3af72ce8834feeef22814 Mon Sep 17 00:00:00 2001 From: Jon Vanausdeln Date: Tue, 14 Jul 2026 16:15:55 -0700 Subject: [PATCH 1/2] Show runtime picker immediately, fetch contributed items after selectNewLanguageRuntime awaited fetchContributedItems() before quickPick.show(). When startupPhase is Complete, that fetch calls each picker contribution's getItems() -- for positron-python this is an extension-host RPC that enumerates interpreters. Right after a window reload the ext host is still activating, so the RPC can hang for seconds and show() is never reached: the session picker button appears to do nothing. This is the Windows-only e2e flake "Open Session QuickPick Menu ... toBeVisible() failed" (catalog-explorer and any test using the python/r fixtures, whose reuse scan opens this picker right after app.restart()). Trace analysis confirmed the click fires the command every time but the .quick-input-widget never enters the DOM. Show the runtimes immediately, then fetch contributed items and rebuild when they arrive (guarded against a disposed picker). Also fixes the real-user papercut where clicking "Start New Console Session" right after opening a workspace does nothing until interpreter discovery finishes. Contributed items now populate after show(), so the vitest suite polls for them; adds a regression test that a slow getItems() no longer blocks the picker from opening. Co-Authored-By: Claude Opus 4.8 --- .../browser/languageRuntimeActions.ts | 21 ++++- .../browser/languageRuntimeActions.vitest.ts | 80 ++++++++++++++----- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts index f1e71c16bb40..6a38204cf8bf 100644 --- a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts +++ b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts @@ -573,8 +573,6 @@ export const selectNewLanguageRuntime = async ( return items; }; - await fetchContributedItems(); - const disposables = new DisposableStore(); const quickPick = disposables.add(quickInputService.createQuickPick({ useSeparators: true })); quickPick.title = options?.title || localize('positron.languageRuntime.startSession', 'Start New Interpreter Session'); @@ -706,6 +704,25 @@ export const selectNewLanguageRuntime = async ( })); quickPick.show(); + + // Populate contributed items (e.g. positron-python's "Install Python...") + // WITHOUT blocking the initial show(). getItems() is an extension-host RPC + // that enumerates interpreters, and right after a window reload it can take + // seconds to resolve while the extension host is still activating. Awaiting + // it before show() (as this previously did) left the picker invisible the + // whole time -- clicking the session button appeared to do nothing at all. + // Show the runtimes immediately, then fold contributed items in when they + // arrive. When startup isn't Complete yet this is skipped and the + // onDidChangeRuntimeStartupPhase handler above performs the fetch instead. + if (languageRuntimeService.startupPhase === RuntimeStartupPhase.Complete) { + fetchContributedItems().then(() => { + // The user may have dismissed the picker before the fetch resolved; + // disposing tears down the quick pick, so don't rebuild a dead one. + if (!disposables.isDisposed) { + rebuildItems(); + } + }); + } }); }; diff --git a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts index bdd2b9b98de3..509721b488ef 100644 --- a/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts +++ b/src/vs/workbench/contrib/languageRuntime/test/browser/languageRuntimeActions.vitest.ts @@ -7,7 +7,7 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { IQuickInputService, IQuickPickItem, QuickInputHideReason, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimePickerContribution, LanguageRuntimeSessionLocation, LanguageRuntimeSessionMode, LanguageRuntimeStartupBehavior, RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; +import { ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimePickerContribution, IRuntimePickerItem, LanguageRuntimeSessionLocation, LanguageRuntimeSessionMode, LanguageRuntimeStartupBehavior, RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { IRuntimeStartupService } from '../../../../services/runtimeStartup/common/runtimeStartupService.js'; import { stubInterface } from '../../../../../test/vitest/stubInterface.js'; import { TestQuickPick } from '../../../../../test/vitest/testQuickPick.js'; @@ -78,9 +78,11 @@ describe('selectNewLanguageRuntime', () => { return ctx.instantiationService.invokeFunction(accessor => selectNewLanguageRuntime(accessor, options)); } - // The helper does `await fetchContributedItems()` before setting up the - // picker, so the items array isn't populated until pick.show() is called. - // Poll until that happens before reading items / firing events from tests. + // The helper builds the runtime rows synchronously and calls pick.show() + // immediately; contributed items (from picker contributions) are fetched + // afterwards and folded in via a rebuild, so they may not be present the + // instant show() is called. Poll for show() before reading runtime rows; + // poll again (vi.waitFor) when asserting on contributed items. async function waitUntilOpened(): Promise { await vi.waitFor(() => expect(pick.show).toHaveBeenCalled()); } @@ -99,6 +101,19 @@ describe('selectNewLanguageRuntime', () => { ); } + function pickItemByLabel(label: string): IQuickPickItem | undefined { + return pick.items.find( + (item): item is IQuickPickItem => item.type !== 'separator' && item.label === label, + ); + } + + // Contributed items are fetched after show() and folded in via a rebuild, + // so tests must poll for them rather than reading synchronously. + async function waitForItemByLabel(label: string): Promise { + await vi.waitFor(() => expect(pickItemByLabel(label)).toBeDefined()); + return pickItemByLabel(label)!; + } + describe('resolution', () => { it('resolves undefined when the picker is hidden without acceptance', async () => { @@ -402,12 +417,9 @@ describe('selectNewLanguageRuntime', () => { const promise = runPicker(); await waitUntilOpened(); - // A contributed item counts as a selectable row, so the empty-state - // placeholder must NOT appear even though there are no runtimes. - const contributedItem = pick.items.find( - (item): item is IQuickPickItem => item.type !== 'separator' && item.label === 'Install Python via uv', - ); - expect(contributedItem).toBeDefined(); + // A contributed item counts as a selectable row, so once it arrives the + // empty-state placeholder must NOT appear even though there are no runtimes. + await waitForItemByLabel('Install Python via uv'); expect(pick.busy).toBe(false); expect(pick.placeholder).toBeUndefined(); @@ -417,6 +429,42 @@ describe('selectNewLanguageRuntime', () => { }); describe('contributed items', () => { + it('opens the picker immediately without waiting for slow contributed items', async () => { + // Regression: getItems() is an extension-host RPC that enumerates + // interpreters and can hang for seconds right after a window reload + // while the extension host is still activating. The picker previously + // awaited it before show(), so a slow RPC left the picker invisible -- + // clicking the session button appeared to do nothing. show() must now + // happen up front, with contributed items folded in once they resolve. + const runtimeService = ctx.get(ILanguageRuntimeService); + registerRuntime(makeRuntime({ runtimeId: 'py-1' })); + + let resolveItems!: (items: IRuntimePickerItem[]) => void; + const contribution: IRuntimePickerContribution = { + handle: 9, + languageId: 'python', + getItems: vi.fn(() => new Promise(resolve => { resolveItems = resolve; })), + onSelect: vi.fn(), + }; + ctx.disposables.add(runtimeService.registerPickerContribution(contribution)); + + const promise = runPicker(); + + // The picker shows even though getItems() has not resolved: runtimes + // are visible immediately, the pending contributed item is not. + await waitUntilOpened(); + expect(contribution.getItems).toHaveBeenCalled(); + expect(pickItemById('py-1')).toBeDefined(); + expect(pickItemByLabel('Install Python via uv')).toBeUndefined(); + + // Once the slow RPC resolves, the contributed item folds in. + resolveItems([{ id: 'install-uv', label: 'Install Python via uv' }]); + await waitForItemByLabel('Install Python via uv'); + + pick.cancel(QuickInputHideReason.Gesture); + await promise; + }); + it('resolves the registered runtime and triggers a quiet rediscovery on selection', async () => { const installedRuntime = makeRuntime({ runtimeId: 'py-installed-by-uv' }); const runtimeService = ctx.get(ILanguageRuntimeService); @@ -436,8 +484,7 @@ describe('selectNewLanguageRuntime', () => { const promise = runPicker(); await waitUntilOpened(); - const installItem = pick.items - .find((item): item is IQuickPickItem => item.type !== 'separator' && item.label === 'Install Python via uv')!; + const installItem = await waitForItemByLabel('Install Python via uv'); pick.accept(installItem); await expect(promise).resolves.toEqual(installedRuntime); @@ -458,8 +505,7 @@ describe('selectNewLanguageRuntime', () => { const promise = runPicker(); await waitUntilOpened(); - const item = pick.items - .find((it): it is IQuickPickItem => it.type !== 'separator' && it.label === 'No-op installer')!; + const item = await waitForItemByLabel('No-op installer'); pick.accept(item); await expect(promise).resolves.toBeUndefined(); }); @@ -478,8 +524,7 @@ describe('selectNewLanguageRuntime', () => { const promise = runPicker(); await waitUntilOpened(); - const item = pick.items - .find((it): it is IQuickPickItem => it.type !== 'separator' && it.label === 'Failing installer')!; + const item = await waitForItemByLabel('Failing installer'); pick.accept(item); await expect(promise).resolves.toBeUndefined(); expect(consoleErrorSpy).toHaveBeenCalled(); @@ -507,8 +552,7 @@ describe('selectNewLanguageRuntime', () => { const promise = runPicker(); await waitUntilOpened(); - const labels = pick.items.map(i => i.label); - expect(labels).toContain('Working option'); + await waitForItemByLabel('Working option'); expect(consoleErrorSpy).toHaveBeenCalled(); pick.cancel(QuickInputHideReason.Gesture); From 3857bad87a9574f8223a386e3abc951c86f3ec62 Mon Sep 17 00:00:00 2001 From: Jon Vanausdeln Date: Tue, 14 Jul 2026 16:21:16 -0700 Subject: [PATCH 2/2] Trim the contributed-items comment Condense the explanatory comment on the deferred contributed-items fetch; no behavior change. Co-Authored-By: Claude Opus 4.8 --- .../browser/languageRuntimeActions.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts index 6a38204cf8bf..132953ca0090 100644 --- a/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts +++ b/src/vs/workbench/contrib/languageRuntime/browser/languageRuntimeActions.ts @@ -705,19 +705,14 @@ export const selectNewLanguageRuntime = async ( quickPick.show(); - // Populate contributed items (e.g. positron-python's "Install Python...") - // WITHOUT blocking the initial show(). getItems() is an extension-host RPC - // that enumerates interpreters, and right after a window reload it can take - // seconds to resolve while the extension host is still activating. Awaiting - // it before show() (as this previously did) left the picker invisible the - // whole time -- clicking the session button appeared to do nothing at all. - // Show the runtimes immediately, then fold contributed items in when they - // arrive. When startup isn't Complete yet this is skipped and the - // onDidChangeRuntimeStartupPhase handler above performs the fetch instead. + // Fold in contributed items after show() rather than awaiting them first: + // getItems() is an extension-host RPC that can hang for seconds right after + // a window reload, which would leave the picker invisible until it resolves. + // When startup isn't Complete yet, the onDidChangeRuntimeStartupPhase + // handler above does the fetch instead. if (languageRuntimeService.startupPhase === RuntimeStartupPhase.Complete) { fetchContributedItems().then(() => { - // The user may have dismissed the picker before the fetch resolved; - // disposing tears down the quick pick, so don't rebuild a dead one. + // Skip if the user dismissed the picker while the fetch was pending. if (!disposables.isDisposed) { rebuildItems(); }