Adds support for stronglyRecommended extensions. Implements #299039#299040
Adds support for stronglyRecommended extensions. Implements #299039#299040
Conversation
📬 CODENOTIFYThe following users are being notified based on files changed in this PR: @bpaseroMatched files:
|
There was a problem hiding this comment.
Pull request overview
This PR implements the stronglyRecommended field in .vscode/extensions.json (#299039). Unlike regular recommendations (which show a passive toast), extensions in stronglyRecommended trigger a modal dialog prompting installation on workspace open. The dialog allows users to selectively install extensions and optionally suppress future prompts (with a "do not show again unless major version change" option).
Changes:
- Adds
stronglyRecommendedfield toIExtensionsConfigContent, JSON schema, and parsing logic across config service and workspace recommendations - Introduces
stronglyRecommendedExtensionList.ts(new UI component for the checkbox list in the dialog) and extendsIDialogService/BrowserDialogHandlerwithrenderBody/buttonOptionssupport - Adds
promptStronglyRecommendedExtensions()toIExtensionRecommendationNotificationServiceincluding per-workspace storage-based ignore state (simple and major-version-aware)
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
workspaceExtensionsConfig.ts |
Adds stronglyRecommended to IExtensionsConfigContent, interface, and parsing |
extensionsFileTemplate.ts |
Adds stronglyRecommended JSON schema entry |
workspaceRecommendations.ts |
Populates _stronglyRecommended and adds entries to _recommendations during fetch() |
extensionRecommendationsService.ts |
Calls promptStronglyRecommendedExtensions() during activation |
stronglyRecommendedExtensionList.ts |
New file: checkbox-list UI for the strongly recommended dialog body |
extensionRecommendationNotificationService.ts |
Implements promptStronglyRecommendedExtensions() with dialog, ignore lists, install logic |
extensionRecommendations.ts |
Extends IExtensionRecommendationNotificationService interface |
extensionRecommendationsIpc.ts |
Adds stub IPC implementations for new interface methods |
dialogs.ts |
Adds renderBody, buttonOptions, ICustomDialogButtonOptions, ICustomDialogButtonControl to dialog API |
dialogHandler.ts |
Wires renderBody and buttonOptions through to the underlying Dialog |
extensions.contribution.ts |
Registers "Reset Strongly Recommended Extensions Ignore State" command |
.vscode/extensions.json |
Moves github.vscode-pull-request-github to stronglyRecommended, adds two more |
stronglyRecommendedDialog.fixture.ts |
New component fixture for UI preview of the dialog |
| if (result) { | ||
| const selected = extensions.filter(e => listResult.checkboxStates.get(e)); | ||
| const unselected = extensions.filter(e => !listResult.checkboxStates.get(e)); | ||
| if (unselected.length) { | ||
| this._addToStronglyRecommendedIgnoreList( | ||
| unselected.map(e => e.identifier.id) | ||
| ); | ||
| } | ||
| if (listResult.doNotShowAgainUnlessMajorVersionChange()) { | ||
| this._addToStronglyRecommendedIgnoreWithMajorVersion( | ||
| extensions.map(e => ({ id: e.identifier.id, majorVersion: parseMajorVersion(e.version) })) | ||
| ); | ||
| } |
There was a problem hiding this comment.
The "Do not show again unless major version change" checkbox state is only processed inside if (result), meaning it's only honored when the user clicks "Install". If the user checks this option and then clicks "Cancel", their preference is silently discarded and the dialog will reappear on the next workspace open. The doNotShowAgainUnlessMajorVersionChange() check should also be evaluated (and the ignore state saved) when the user dismisses via Cancel, to respect the user's explicitly stated intent of not being shown the dialog again.
| if (extensionsConfig.stronglyRecommended) { | ||
| for (const extensionId of extensionsConfig.stronglyRecommended) { | ||
| if (invalidRecommendations.indexOf(extensionId) === -1) { | ||
| const workspaceExtUri = this.workspaceExtensionIds.get(extensionId.toLowerCase()); | ||
| const extension = workspaceExtUri ?? extensionId; | ||
| const reason = { | ||
| reasonId: ExtensionRecommendationReason.Workspace, | ||
| reasonText: localize('stronglyRecommendedExtension', "This extension is strongly recommended by users of the current workspace.") | ||
| }; | ||
| this._stronglyRecommended.push(extension); | ||
| if (workspaceExtUri) { | ||
| this._recommendations.push({ extension: workspaceExtUri, reason }); | ||
| } else { | ||
| this._recommendations.push({ extension: extensionId, reason }); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Strongly recommended extensions are added to both _recommendations (lines 155–159) and _stronglyRecommended (line 154). Since promptWorkspaceRecommendations() in ExtensionRecommendationsService uses workspaceRecommendations.recommendations (which now includes strongly recommended entries), those extensions will appear in BOTH the regular workspace recommendations toast notification (after 5 seconds) and the strongly recommended modal dialog (immediately). A user who dismisses or partially installs via the modal may then see the same extension recommended again in the toast. Consider filtering strongly recommended extensions out of the workspace recommendations notification to avoid duplicating prompts.
| doNotShowAgainUnlessMajorVersionChange: () => doNotShowCb.checked, | ||
| styleInstallButton(button: ICustomDialogButtonControl) { | ||
| const updateEnabled = () => { button.enabled = hasSelection(); }; | ||
| disposables.add(onSelectionChanged.event(updateEnabled)); |
There was a problem hiding this comment.
The styleInstallButton method subscribes to onSelectionChanged events via disposables.add(onSelectionChanged.event(updateEnabled)), but never calls updateEnabled() immediately upon registration. The initial enabled state of the Install button is therefore never explicitly set by styleInstallButton. Although the button defaults to enabled and all checkboxes start checked (so the initial state is coincidentally correct), this approach is fragile. If the initial state were to change, the button would be out of sync until the first checkbox interaction. Consider calling updateEnabled() once immediately after subscribing to ensure correctness.
| disposables.add(onSelectionChanged.event(updateEnabled)); | |
| disposables.add(onSelectionChanged.event(updateEnabled)); | |
| updateEnabled(); |
| row.appendChild(cb.domNode); | ||
|
|
||
| const label = row.appendChild($('span')); | ||
| label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`; |
There was a problem hiding this comment.
The user-visible label showing extension name, version, and publisher is a raw template literal that is not wrapped in localize(). The static text "v" (version prefix) is baked in and will not be translated. Looking at similar patterns in the codebase (e.g., extensionRecommendationNotificationService.ts line 261 uses localize('extensionFromPublisher', "'{0}' extension from {1}", ...)), user-facing strings containing extension metadata should be localized with placeholders.
| label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`; | |
| label.textContent = localize('stronglyRecommendedExtensionLabel', "{0} v{1} \u2014 {2}", ext.displayName, ext.version, ext.publisherDisplayName); |
| @@ -35,6 +36,7 @@ export interface IWorkspaceExtensionsConfigService { | |||
| readonly onDidChangeExtensionsConfigs: Event<void>; | |||
| getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>; | |||
| getRecommendations(): Promise<string[]>; | |||
There was a problem hiding this comment.
The getStronglyRecommended() method is added to the IWorkspaceExtensionsConfigService interface and implemented in WorkspaceExtensionsConfigService, but it is never called anywhere in the codebase. The strongly recommended data is accessed via workspaceRecommendations.stronglyRecommended instead. If this method is not needed as part of the public API, consider removing it to avoid dead code. If it is intended for future use or for external consumers, document that intent.
| getRecommendations(): Promise<string[]>; | |
| getRecommendations(): Promise<string[]>; | |
| /** | |
| * Returns the list of strongly recommended extensions from the workspace extensions configuration. | |
| * | |
| * Note: This is part of the public service API and may be used by external or future consumers, | |
| * even though current internal callers access strongly recommended extensions directly from | |
| * the underlying workspace recommendations data. | |
| */ |
| private _stronglyRecommended: Array<string | URI> = []; | ||
| get stronglyRecommended(): ReadonlyArray<string | URI> { return this._stronglyRecommended; } |
There was a problem hiding this comment.
No tests have been added for the new stronglyRecommended functionality. The existing extensionRecommendationsService.test.ts has comprehensive coverage for workspace recommendations (testing scenarios like: already installed extensions, format validation, ignore states, etc.). Similar tests should be added for strongly recommended extensions, covering at minimum: loading from extensions.json, format validation, filtering of already-installed extensions, and the ignore/major-version-ignore state persistence.
| stronglyRecommended: { | ||
| type: 'array', | ||
| description: localize('app.extensions.json.stronglyRecommended', "List of extensions that are strongly recommended for users of this workspace. Users will be prompted with a dialog to install these extensions. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), | ||
| items: { | ||
| type: 'string', | ||
| pattern: EXTENSION_IDENTIFIER_PATTERN, | ||
| errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") | ||
| }, | ||
| }, |
There was a problem hiding this comment.
The ExtensionsConfigurationInitialContent template (the scaffolded extensions.json content shown when creating a new file) does not include the new stronglyRecommended field. Adding a commented-out example entry similar to how recommendations and unwantedRecommendations are included would improve discoverability of the new feature.
Fixes #299039