Supplement Packages pane with metadata from Posit Package Manager (p3m)#13067
Supplement Packages pane with metadata from Posit Package Manager (p3m)#13067bricestacey merged 27 commits intomainfrom
Conversation
|
E2E Tests 🚀 |
|
Newbie question - How is the Refresh Packages button different than the Refresh Metadata button? |
|
Nonblocking comments
https://microsoft.github.io/vscode-codicons/dist/codicon.html |
Resolve merge conflicts in listPackages.tsx: - Incorporate new ActionBarFilter props (showClearAlways, clearButtonIcon, size) - Add handleFilterTextChanged to clear selection on filter change - Add empty state message when no packages match filter - Keep sort dropdown and action bar structure
The P3M API returns a 400 error when requesting download counts if usage data is disabled on the server. Set omit_downloads to true since download counts are not used in the UI. See #12928
Extend LanguageRuntimePackage interface with optional metadata fields (description, license, latestVersion, availableVersions, packageSize, publishedDate, downloads) populated from P3M API. Add PackageDetails component that displays metadata when a package is selected in the list. The details panel shows description, license, latest version with update indicator, size, published date, and download count. Wire up pip and uv package managers to fetch P3M metadata and merge it with installed package information. See #12928
Split package fetching into two stages for better UX: 1. getPackages() returns basic list immediately from kernel 2. getPackageMetadata() fetches P3M metadata asynchronously The package list now displays instantly while metadata loads in the background. When metadata arrives, the UI updates automatically. Added optional getPackageMetadata() method to IPackageManager and LanguageRuntimePackageManager interfaces. The instance orchestrates the two-stage fetch and fires onDidRefreshPackagesInstance twice. See #12928
Implement the extension host bridge layer for getPackageMetadata: - Add $getPackageMetadata to ExtHostLanguageRuntimeShape protocol - Implement in extHostLanguageRuntime.ts - Add adapter method in mainThreadLanguageRuntime.ts The method returns undefined when the extension's package manager doesn't implement getPackageMetadata, allowing graceful degradation. See #12928
Use CancellationToken.None for the background metadata fetch since it runs after the main refresh operation completes. The original token may be disposed at that point, causing spurious cancellation errors. See #12928
Maps don't serialize properly over the extension host IPC channel. Convert Map to plain object (Record) in extHostLanguageRuntime before sending, and convert back to Map in the mainThread adapter. See #12928
Show a simple up arrow next to packages that have a newer version available instead of expanding a details panel when selected. See #12928
Keep package metadata in a separate cache that persists across refreshes. When refreshing packages, existing cached metadata is preserved and only new packages trigger a P3M API call. See #12928
Add a "Refresh Metadata" action in the overflow menu that clears the metadata cache and fetches fresh data from P3M. Also add a guard to prevent concurrent metadata fetch calls. See #12928
d1c152b to
e81b98e
Compare
Conda package names don't cleanly map to PyPI, so fetching metadata from P3M's PyPI repo could return incorrect latest versions and licenses for conda packages. See #12928
The sort dropdown is being developed on a separate branch; it was unrelated to the P3M metadata work in this PR. See #12928
The AbortController was wired up via an undisposed token.onCancellationRequested subscription, so a long-lived token (e.g. a session-scoped token reused across many metadata fetches) would accumulate listeners. Inline the signal wiring and dispose the subscription in a finally block. See #12928
refreshPackages inlined the same two-stage fetch pattern already in _refreshPackagesInternal. Have it delegate to the internal helper and just wrap with the refresh state fire. See #12928
The pip and uv package managers had identical 12-line blocks converting P3M API fields onto the LanguageRuntimePackage shape. Hoist the mapping into fetchP3MPackageMetadata and have the managers delegate. See #12928
Keep only the fields currently (or imminently) consumed by the UI: latestVersion, license, publishedDate. Drop description, availableVersions, packageSize, and downloads from the API surface until there's a consumer for them. See #12928
There was a problem hiding this comment.
Looking at refreshMetadata:
refreshMetadata's cache clear races with the background _fetchAndMergeMetadata from refreshPackages, causing the forced refresh to silently no-op (guard bails early) while the old in-flight fetch repopulates the cache with stale data keyed to the previous package list — fix by tracking the fetch as a CancelablePromise<void> and cancel-and-restart instead of guarding.
| /** | ||
| * P3M package metadata returned from the API. | ||
| */ | ||
| interface P3MPackageMetadata { |
There was a problem hiding this comment.
Everything in P3MPackageMetadata should be readonly.
interface P3MPackageMetadata {
readonly name: string;
readonly version: string;
readonly summary: string | null;
readonly license: string | null;
readonly licenses?: readonly string[];
readonly license_types?: readonly string[];
readonly package_date: string | null;
readonly package_size: number | null;
readonly downloads: number | null;
readonly available_versions?: readonly string[];
readonly dependencies?: {
readonly imports?: ReadonlyArray<{ readonly name: string; readonly version?: string; readonly operator?: string }>;
readonly suggests?: ReadonlyArray<{ readonly name: string; readonly version?: string; readonly operator?: string }>;
};
}
| /** | ||
| * Request body for the P3M filter packages API. | ||
| */ | ||
| interface P3MFilterRequest { |
There was a problem hiding this comment.
Everything in P3MFilterRequest should be readonly.
interface P3MFilterRequest {
readonly names: readonly string[];
readonly repo: string;
readonly omit_downloads?: boolean;
readonly omit_dependencies?: boolean;
readonly omit_package_details?: boolean;
}
| * Parse NDJSON (newline-delimited JSON) response into an array of objects. | ||
| * Each line in the response is a separate JSON object. | ||
| */ | ||
| function parseNDJSON<T>(text: string): T[] { |
There was a problem hiding this comment.
I'm curious as to why this is a generic when it's used only once as:
const packages = parseNDJSON<P3MPackageMetadata>(text);
Do you expect there to be other uses in the future?
There was a problem hiding this comment.
This was just what Claude did originally.
The generic implementation makes sense to me and could be reused if necessary. It's all compile time and has no bearing on its actual functionality.
IMO the non-generic version is kinda weird because it's so specific to a given type, when the functionality doesn't care about the type.
|
|
||
| const controller = token ? new AbortController() : undefined; | ||
| const cancelSubscription = | ||
| controller && token ? token.onCancellationRequested(() => controller.abort()) : undefined; |
There was a problem hiding this comment.
const controller = new AbortController();
const cancelSubscription = token?.onCancellationRequested(() => controller.abort());
Seems simpler.
| * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| 'use strict'; |
There was a problem hiding this comment.
No-op in TS modules (already strict).
| } | ||
|
|
||
| // Clear the cache to force a full refresh | ||
| this._metadataCache.clear(); |
There was a problem hiding this comment.
Race between refreshMetadata's cache clear and the background fetch.
refreshPackages fires off _fetchAndMergeMetadata as a background task with CancellationToken.None (intentional, so it isn't killed by the caller's token). If the user invokes refreshMetadata while that background fetch is in flight, the current code:
- Clears
_metadataCache. - Calls
_fetchAndMergeMetadata, which hits the_metadataFetchInProgressguard and returns early. - Resolves, so the caller thinks the forced refresh succeeded.
- The original background fetch eventually completes and repopulates the cache — with data keyed against the earlier package list, not the current one.
The "force refresh" contract isn't met, and the final cache state depends on whichever request was in flight.
Suggested fix: replace the _metadataFetchInProgress boolean with a tracked in-flight handle so callers can cancel it explicitly, and use cancel-and-restart instead of a guard.
private _metadataFetch?: CancelablePromise<void>;_fetchAndMergeMetadatawraps its body increateCancelablePromise, stores the handle onthis._metadataFetch, and cancels any prior value at the top (so re-entrance supersedes rather than no-ops).- Pass the internal token into
packageManager.getPackageMetadataand re-checktoken.isCancellationRequestedafter the await, before writing to the cache — guarantees a cancelled fetch can't pollute the cache after a caller has cleared it. - Propagate the external caller's token:
externalToken.onCancellationRequested(() => fetch.cancel()), and dispose the subscription in a.finally. - Swallow
CancellationError(viaisCancellationError); keep logging for genuine failures. - In
refreshMetadata, callthis._metadataFetch?.cancel()beforethis._metadataCache.clear()— closes the race. - Override
dispose()to cancel any in-flight fetch so background work doesn't outlive the instance.
Net effect: the _metadataFetchInProgress flag goes away, and refreshMetadata reliably performs a full re-fetch regardless of what else is in flight.
refreshMetadata's cache clear could race with the background fetch from refreshPackages: the boolean guard would bail the forced refresh early, and the original fetch would then repopulate the cache keyed to the previous package list. Replace the guard with a CancelablePromise handle so re-entrance supersedes the prior fetch, cancel it before clearing the cache, and re-check cancellation post-await so a cancelled fetch can't pollute the cache. Also address PR feedback: readonly interfaces in p3mSearch, simpler AbortController setup, drop 'use strict' in the unit test.

See #12928
Adds package metadata from Posit Package Manager (P3M) to the Packages pane for Python environments (except conda). Packages that have a newer version available now show an up arrow indicator.
This adds support for fetching package metadata to supplement the packages pane. There are two methods:
Here is a demo with an added up arrow next to any outdated package.
Note that the code has support for fetching additional metadata, which I had started to implement. However, this simple up arrow icon was the most subtle and simple solution to prove out the basic data flows, when to automatically fetch metadata, and manually supporting the action.
Details
P3M Integration:
p3mSearch.tsmodule in positron-python extension callsPOST https://p3m.dev/__api__/filter/packagesUI Changes:
↑) indicator appears next to package version whenlatestVersion !== versionArchitecture:
PositronPackagesInstance$getPackageMetadatafor extension host communicationRelease Notes
New Features
Bug Fixes
QA Notes
This only supports PIP and UV projects. So be sure you're not in a conda session.
@:packages-pane