Derive chat extension enablement from chat.disableAIFeatures - #332111
Derive chat extension enablement from chat.disableAIFeatures#332111justanotheranonymoususer wants to merge 8 commits into
Conversation
chat.disableAIFeatures is registered with ConfigurationScope.WINDOW, so it is accepted in remote and workspace settings, but applying it persisted a global disable for the chat extension. That list is machine wide and shared by every window on the client, so a value set only in remote settings also disables chat in local windows, and a local window cannot recover on its own because the remote window re-applies the disable on every start. Add EnablementState.DisabledByAIFeaturesSetting and compute it in _computeEnablementState alongside the other window-derived reasons, so the extension is disabled exactly where the setting resolves. Nothing is persisted, which makes the state follow whichever settings.json applies to the window. canChangeEnablement reports false while the setting owns the state, and a configuration listener fires onEnablementChanged so the extensions view and the extension host pick the change up. ChatTeardownContribution no longer changes enablement. It keeps hiding the auxiliary bar and requesting the extension host restart. Installs that already recorded the global disable would otherwise keep it forever, so the enablement service drops that entry on startup once the setting resolves to true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Derives Copilot Chat extension enablement from the window-scoped chat.disableAIFeatures setting instead of persisting global disablement.
Changes:
- Adds a setting-derived enablement state and change notifications.
- Migrates legacy global disablement.
- Removes enablement mutation from chat teardown handling.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
extensionManagement.ts |
Adds the AI-setting disablement state. |
extensionEnablementService.ts |
Computes and reconciles chat enablement. |
chatSetupContributions.ts |
Limits teardown to UI cleanup and extension-host updates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
|
|
||
| this.ensureChatExtensionInitialDisabledState(); | ||
| this.reconcileChatExtensionGlobalDisablement(); |
| case EnablementState.DisabledByAIFeaturesSetting: | ||
| throw new Error(localize('cannot change enablement ai features', "Cannot change enablement of {0} extension because AI features are disabled in settings", extension.manifest.displayName || extension.identifier.id)); |
| else if (this._isDisabledByAIFeaturesSetting(extension)) { | ||
| enablementState = EnablementState.DisabledByAIFeaturesSetting; | ||
| } |
| if (this.configurationService.getValue(ChatAIDisabledSettingId) !== true || !this._isDisabledGlobally({ id: this._chatExtensionId })) { | ||
| return; | ||
| } | ||
|
|
||
| this.logService.debug('Removing global disablement of builtin chat extension in favor of chat.disableAIFeatures'); | ||
| this.globalExtensionEnablementService.enableExtension({ id: this._chatExtensionId }, SOURCE) |
Follow-up to deriving the chat extension's enablement from chat.disableAIFeatures. Everything that turns AI features back on has to clear the setting first, and the cleanup of the legacy global disable has to survive the migration writing that setting asynchronously. reconcileChatExtensionGlobalDisablement also runs from the configuration listener now. ensureChatExtensionInitialDisabledState updates the setting through an unawaited updateValue, so the constructor pass read the pre-migration value and left the stale global disable in place, where it would have stayed forever. EnableAIFeaturesInWorkspaceAction gated on canChangeWorkspaceEnablement, which reports false once the setting owns the state, disabling the one action meant to escape a workspace or remote value. It checks the workspace state directly, exempts the derived state, and clears the setting before touching persisted enablement. ensureChatExtensionEnabled called setEnablement while the setting was still true, which throws instead of enabling chat for Manage Language Models. It clears the setting first, through the untargeted updateValue so a remote value is cleared where it is defined, and changes persisted enablement only if the extension is still disabled afterwards. Adds tests for the derived state, the rejection of enablement changes, the change event, and eviction of the legacy global disable both on startup and on a later flip, plus a negative case asserting a genuine global disable survives while the setting is false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts:803
- This method-body explanation exceeds the codebase's one-line inline-comment convention. Keep only the non-obvious extension-host restart requirement.
// The chat extension is disabled by the enablement service deriving this setting,
// the extension host still has to be told to pick the change up.
src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts:215
- This migration only clears the profile-wide legacy entry, but the removed teardown code also persisted
DisabledWorkspacewhenever the setting had a workspace value. After upgrading such a workspace, changingchat.disableAIFeaturesfrom true to false directly leaves that workspace entry behind, so chat remains disabled instead of following the setting. Reconcile the legacy workspace-disabled entry as well and cover the true-to-false configuration path.
if (this.configurationService.getValue(ChatAIDisabledSettingId) !== true || !this._isDisabledGlobally({ id: this._chatExtensionId })) {
return;
}
this.logService.debug('Removing global disablement of builtin chat extension in favor of chat.disableAIFeatures');
src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts:210
- This method-body block is longer than the codebase's brief-inline-comment convention and largely restates the reconciliation logic. Condense it to the essential migration invariant so it is less likely to drift from the implementation.
// A globally disabled chat extension applies to every window on this machine, whereas
// `chat.disableAIFeatures` applies only where its configuration resolves. Releases that
// implemented the setting as a global disable leak into windows the setting does not cover,
// so drop the entry that the derived enablement state already accounts for. Runs on every
// change too, because the migration above settles the setting asynchronously.
| this.ensureChatExtensionInitialDisabledState(); | ||
| this.reconcileChatExtensionGlobalDisablement(); |
| // While the setting resolves to true it owns enablement and canChangeWorkspaceEnablement | ||
| // reports false. `run` clears the setting before touching persisted state, so this action | ||
| // stays the way out of it. |
| // Enablement is derived from `chat.disableAIFeatures` wherever it resolves to true, so | ||
| // the setting has to go before persisted enablement can be changed. |
The global disabled-extension list is profile scoped while the enablement service survives profile switches, so an entry in another profile was never reconciled: no configuration event fires, and clearing the setting afterwards left that profile disabled. Reconciliation now also runs from _computeEnablementState, next to ensureChatExtensionInitialDisabledState, which sits there for the same reason. The removed teardown persisted DisabledWorkspace whenever the setting had a workspace value, so an upgraded workspace keeps an entry that outlives the setting. Reconciliation mirrors that branch: a workspace value of true drops the workspace entry, anything else drops the global one. Evicting on the effective value alone would also discard the workspace entry that DisableAIFeaturesInWorkspaceAction writes without touching the setting. Running on every computation would otherwise wipe the disable ensureChatExtensionInitialDisabledState writes for a profile where chat setup never completed, which would activate chat there as soon as the setting was cleared. The global branch bails while the entitlement context reports setup incomplete, matching that method's own condition. reconcileChatExtensionGlobalDisablement becomes reconcileChatExtensionDisablement now that it covers both stores, and three method-body comments are condensed to one line each. Adds tests for reconciliation without a configuration event, for the workspace entry being dropped and the setting then following true to false, and for the incomplete-setup disable surviving. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| } | ||
|
|
||
| // Enablement is derived, but the extension host still has to be told to pick the change up. | ||
| await this.extensionsWorkbenchService.updateRunningExtensions(chatDisabled ? localize('restartExtensionHost.reason.disable', "Disabling AI features") : localize('restartExtensionHost.reason.enable', "Enabling AI features")); |
| if (!this.extensionEnablementService.isEnabled(this.extension.local)) { | ||
| await this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.EnabledWorkspace); |
DisableAIFeaturesInWorkspaceAction persisted DisabledWorkspace and never wrote chat.disableAIFeatures, so the extension went down while the setting stayed false and every setting-driven check disagreed with it. It writes true at ConfigurationTarget.WORKSPACE now and lets the derived state disable the extension, gating on a workspace existing plus the setting and current enablement rather than on canChangeWorkspaceEnablement. Its own updateRunningExtensions call goes away because the configuration listener in ChatTeardownContribution already covers it. Nothing writes a workspace disable for the chat extension any more, so the workspace branch of reconcileChatExtensionDisablement only ever sees entries left by older releases. getRuntimeState reports ReloadWindow whenever a remote extension management server exists, and updateRunningExtensions skips every state except RestartExtensions, so toggling the setting left the remote extension host running, which is the scenario this whole change is for. handleChatDisabled reads the chat extension's runtime state and reloads when it asks for one, matching the mapping the workbench uses for its own restart-required notification and in ExtensionRuntimeStateAction. Both paths that re-enable chat now check canChangeEnablement before calling setEnablement. Clearing one configuration target can leave a higher precedence value in place, and the setting still owns enablement in that case, so setEnablement would throw instead of enabling anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts:218
- A higher-precedence workspace value of
falsetakes this return path, so aDisabledGloballyentry persisted by an older build is left in place. In that case the derived AI-setting state is false but_getUserEnablementStatestill reportsDisabledGlobally; editing the workspace setting directly therefore cannot recover this window (only the action path compensates by persistingEnabledWorkspace). Reconcile the legacy global entry when a workspace override masks the user/remotetruevalue, or use a one-time migration marker so persisted state cannot override the resolved setting.
return;
| if (!extensionEnablementService.isEnabled(local) && extensionEnablementService.canChangeEnablement(local)) { | ||
| await extensionsWorkbenchService.setEnablement([chatExtension], EnablementState.EnabledGlobally); | ||
| } |
ensureChatExtensionEnabled guarded setEnablement on canChangeEnablement, which turned every unchangeable reason into a silent success: allowlist, environment, malicious, workspace trust. Manage Language Models then opened its editor with chat still disabled and nothing explaining why, where setEnablement previously threw and stopped the command. The guard is gone, so once the setting is cleared setEnablement validates whatever reason remains and reports it. EnableAIFeaturesInWorkspaceAction keeps a narrower check. Dropping the guard there makes it throw in extensionsActions.test.ts, whose stubbed updateValue is a no-op so the setting never clears. It returns early only when the setting still resolves to true after its own write, which means a more specific value masks it and there is no persisted state to fix, and otherwise lets setEnablement report the reason. reconcileChatExtensionDisablement branched on the current workspace value, but the branch older releases took depended on the workspace value at the time they ran. A user value of true that persisted a global disable, later masked by a workspace false, took the workspace return and left that entry in place forever: the derived state is off while _getUserEnablementState still reports DisabledGlobally, and no edit of the setting can recover the window. The two stores are reconciled independently now, and the global one against the user and application values rather than the resolved one. The workspace value stub in the tests set the user value alongside the workspace one, which made the two indistinguishable. It resolves them separately now, the way the real service does, and a case covers the masked global entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| // is not recreated, but the storage scope changes to the new profile. | ||
| if (extension.identifier.id.toLowerCase() === this._chatExtensionId) { | ||
| this.ensureChatExtensionInitialDisabledState(); | ||
| this.reconcileChatExtensionDisablement(); |
…--------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './media/extensionActions.css';
import { localize, localize2 } from '../../../../nls.js';
import { IAction, Action, Separator, SubmenuAction, IActionChangeEvent } from '../../../../base/common/actions.js';
import { Delayer, Promises, Throttler } from '../../../../base/common/async.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import * as json from '../../../../base/common/json.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { disposeIfDisposable } from '../../../../base/common/lifecycle.js';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js';
import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js';
import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode, IAllowedExtensionsService, shouldRequireRepositorySignatureFor } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js';
import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
import { areSameExtensions, getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, getWorkspaceSupportTypeMessage, TargetPlatform, isApplicationScopedExtension } from '../../../../platform/extensions/common/extensions.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { IFileService, IFileContent } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { IExtensionService, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js';
import { URI } from '../../../../base/common/uri.js';
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ChatAIDisabledSettingId } from '../../../../platform/chat/common/chatSettings.js';
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { buttonBackground, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator, buttonSecondaryBorder } from '../../../../platform/theme/common/colorRegistry.js';
import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js';
import { ITextEditorSelection } from '../../../../platform/editor/common/editor.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js';
import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IQuickPickItem, IQuickInputService, QuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { alert } from '../../../../base/browser/ui/aria/aria.js';
import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from '../../../services/themes/common/workbenchThemeService.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IDialogService, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js';
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
import { IActionViewItemOptions, ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from '../../../services/extensionRecommendations/common/workspaceExtensionsConfig.js';
import { getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';
import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js';
import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { errorIcon, infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from './extensionsIcons.js';
import { isIOS, isWeb, language } from '../../../../base/common/platform.js';
import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js';
import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js';
import { createCommandUri, escapeMarkdownSyntaxTokens, IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { fromNow } from '../../../../base/common/date.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { getLocale } from '../../../../platform/languagePacks/common/languagePacks.js';
import { ILocaleService } from '../../../services/localization/common/locale.js';
import { isString } from '../../../../base/common/types.js';
import { showWindowLogActionId } from '../../../services/log/common/logConstants.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { IUpdateService } from '../../../../platform/update/common/update.js';
import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
import { IAuthenticationUsageService } from '../../../services/authentication/browser/authenticationUsageService.js';
import { IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { IWorkbenchIssueService } from '../../issue/common/issue.js';
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
import { getWorkbenchMenuMotionContextMenuOptions } from '../../../browser/actions/menuMotion.js';
export class PromptExtensionInstallFailureAction extends Action {
constructor(
private readonly extension: IExtension,
private readonly options: InstallOptions | undefined,
private readonly version: string,
private readonly installOperation: InstallOperation,
private readonly error: Error,
@IProductService private readonly productService: IProductService,
@IOpenerService private readonly openerService: IOpenerService,
@INotificationService private readonly notificationService: INotificationService,
@IDialogService private readonly dialogService: IDialogService,
@ICommandService private readonly commandService: ICommandService,
@ILogService private readonly logService: ILogService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,
@IWorkbenchIssueService private readonly workbenchIssueService: IWorkbenchIssueService,
) {
super('extension.promptExtensionInstallFailure');
}
override async run(): Promise<void> {
if (isCancellationError(this.error)) {
return;
}
this.logService.error(this.error);
if (this.error.name === ExtensionManagementErrorCode.Unsupported) {
const productName = isWeb ? localize('VS Code for Web', "{0} for the Web", this.productService.nameLong) : this.productService.nameLong;
const message = localize('cannot be installed', "The '{0}' extension is not available in {1}. Click 'More Information' to learn more.", this.extension.displayName || this.extension.identifier.id, productName);
const { confirmed } = await this.dialogService.confirm({
type: Severity.Info,
message,
primaryButton: localize({ key: 'more information', comment: ['&& denotes a mnemonic'] }, "&&More Information"),
cancelButton: localize('close', "Close")
});
if (confirmed) {
this.openerService.open(isWeb ? URI.parse('https://aka.ms/vscode-web-extensions-guide') : URI.parse('https://aka.ms/vscode-remote'));
}
return;
}
if (ExtensionManagementErrorCode.ReleaseVersionNotFound === (<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.prompt({
type: 'error',
message: getErrorMessage(this.error),
buttons: [{
label: localize('install prerelease', "Install Pre-Release"),
run: () => {
const installAction = this.instantiationService.createInstance(InstallAction, { installPreReleaseVersion: true });
installAction.extension = this.extension;
return installAction.run();
}
}],
cancelButton: localize('cancel', "Cancel")
});
return;
}
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.info(getErrorMessage(this.error));
return;
}
if (ExtensionManagementErrorCode.PackageNotSigned === (<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.prompt({
type: 'error',
message: localize('not signed', "'{0}' is an extension from an unknown source. Are you sure you want to install?", this.extension.displayName),
detail: getErrorMessage(this.error),
buttons: [{
label: localize('install anyway', "Install Anyway"),
run: () => {
const installAction = this.instantiationService.createInstance(InstallAction, { ...this.options, donotVerifySignature: true, });
installAction.extension = this.extension;
return installAction.run();
}
}],
cancelButton: true
});
return;
}
if (ExtensionManagementErrorCode.SignatureVerificationFailed === (<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.prompt({
type: 'error',
message: localize('verification failed', "Cannot install '{0}' extension because {1} cannot verify the extension signature", this.extension.displayName, this.productService.nameLong),
detail: getErrorMessage(this.error),
buttons: [{
label: localize('learn more', "Learn More"),
run: () => this.openerService.open('https://code.visualstudio.com/docs/editor/extension-marketplace#_the-extension-signature-cannot-be-verified-by-vs-code')
}, {
label: localize('install donot verify', "Install Anyway (Don't Verify Signature)"),
run: () => {
const installAction = this.instantiationService.createInstance(InstallAction, { ...this.options, donotVerifySignature: true, });
installAction.extension = this.extension;
return installAction.run();
}
}],
cancelButton: true
});
return;
}
if (ExtensionManagementErrorCode.SignatureVerificationInternal === (<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.prompt({
type: 'error',
message: localize('verification failed', "Cannot install '{0}' extension because {1} cannot verify the extension signature", this.extension.displayName, this.productService.nameLong),
detail: getErrorMessage(this.error),
buttons: [{
label: localize('learn more', "Learn More"),
run: () => this.openerService.open('https://code.visualstudio.com/docs/editor/extension-marketplace#_the-extension-signature-cannot-be-verified-by-vs-code')
}, {
label: localize('report issue', "Report Issue"),
run: () => this.workbenchIssueService.openReporter({
issueTitle: localize('report issue title', "Extension Signature Verification Failed: {0}", this.extension.displayName),
issueBody: localize('report issue body', "Please include following log `F1 > Open View... > Shared` below.\n\n")
})
}, {
label: localize('install donot verify', "Install Anyway (Don't Verify Signature)"),
run: () => {
const installAction = this.instantiationService.createInstance(InstallAction, { ...this.options, donotVerifySignature: true, });
installAction.extension = this.extension;
return installAction.run();
}
}],
cancelButton: true
});
return;
}
const operationMessage = this.installOperation === InstallOperation.Update ? localize('update operation', "Error while updating '{0}' extension.", this.extension.displayName || this.extension.identifier.id)
: localize('install operation', "Error while installing '{0}' extension.", this.extension.displayName || this.extension.identifier.id);
let additionalMessage;
const promptChoices: IPromptChoice[] = [];
const downloadUrl = await this.getDownloadUrl();
if (downloadUrl) {
additionalMessage = localize('check logs', "Please check the [log]({0}) for more details.", createCommandUri(showWindowLogActionId).toString());
promptChoices.push({
label: localize('download', "Try Downloading Manually..."),
run: () => this.openerService.open(downloadUrl).then(() => {
this.notificationService.prompt(
Severity.Info,
localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', this.extension.identifier.id),
[{
label: localize('installVSIX', "Install from VSIX..."),
run: () => this.commandService.executeCommand(SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID)
}]
);
})
});
}
const message = `${operationMessage}${additionalMessage ? ` ${additionalMessage}` : ''}`;
this.notificationService.prompt(Severity.Error, message, promptChoices);
}
private async getDownloadUrl(): Promise<URI | undefined> {
if (isIOS) {
return undefined;
}
if (!this.extension.gallery) {
return undefined;
}
if (!this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer) {
return undefined;
}
let targetPlatform = this.extension.gallery.properties.targetPlatform;
if (targetPlatform !== TargetPlatform.UNIVERSAL && targetPlatform !== TargetPlatform.UNDEFINED && this.extensionManagementServerService.remoteExtensionManagementServer) {
try {
const manifest = await this.galleryService.getManifest(this.extension.gallery, CancellationToken.None);
if (manifest && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(manifest)) {
targetPlatform = await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.getTargetPlatform();
}
} catch (error) {
this.logService.error(error);
return undefined;
}
}
if (targetPlatform === TargetPlatform.UNKNOWN) {
return undefined;
}
const [extension] = await this.galleryService.getExtensions([{
...this.extension.identifier,
version: this.version
}], {
targetPlatform
}, CancellationToken.None);
if (!extension) {
return undefined;
}
return URI.parse(extension.assets.download.uri);
}
}
export interface IExtensionActionChangeEvent extends IActionChangeEvent {
readonly hidden?: boolean;
readonly menuActions?: IAction[];
}
export abstract class ExtensionAction extends Action implements IExtensionContainer {
protected override _onDidChange = this._register(new Emitter<IExtensionActionChangeEvent>());
override get onDidChange() { return this._onDidChange.event; }
static readonly EXTENSION_ACTION_CLASS = 'extension-action';
static readonly TEXT_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} text`;
static readonly LABEL_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} label`;
static readonly ICON_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} icon`;
private _extension: IExtension | null = null;
get extension(): IExtension | null { return this._extension; }
set extension(extension: IExtension | null) { this._extension = extension; this.update(); }
private _hidden: boolean = false;
get hidden(): boolean { return this._hidden; }
set hidden(hidden: boolean) {
if (this._hidden !== hidden) {
this._hidden = hidden;
this._onDidChange.fire({ hidden });
}
}
protected override _setEnabled(value: boolean): void {
super._setEnabled(value);
if (this.hideOnDisabled) {
this.hidden = !value;
}
}
protected hideOnDisabled: boolean = true;
abstract update(): void;
}
export class ButtonWithDropDownExtensionAction extends ExtensionAction {
private primaryAction: IAction | undefined;
readonly menuActionClassNames: string[] = [];
private _menuActions: IAction[] = [];
get menuActions(): IAction[] { return [...this._menuActions]; }
override get extension(): IExtension | null {
return super.extension;
}
override set extension(extension: IExtension | null) {
this.extensionActions.forEach(a => a.extension = extension);
super.extension = extension;
}
protected readonly extensionActions: ExtensionAction[];
constructor(
id: string,
clazz: string,
private readonly actionsGroups: ExtensionAction[][],
) {
clazz = `${clazz} action-dropdown`;
super(id, undefined, clazz);
this.menuActionClassNames = clazz.split(' ');
this.hideOnDisabled = false;
this.extensionActions = actionsGroups.flat();
this.update();
this._register(Event.any(...this.extensionActions.map(a => a.onDidChange))(() => this.update(true)));
this.extensionActions.forEach(a => this._register(a));
}
update(donotUpdateActions?: boolean): void {
if (!donotUpdateActions) {
this.extensionActions.forEach(a => a.update());
}
const actionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => !a.hidden));
let actions: IAction[] = [];
for (const visibleActions of actionsGroups) {
if (visibleActions.length) {
actions = [...actions, ...visibleActions, new Separator()];
}
}
actions = actions.length ? actions.slice(0, actions.length - 1) : actions;
this.primaryAction = actions[0];
this._menuActions = actions.length > 1 ? actions : [];
this._onDidChange.fire({ menuActions: this._menuActions });
if (this.primaryAction) {
this.hidden = false;
this.enabled = this.primaryAction.enabled;
this.label = this.getLabel(this.primaryAction as ExtensionAction);
this.tooltip = this.primaryAction.tooltip;
} else {
this.hidden = true;
this.enabled = false;
}
}
override async run(): Promise<void> {
if (this.enabled) {
await this.primaryAction?.run();
}
}
protected getLabel(action: ExtensionAction): string {
return action.label;
}
}
export class ButtonWithDropdownExtensionActionViewItem extends ActionWithDropdownActionViewItem {
constructor(
action: ButtonWithDropDownExtensionAction,
options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions,
contextMenuProvider: IContextMenuProvider
) {
super(null, action, options, contextMenuProvider);
this._register(action.onDidChange(e => {
if (e.hidden !== undefined || e.menuActions !== undefined) {
this.updateClass();
}
}));
}
override render(container: HTMLElement): void {
super.render(container);
this.updateClass();
}
protected override updateClass(): void {
super.updateClass();
if (this.element && this.dropdownMenuActionViewItem?.element) {
this.element.classList.toggle('hide', (<ButtonWithDropDownExtensionAction>this._action).hidden);
const isMenuEmpty = (<ButtonWithDropDownExtensionAction>this._action).menuActions.length === 0;
this.element.classList.toggle('empty', isMenuEmpty);
this.dropdownMenuActionViewItem.element.classList.toggle('hide', isMenuEmpty);
}
}
}
export class InstallAction extends ExtensionAction {
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;
private static readonly HIDE = `${this.CLASS} hide`;
protected _manifest: IExtensionManifest | null = null;
set manifest(manifest: IExtensionManifest | null) {
this._manifest = manifest;
this.updateLabel();
}
private readonly updateThrottler = this._register(new Throttler());
public readonly options: InstallOptions;
constructor(
options: InstallOptions,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionService private readonly runtimeExtensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@ILabelService private readonly labelService: ILabelService,
@IDialogService private readonly dialogService: IDialogService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService,
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
) {
super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false);
this.hideOnDisabled = false;
this.options = { isMachineScoped: false, ...options };
this.update();
this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update()));
this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this));
}
update(): void {
this.updateThrottler.queue(() => this.computeAndUpdateEnablement());
}
protected async computeAndUpdateEnablement(): Promise<void> {
this.enabled = false;
this.class = InstallAction.HIDE;
this.hidden = true;
if (!this.extension) {
return;
}
if (this.extension.isBuiltin) {
return;
}
if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) {
return;
}
if (this.extension.state !== ExtensionState.Uninstalled) {
return;
}
if (this.options.installPreReleaseVersion && (!this.extension.hasPreReleaseVersion || this.allowedExtensionsService.isAllowed({ id: this.extension.identifier.id, publisherDisplayName: this.extension.publisherDisplayName, prerelease: true }) !== true)) {
return;
}
if (!this.options.installPreReleaseVersion && !this.extension.hasReleaseVersion) {
return;
}
this.hidden = false;
this.class = InstallAction.CLASS;
if (await this.extensionsWorkbenchService.canInstall(this.extension) === true) {
this.enabled = true;
this.updateLabel();
}
}
override async run(): Promise<any> {
if (!this.extension) {
return;
}
if (this.extension.gallery && !this.extension.gallery.isSigned && shouldRequireRepositorySignatureFor(this.extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) {
const { result } = await this.dialogService.prompt({
type: Severity.Warning,
message: localize('not signed', "'{0}' is an extension from an unknown source. Are you sure you want to install?", this.extension.displayName),
detail: localize('not signed detail', "Extension is not signed."),
buttons: [
{
label: localize('install anyway', "Install Anyway"),
run: () => {
this.options.donotVerifySignature = true;
return true;
}
}
],
cancelButton: {
run: () => false
}
});
if (!result) {
return;
}
}
if (this.extension.deprecationInfo) {
let detail: string | MarkdownString = localize('deprecated message', "This extension is deprecated as it is no longer being maintained.");
enum DeprecationChoice {
InstallAnyway = 0,
ShowAlternateExtension = 1,
ConfigureSettings = 2,
Cancel = 3
}
const buttons: IPromptButton<DeprecationChoice>[] = [
{
label: localize('install anyway', "Install Anyway"),
run: () => DeprecationChoice.InstallAnyway
}
];
if (this.extension.deprecationInfo.extension) {
detail = localize('deprecated with alternate extension message', "This extension is deprecated. Use the {0} extension instead.", this.extension.deprecationInfo.extension.displayName);
const alternateExtension = this.extension.deprecationInfo.extension;
buttons.push({
label: localize({ key: 'Show alternate extension', comment: ['&& denotes a mnemonic'] }, "&&Open {0}", this.extension.deprecationInfo.extension.displayName),
run: async () => {
const [extension] = await this.extensionsWorkbenchService.getExtensions([{ id: alternateExtension.id, preRelease: alternateExtension.preRelease }], CancellationToken.None);
await this.extensionsWorkbenchService.open(extension);
return DeprecationChoice.ShowAlternateExtension;
}
});
} else if (this.extension.deprecationInfo.settings) {
detail = localize('deprecated with alternate settings message', "This extension is deprecated as this functionality is now built-in to VS Code.");
const settings = this.extension.deprecationInfo.settings;
buttons.push({
label: localize({ key: 'configure in settings', comment: ['&& denotes a mnemonic'] }, "&&Configure Settings"),
run: async () => {
await this.preferencesService.openSettings({ query: settings.map(setting => `@id:${setting}`).join(' ') });
return DeprecationChoice.ConfigureSettings;
}
});
} else if (this.extension.deprecationInfo.additionalInfo) {
detail = new MarkdownString(`${detail} ${this.extension.deprecationInfo.additionalInfo}`);
}
const { result } = await this.dialogService.prompt({
type: Severity.Warning,
message: localize('install confirmation', "Are you sure you want to install '{0}'?", this.extension.displayName),
detail: isString(detail) ? detail : undefined,
custom: isString(detail) ? undefined : {
markdownDetails: [{
markdown: detail
}]
},
buttons,
cancelButton: {
run: () => DeprecationChoice.Cancel
}
});
if (result !== DeprecationChoice.InstallAnyway) {
return;
}
}
this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: this.options.installPreReleaseVersion });
alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName));
/* __GDPR__
"extensions:action:install" : {
"owner": "sandy081",
"actionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
this.telemetryService.publicLog('extensions:action:install', { ...this.extension.telemetryData, actionId: this.id });
const extension = await this.install(this.extension);
if (extension?.local) {
alert(localize('installExtensionComplete', "Installing extension {0} is completed.", this.extension.displayName));
const runningExtension = await this.getRunningExtension(extension.local);
if (runningExtension && !(runningExtension.activationEvents && runningExtension.activationEvents.some(activationEent => activationEent.startsWith('onLanguage')))) {
const action = await this.getThemeAction(extension);
if (action) {
action.extension = extension;
try {
return action.run({ showCurrentTheme: true, ignoreFocusLost: true });
} finally {
action.dispose();
}
}
}
}
}
private async getThemeAction(extension: IExtension): Promise<ExtensionAction | undefined> {
const colorThemes = await this.workbenchThemeService.getColorThemes();
if (colorThemes.some(theme => isThemeFromExtension(theme, extension))) {
return this.instantiationService.createInstance(SetColorThemeAction);
}
const fileIconThemes = await this.workbenchThemeService.getFileIconThemes();
if (fileIconThemes.some(theme => isThemeFromExtension(theme, extension))) {
return this.instantiationService.createInstance(SetFileIconThemeAction);
}
const productIconThemes = await this.workbenchThemeService.getProductIconThemes();
if (productIconThemes.some(theme => isThemeFromExtension(theme, extension))) {
return this.instantiationService.createInstance(SetProductIconThemeAction);
}
return undefined;
}
private async install(extension: IExtension): Promise<IExtension | undefined> {
try {
return await this.extensionsWorkbenchService.install(extension, this.options);
} catch (error) {
await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, this.options, extension.latestVersion, InstallOperation.Install, error).run();
return undefined;
}
}
private async getRunningExtension(extension: ILocalExtension): Promise<IExtensionDescription | null> {
const runningExtension = await this.runtimeExtensionService.getExtension(extension.identifier.id);
if (runningExtension) {
return runningExtension;
}
if (this.runtimeExtensionService.canAddExtension(toExtensionDescription(extension))) {
return new Promise<IExtensionDescription | null>((c, e) => {
const disposable = this.runtimeExtensionService.onDidChangeExtensions(async () => {
const runningExtension = await this.runtimeExtensionService.getExtension(extension.identifier.id);
if (runningExtension) {
disposable.dispose();
c(runningExtension);
}
});
});
}
return null;
}
protected updateLabel(): void {
this.label = this.getLabel();
}
getLabel(primary?: boolean): string {
if (this.extension?.isWorkspaceScoped && this.extension.resourceExtension && this.contextService.isInsideWorkspace(this.extension.resourceExtension.location)) {
return localize('install workspace version', "Install Workspace Extension");
}
/* install pre-release version */
if (this.options.installPreReleaseVersion && this.extension?.hasPreReleaseVersion) {
return primary ? localize('install pre-release', "Install Pre-Release") : localize('install pre-release version', "Install Pre-Release Version");
}
/* install released version that has a pre release version */
if (this.extension?.hasPreReleaseVersion) {
return primary ? localize('install', "Install") : localize('install release version', "Install Release Version");
}
return localize('install', "Install");
}
}
export class InstallDropdownAction extends ButtonWithDropDownExtensionAction {
set manifest(manifest: IExtensionManifest | null) {
this.extensionActions.forEach(a => (<InstallAction>a).manifest = manifest);
this.update();
}
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService,
) {
super(`extensions.installActions`, InstallAction.CLASS, [
[
instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionManagementService.preferPreReleases }),
instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionManagementService.preferPreReleases }),
]
]);
}
protected override getLabel(action: InstallAction): string {
return action.getLabel(true);
}
}
export class InstallingLabelAction extends ExtensionAction {
private static readonly LABEL = localize('installing', "Installing");
private static readonly CLASS = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`;
constructor() {
super('extension.installing', InstallingLabelAction.LABEL, InstallingLabelAction.CLASS, false);
}
update(): void {
this.class = `${InstallingLabelAction.CLASS}${this.extension && this.extension.state === ExtensionState.Installing ? '' : ' hide'}`;
}
}
export abstract class InstallInOtherServerAction extends ExtensionAction {
protected static readonly INSTALL_LABEL = localize('install', "Install");
protected static readonly INSTALLING_LABEL = localize('installing', "Installing");
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install-other-server`;
private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install-other-server installing`;
updateWhenCounterExtensionChanges: boolean = true;
constructor(
id: string,
private readonly server: IExtensionManagementServer | null,
private readonly canInstallAnyWhere: boolean,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) {
super(id, InstallInOtherServerAction.INSTALL_LABEL, InstallInOtherServerAction.Class, false);
this.update();
}
update(): void {
this.enabled = false;
this.class = InstallInOtherServerAction.Class;
if (this.canInstall()) {
const extensionInOtherServer = this.extensionsWorkbenchService.installed.filter(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server === this.server)[0];
if (extensionInOtherServer) {
// Getting installed in other server
if (extensionInOtherServer.state === ExtensionState.Installing && !extensionInOtherServer.local) {
this.enabled = true;
this.label = InstallInOtherServerAction.INSTALLING_LABEL;
this.class = InstallInOtherServerAction.InstallingClass;
}
} else {
// Not installed in other server
this.enabled = true;
this.label = this.getInstallLabel();
}
}
}
protected canInstall(): boolean {
// Disable if extension is not installed or not an user extension
if (
!this.extension
|| !this.server
|| !this.extension.local
|| this.extension.state !== ExtensionState.Installed
|| this.extension.type !== ExtensionType.User
|| this.extension.enablementState === EnablementState.DisabledByEnvironment || this.extension.enablementState === EnablementState.DisabledByTrustRequirement || this.extension.enablementState === EnablementState.DisabledByVirtualWorkspace
) {
return false;
}
if (isLanguagePackExtension(this.extension.local.manifest)) {
return true;
}
// Prefers to run on UI
if (this.server === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local.manifest)) {
return true;
}
// Prefers to run on Workspace
if (this.server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local.manifest)) {
return true;
}
// Prefers to run on Web
if (this.server === this.extensionManagementServerService.webExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWeb(this.extension.local.manifest)) {
return true;
}
if (this.canInstallAnyWhere) {
// Can run on UI
if (this.server === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.canExecuteOnUI(this.extension.local.manifest)) {
return true;
}
// Can run on Workspace
if (this.server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.canExecuteOnWorkspace(this.extension.local.manifest)) {
return true;
}
}
return false;
}
override async run(): Promise<void> {
if (!this.extension?.local) {
return;
}
if (!this.extension?.server) {
return;
}
if (!this.server) {
return;
}
this.extensionsWorkbenchService.open(this.extension);
alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName));
return this.extensionsWorkbenchService.installInServer(this.extension, this.server);
}
protected abstract getInstallLabel(): string;
}
export class RemoteInstallAction extends InstallInOtherServerAction {
constructor(
canInstallAnyWhere: boolean,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) {
super(`extensions.remoteinstall`, extensionManagementServerService.remoteExtensionManagementServer, canInstallAnyWhere, extensionsWorkbenchService, extensionManagementServerService, extensionManifestPropertiesService);
}
protected getInstallLabel(): string {
return this.extensionManagementServerService.remoteExtensionManagementServer
? localize({ key: 'install in remote', comment: ['This is the name of the action to install an extension in remote server. Placeholder is for the name of remote server.'] }, "Install in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label)
: InstallInOtherServerAction.INSTALL_LABEL;
}
}
export class LocalInstallAction extends InstallInOtherServerAction {
constructor(
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) {
super(`extensions.localinstall`, extensionManagementServerService.localExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, extensionManifestPropertiesService);
}
protected getInstallLabel(): string {
return localize('install locally', "Install Locally");
}
}
export class WebInstallAction extends InstallInOtherServerAction {
constructor(
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) {
super(`extensions.webInstall`, extensionManagementServerService.webExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, extensionManifestPropertiesService);
}
protected getInstallLabel(): string {
return localize('install browser', "Install in Browser");
}
}
export class UninstallAction extends ExtensionAction {
static readonly UninstallLabel = localize('uninstallAction', "Uninstall");
private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling");
static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`;
private static readonly UnInstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall uninstalling`;
constructor(
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
@IDialogService private readonly dialogService: IDialogService
) {
super('extensions.uninstall', UninstallAction.UninstallLabel, UninstallAction.UninstallClass, false);
this.update();
}
update(): void {
if (!this.extension) {
this.enabled = false;
return;
}
const state = this.extension.state;
if (state === ExtensionState.Uninstalling) {
this.label = UninstallAction.UninstallingLabel;
this.class = UninstallAction.UnInstallingClass;
this.enabled = false;
return;
}
this.label = this.extension.local?.isApplicationScoped && this.userDataProfilesService.profiles.length > 1 ? localize('uninstallAll', "Uninstall (All Profiles)") : UninstallAction.UninstallLabel;
this.class = UninstallAction.UninstallClass;
this.tooltip = UninstallAction.UninstallLabel;
if (state !== ExtensionState.Installed) {
this.enabled = false;
return;
}
if (this.extension.isBuiltin) {
this.enabled = false;
return;
}
this.enabled = true;
}
override async run(): Promise<any> {
if (!this.extension) {
return;
}
alert(localize('uninstallExtensionStart', "Uninstalling extension {0} started.", this.extension.displayName));
try {
await this.extensionsWorkbenchService.uninstall(this.extension);
alert(localize('uninstallExtensionComplete', "Please reload Visual Studio Code to complete the uninstallation of the extension {0}.", this.extension.displayName));
} catch (error) {
if (!isCancellationError(error)) {
this.dialogService.error(getErrorMessage(error));
}
}
}
}
export class UpdateAction extends ExtensionAction {
private static readonly EnabledClass = `${this.LABEL_ACTION_CLASS} update`;
private static readonly DisabledClass = `${this.EnabledClass} disabled`;
private readonly updateThrottler = this._register(new Throttler());
constructor(
private readonly verbose: boolean,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IDialogService private readonly dialogService: IDialogService,
@IOpenerService private readonly openerService: IOpenerService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super(`extensions.update`, localize('update', "Update"), UpdateAction.DisabledClass, false);
this.update();
}
update(): void {
this.updateThrottler.queue(() => this.computeAndUpdateEnablement());
if (this.extension) {
this.label = this.verbose ? localize('update to', "Update to v{0}", this.extension.latestVersion) : localize('update', "Update");
}
}
private async computeAndUpdateEnablement(): Promise<void> {
this.enabled = false;
this.class = UpdateAction.DisabledClass;
if (!this.extension) {
return;
}
if (this.extension.deprecationInfo) {
return;
}
const canInstall = await this.extensionsWorkbenchService.canInstall(this.extension);
const isInstalled = this.extension.state === ExtensionState.Installed;
this.enabled = canInstall === true && isInstalled && this.extension.outdated;
this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass;
}
override async run(): Promise<any> {
if (!this.extension) {
return;
}
const consent = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension);
if (consent) {
const { result } = await this.dialogService.prompt<'update' | 'review' | 'cancel'>({
type: 'warning',
title: localize('updateExtensionConsentTitle', "Update {0} Extension", this.extension.displayName),
message: localize('updateExtensionConsent', "{0}\n\nWould you like to update the extension?", consent),
buttons: [{
label: localize('update', "Update"),
run: () => 'update'
}, {
label: localize('review', "Review"),
run: () => 'review'
}, {
label: localize('cancel', "Cancel"),
run: () => 'cancel'
}]
});
if (result === 'cancel') {
return;
}
if (result === 'review') {
if (this.extension.hasChangelog()) {
return this.extensionsWorkbenchService.open(this.extension, { tab: ExtensionEditorTab.Changelog });
}
if (this.extension.repository) {
return this.openerService.open(this.extension.repository);
}
return this.extensionsWorkbenchService.open(this.extension);
}
}
const installOptions: InstallOptions = {};
if (this.extension.local?.source === 'vsix' && this.extension.local.pinned) {
installOptions.pinned = false;
}
if (this.extension.local?.preRelease) {
installOptions.installPreReleaseVersion = true;
}
try {
alert(localize('updateExtensionStart', "Updating extension {0} to version {1} started.", this.extension.displayName, this.extension.latestVersion));
await this.extensionsWorkbenchService.install(this.extension, installOptions);
alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", this.extension.displayName, this.extension.latestVersion));
} catch (err) {
this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, installOptions, this.extension.latestVersion, InstallOperation.Update, err).run();
}
}
}
export class ToggleAutoUpdateForExtensionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.toggleAutoUpdateForExtension';
static readonly LABEL = localize2('enableAutoUpdateLabel', "Auto Update");
private static readonly EnabledClass = `${ExtensionAction.EXTENSION_ACTION_CLASS} auto-update`;
private static readonly DisabledClass = `${this.EnabledClass} hide`;
constructor(
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(ToggleAutoUpdateForExtensionAction.ID, ToggleAutoUpdateForExtensionAction.LABEL.value, ToggleAutoUpdateForExtensionAction.DisabledClass);
this._register(configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
this.update();
}
}));
this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(e => this.update()));
this.update();
}
override update() {
this.enabled = false;
this.class = ToggleAutoUpdateForExtensionAction.DisabledClass;
if (!this.extension) {
return;
}
if (this.extension.isBuiltin) {
return;
}
if (this.extension.deprecationInfo?.disallowInstall) {
return;
}
const extension = this.extension.local ?? this.extension.gallery;
if (extension && this.allowedExtensionsService.isAllowed(extension) !== true) {
return;
}
if (this.extensionsWorkbenchService.getAutoUpdateValue() === 'on' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) {
return;
}
this.enabled = true;
this.class = ToggleAutoUpdateForExtensionAction.EnabledClass;
this.checked = this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension);
}
override async run(): Promise<any> {
if (!this.extension) {
return;
}
const enableAutoUpdate = !this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension);
await this.extensionsWorkbenchService.updateAutoUpdateEnablementFor(this.extension, enableAutoUpdate);
if (enableAutoUpdate) {
alert(localize('enableAutoUpdate', "Enabled auto updates for", this.extension.displayName));
} else {
alert(localize('disableAutoUpdate', "Disabled auto updates for", this.extension.displayName));
}
}
}
export class ToggleAutoUpdatesForPublisherAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.toggleAutoUpdatesForPublisher';
static readonly LABEL = localize('toggleAutoUpdatesForPublisherLabel', "Auto Update All (From Publisher)");
constructor(
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService
) {
super(ToggleAutoUpdatesForPublisherAction.ID, ToggleAutoUpdatesForPublisherAction.LABEL);
}
override update() { }
override async run(): Promise<any> {
if (!this.extension) {
return;
}
alert(localize('ignoreExtensionUpdatePublisher', "Ignoring updates published by {0}.", this.extension.publisherDisplayName));
const enableAutoUpdate = !this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension.publisher);
await this.extensionsWorkbenchService.updateAutoUpdateEnablementFor(this.extension.publisher, enableAutoUpdate);
if (enableAutoUpdate) {
alert(localize('enableAutoUpdate', "Enabled auto updates for", this.extension.displayName));
} else {
alert(localize('disableAutoUpdate', "Disabled auto updates for", this.extension.displayName));
}
}
}
export class MigrateDeprecatedExtensionAction extends ExtensionAction {
private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} migrate`;
private static readonly DisabledClass = `${this.EnabledClass} disabled`;
constructor(
private readonly small: boolean,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService
) {
super('extensionsAction.migrateDeprecatedExtension', localize('migrateExtension', "Migrate"), MigrateDeprecatedExtensionAction.DisabledClass, false);
this.update();
}
update(): void {
this.enabled = false;
this.class = MigrateDeprecatedExtensionAction.DisabledClass;
if (!this.extension?.local) {
return;
}
if (this.extension.state !== ExtensionState.Installed) {
return;
}
if (!this.extension.deprecationInfo?.extension) {
return;
}
const id = this.extension.deprecationInfo.extension.id;
if (this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, { id }))) {
return;
}
this.enabled = true;
this.class = MigrateDeprecatedExtensionAction.EnabledClass;
this.tooltip = localize('migrate to', "Migrate to {0}", this.extension.deprecationInfo.extension.displayName);
this.label = this.small ? localize('migrate', "Migrate") : this.tooltip;
}
override async run(): Promise<any> {
if (!this.extension?.deprecationInfo?.extension) {
return;
}
const local = this.extension.local;
await this.extensionsWorkbenchService.uninstall(this.extension);
const [extension] = await this.extensionsWorkbenchService.getExtensions([{ id: this.extension.deprecationInfo.extension.id, preRelease: this.extension.deprecationInfo?.extension?.preRelease }], CancellationToken.None);
await this.extensionsWorkbenchService.install(extension, { isMachineScoped: local?.isMachineScoped });
}
}
export abstract class DropDownExtensionAction extends ExtensionAction {
constructor(
id: string,
label: string,
cssClass: string,
enabled: boolean,
@IInstantiationService protected instantiationService: IInstantiationService
) {
super(id, label, cssClass, enabled);
}
private _actionViewItem: DropDownExtensionActionViewItem | null = null;
createActionViewItem(options: IActionViewItemOptions): DropDownExtensionActionViewItem {
this._actionViewItem = this.instantiationService.createInstance(DropDownExtensionActionViewItem, this, options);
return this._actionViewItem;
}
public override run(actionGroups: IAction[][]): Promise<any> {
this._actionViewItem?.showMenu(actionGroups);
return Promise.resolve();
}
}
export class DropDownExtensionActionViewItem extends ActionViewItem {
constructor(
action: IAction,
options: IActionViewItemOptions,
@IContextMenuService private readonly contextMenuService: IContextMenuService
) {
super(null, action, { ...options, icon: true, label: true });
}
public showMenu(menuActionGroups: IAction[][]): void {
if (this.element) {
const actions = this.getActions(menuActionGroups);
this.contextMenuService.showContextMenu({
...getWorkbenchMenuMotionContextMenuOptions(this.element),
getActions: () => actions,
actionRunner: this.actionRunner,
onHide: () => disposeIfDisposable(actions)
});
}
}
private getActions(menuActionGroups: IAction[][]): IAction[] {
let actions: IAction[] = [];
for (const menuActions of menuActionGroups) {
actions = [...actions, ...menuActions, new Separator()];
}
return actions.length ? actions.slice(0, actions.length - 1) : actions;
}
}
async function getContextMenuActionsGroups(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): Promise<[string, Array<MenuItemAction | SubmenuItemAction>][]> {
return instantiationService.invokeFunction(async accessor => {
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extensionEnablementService = accessor.get(IWorkbenchExtensionEnablementService);
const menuService = accessor.get(IMenuService);
const extensionRecommendationsService = accessor.get(IExtensionRecommendationsService);
const extensionIgnoredRecommendationsService = accessor.get(IExtensionIgnoredRecommendationsService);
const workbenchThemeService = accessor.get(IWorkbenchThemeService);
const authenticationUsageService = accessor.get(IAuthenticationUsageService);
const allowedExtensionsService = accessor.get(IAllowedExtensionsService);
const cksOverlay: [string, any][] = [];
if (extension) {
cksOverlay.push(['extension', extension.identifier.id]);
cksOverlay.push(['isBuiltinExtension', extension.isBuiltin]);
cksOverlay.push(['isDefaultApplicationScopedExtension', extension.local && isApplicationScopedExtension(extension.local.manifest)]);
cksOverlay.push(['isApplicationScopedExtension', extension.local && extension.local.isApplicationScoped]);
cksOverlay.push(['isWorkspaceScopedExtension', extension.isWorkspaceScoped]);
cksOverlay.push(['isGalleryExtension', !!extension.identifier.uuid]);
if (extension.local) {
cksOverlay.push(['extensionSource', extension.local.source]);
}
cksOverlay.push(['extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration]);
cksOverlay.push(['extensionHasKeybindings', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.keybindings]);
cksOverlay.push(['extensionHasCommands', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes?.commands]);
cksOverlay.push(['isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]]);
cksOverlay.push(['isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace]);
cksOverlay.push(['isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())]);
cksOverlay.push(['isExtensionPinned', extension.pinned]);
cksOverlay.push(['isExtensionEnabled', extensionEnablementService.isEnabledEnablementState(extension.enablementState)]);
switch (extension.state) {
case ExtensionState.Installing:
cksOverlay.push(['extensionStatus', 'installing']);
break;
case ExtensionState.Installed:
cksOverlay.push(['extensionStatus', 'installed']);
break;
case ExtensionState.Uninstalling:
cksOverlay.push(['extensionStatus', 'uninstalling']);
break;
case ExtensionState.Uninstalled:
cksOverlay.push(['extensionStatus', 'uninstalled']);
break;
}
cksOverlay.push(['installedExtensionIsPreReleaseVersion', !!extension.local?.isPreReleaseVersion]);
cksOverlay.push(['installedExtensionIsOptedToPreRelease', !!extension.local?.preRelease]);
cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]);
cksOverlay.push(['galleryExtensionHasPreReleaseVersion', extension.gallery?.hasPreReleaseVersion]);
cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]);
cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]);
cksOverlay.push(['extensionDisallowInstall', extension.isMalicious || extension.deprecationInfo?.disallowInstall]);
cksOverlay.push(['isExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName }) === true]);
cksOverlay.push(['isPreReleaseExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName, prerelease: true }) === true]);
cksOverlay.push(['extensionIsUnsigned', extension.gallery && !extension.gallery.isSigned]);
cksOverlay.push(['extensionIsPrivate', extension.gallery?.private]);
const [colorThemes, fileIconThemes, productIconThemes, extensionUsesAuth] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes(), authenticationUsageService.extensionUsesAuth(extension.identifier.id.toLowerCase())]);
cksOverlay.push(['extensionHasColorThemes', colorThemes.some(theme => isThemeFromExtension(theme, extension))]);
cksOverlay.push(['extensionHasFileIconThemes', fileIconThemes.some(theme => isThemeFromExtension(theme, extension))]);
cksOverlay.push(['extensionHasProductIconThemes', productIconThemes.some(theme => isThemeFromExtension(theme, extension))]);
cksOverlay.push(['extensionHasAccountPreferences', extensionUsesAuth]);
cksOverlay.push(['canSetLanguage', extensionsWorkbenchService.canSetLanguage(extension)]);
cksOverlay.push(['isActiveLanguagePackExtension', extension.gallery && language === getLocale(extension.gallery)]);
}
const actionsGroups = menuService.getMenuActions(MenuId.ExtensionContext, contextKeyService.createOverlay(cksOverlay), { shouldForwardArgs: true });
return actionsGroups;
});
}
function toActions(actionsGroups: [string, Array<MenuItemAction | SubmenuItemAction>][], instantiationService: IInstantiationService): IAction[][] {
const result: IAction[][] = [];
for (const [, actions] of actionsGroups) {
result.push(actions.map(action => {
if (action instanceof SubmenuAction) {
return action;
}
return instantiationService.createInstance(MenuItemExtensionAction, action);
}));
}
return result;
}
export async function getContextMenuActions(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): Promise<IAction[][]> {
const actionsGroups = await getContextMenuActionsGroups(extension, contextKeyService, instantiationService);
return toActions(actionsGroups, instantiationService);
}
export class ManageExtensionAction extends DropDownExtensionAction {
static readonly ID = 'extensions.manage';
private static readonly Class = `${ExtensionAction.ICON_ACTION_CLASS} manage ` + ThemeIcon.asClassName(manageExtensionIcon);
private static readonly HideManageExtensionClass = `${this.Class} hide`;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService private readonly extensionService: IExtensionService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IProductService private readonly productService: IProductService,
) {
super(ManageExtensionAction.ID, '', '', true, instantiationService);
this.tooltip = localize('manage', "Manage");
this.update();
}
async getActionGroups(): Promise<IAction[][]> {
const groups: IAction[][] = [];
const contextMenuActionsGroups = await getContextMenuActionsGroups(this.extension, this.contextKeyService, this.instantiationService);
const themeActions: IAction[] = [], installActions: IAction[] = [], updateActions: IAction[] = [], otherActionGroups: IAction[][] = [];
for (const [group, actions] of contextMenuActionsGroups) {
if (group === INSTALL_ACTIONS_GROUP) {
installActions.push(...toActions([[group, actions]], this.instantiationService)[0]);
} else if (group === UPDATE_ACTIONS_GROUP) {
updateActions.push(...toActions([[group, actions]], this.instantiationService)[0]);
} else if (group === THEME_ACTIONS_GROUP) {
themeActions.push(...toActions([[group, actions]], this.instantiationService)[0]);
} else {
otherActionGroups.push(...toActions([[group, actions]], this.instantiationService));
}
}
if (themeActions.length) {
groups.push(themeActions);
}
const isChatExtension = this.extension && ExtensionIdentifier.equals(this.extension.identifier.id, this.productService.defaultChatAgent?.chatExtensionId);
if (isChatExtension) {
groups.push([
this.instantiationService.createInstance(EnableAIFeaturesGloballyAction),
this.instantiationService.createInstance(EnableAIFeaturesInWorkspaceAction)
]);
groups.push([
this.instantiationService.createInstance(DisableAIFeaturesGloballyAction),
this.instantiationService.createInstance(DisableAIFeaturesInWorkspaceAction)
]);
} else {
groups.push([
this.instantiationService.createInstance(EnableGloballyAction),
this.instantiationService.createInstance(EnableForWorkspaceAction)
]);
groups.push([
this.instantiationService.createInstance(DisableGloballyAction),
this.instantiationService.createInstance(DisableForWorkspaceAction)
]);
}
if (updateActions.length) {
groups.push(updateActions);
}
groups.push([
...(installActions.length ? installActions : []),
this.instantiationService.createInstance(InstallAnotherVersionAction, this.extension, false),
this.instantiationService.createInstance(UninstallAction),
]);
otherActionGroups.forEach(actions => groups.push(actions));
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = this.extension;
}
}));
return groups;
}
override async run(): Promise<any> {
await this.extensionService.whenInstalledExtensionsRegistered();
return super.run(await this.…
reconcileChatExtensionDisablement ran from _computeEnablementState, which is the enablement getter and fires on every lookup, so the cleanup behaved as a standing rule rather than a migration: any persisted disable for the chat extension was dropped on sight while the setting resolved to true, including one written after the fact by another window. It runs from _onDidChangeExtensions when isProfileSwitch is set now, which is the signal the service already carries for the storage scope changing under it, alongside the constructor. The configuration listener stays a trigger. ensureChatExtensionInitialDisabledState settles the setting through an unawaited updateValue, so the constructor pass reads the pre-migration value and only that listener catches the write. A profile-scoped migration marker is deliberately not used. The condition that identifies an entry as legacy is window-dependent: the global entry was written for a user or remote value of true, and a local window cannot see a remote-only value. A marker burned by whichever window starts first would leave that entry in place for the window the setting actually applies to, which is the case this change exists to fix. The test that drove reconciliation through the getter fires the profile change event instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| // Releases that applied `chat.disableAIFeatures` by persisting enablement left an entry that | ||
| // outlives the setting: a global one reaches every window on this machine, a workspace one | ||
| // survives the setting being cleared. Drop whichever the derived state now accounts for. Runs | ||
| // at startup and on profile switch only, so it cannot discard a disable made after either. |
| if (isProfileSwitch) { | ||
| // The disabled extension lists are profile scoped, so the new profile carries its own entries | ||
| this.reconcileChatExtensionDisablement(); | ||
| } |
A persisted disable for the chat extension is redundant wherever chat.disableAIFeatures already resolves to true, and neither combination is reachable any more: the extensions view offers only the AI actions for that extension and all of them write the setting, canChangeEnablement refuses while the setting owns the state, and the one remaining writer is behind the setup-completed guard in ensureChatExtensionInitialDisabledState. Collapsing those states is ordinary correct behavior rather than a one-time cleanup, so gating it behind a completion marker is the wrong shape for it. The markers also failed open. Each was stored before its eviction resolved, so a rejected enableExtension marked the profile done and stranded the entry for good. Without them a failed eviction is simply retried on the next enablement computation. The check stays on _computeEnablementState. That is the hook the service already uses to notice profile switches, because it survives them while the storage scope changes underneath it, and the disabled extension lists are profile scoped. The extension change event cannot serve here: ExtensionsManager.updateExtensions only emits when the profile delta is non-empty, and the builtin chat extension is normally present in both profiles. The test asserting a single eviction goes with the markers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Last commit undoes the previous incorrect review and commit. Now I believe it's ready. |
chat.disableAIFeatures is registered with ConfigurationScope.WINDOW, so it is accepted in remote and workspace settings, but applying it persisted a global disable for the chat extension. That list is machine wide and shared by every window on the client, so a value set only in remote settings also disables chat in local windows, and a local window cannot recover on its own because the remote window re-applies the disable on every start.
Add EnablementState.DisabledByAIFeaturesSetting and compute it in _computeEnablementState alongside the other window-derived reasons, so the extension is disabled exactly where the setting resolves. Nothing is persisted, which makes the state follow whichever settings.json applies to the window. canChangeEnablement reports false while the setting owns the state, and a configuration listener fires onEnablementChanged so the extensions view and the extension host pick the change up.
ChatTeardownContribution no longer changes enablement. It keeps hiding the auxiliary bar and requesting the extension host restart.
Installs that already recorded the global disable would otherwise keep it forever, so the enablement service drops that entry on startup once the setting resolves to true.
Hopefully helps with this that nobody helped me solve:
#248248
I just want to disable copilot for remote connections.