Skip to content

Adds support for stronglyRecommended extensions. Implements #299039#299040

Open
hediet wants to merge 1 commit intomainfrom
hediet/b/substantial-bovid
Open

Adds support for stronglyRecommended extensions. Implements #299039#299040
hediet wants to merge 1 commit intomainfrom
hediet/b/substantial-bovid

Conversation

@hediet
Copy link
Member

@hediet hediet commented Mar 3, 2026

Fixes #299039

Copilot AI review requested due to automatic review settings March 3, 2026 19:33
@hediet hediet enabled auto-merge (rebase) March 3, 2026 19:33
@hediet hediet self-assigned this Mar 3, 2026
@vs-code-engineering
Copy link

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@bpasero

Matched files:

  • src/vs/platform/dialogs/common/dialogs.ts
  • src/vs/workbench/browser/parts/dialogs/dialogHandler.ts

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stronglyRecommended field to IExtensionsConfigContent, 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 extends IDialogService / BrowserDialogHandler with renderBody / buttonOptions support
  • Adds promptStronglyRecommendedExtensions() to IExtensionRecommendationNotificationService including 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

Comment on lines +552 to +564
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) }))
);
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +162
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 });
}
}
}
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
doNotShowAgainUnlessMajorVersionChange: () => doNotShowCb.checked,
styleInstallButton(button: ICustomDialogButtonControl) {
const updateEnabled = () => { button.enabled = hasSelection(); };
disposables.add(onSelectionChanged.event(updateEnabled));
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
disposables.add(onSelectionChanged.event(updateEnabled));
disposables.add(onSelectionChanged.event(updateEnabled));
updateEnabled();

Copilot uses AI. Check for mistakes.
row.appendChild(cb.domNode);

const label = row.appendChild($('span'));
label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
@@ -35,6 +36,7 @@ export interface IWorkspaceExtensionsConfigService {
readonly onDidChangeExtensionsConfigs: Event<void>;
getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>;
getRecommendations(): Promise<string[]>;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +29
private _stronglyRecommended: Array<string | URI> = [];
get stronglyRecommended(): ReadonlyArray<string | URI> { return this._stronglyRecommended; }
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +36
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'.")
},
},
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Strongly Recommended Extensions

2 participants