mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 15:40:41 -05:00
Support uninstalling extensions from Tools section of Chat Customizations (#323981)
* Support uninstalling extensions from Tools section of Chat Customizations * PR feedback
This commit is contained in:
@@ -8,9 +8,12 @@ import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
|
||||
import { Button } from '../../../../../base/browser/ui/button/button.js';
|
||||
import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
|
||||
import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';
|
||||
import { IListContextMenuEvent, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';
|
||||
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
import { Checkbox, TriStateCheckbox } from '../../../../../base/browser/ui/toggle/toggle.js';
|
||||
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
|
||||
import { IAnchor } from '../../../../../base/browser/ui/contextview/contextview.js';
|
||||
import { Action } from '../../../../../base/common/actions.js';
|
||||
import { Delayer } from '../../../../../base/common/async.js';
|
||||
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
@@ -18,12 +21,13 @@ import { Emitter } from '../../../../../base/common/event.js';
|
||||
import { IMatch, matchesContiguousSubString } from '../../../../../base/common/filters.js';
|
||||
import { KeyCode } from '../../../../../base/common/keyCodes.js';
|
||||
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { autorun, derived, IObservable, IReader, observableValue } from '../../../../../base/common/observable.js';
|
||||
import { autorun, derived, IObservable, IReader, observableSignalFromEvent, observableValue } from '../../../../../base/common/observable.js';
|
||||
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
|
||||
import { IContextViewService } from '../../../../../platform/contextview/browser/contextView.js';
|
||||
import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { WorkbenchList } from '../../../../../platform/list/browser/listService.js';
|
||||
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
|
||||
@@ -167,6 +171,8 @@ export class ToolsListWidget extends Disposable {
|
||||
@ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService,
|
||||
@IAgentHostToolSetEnablementService private readonly _enablementService: IAgentHostToolSetEnablementService,
|
||||
@IContextViewService private readonly _contextViewService: IContextViewService,
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
@@ -298,6 +304,8 @@ export class ToolsListWidget extends Disposable {
|
||||
this._onDidSelectExtension.fire(e.element);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._galleryList.onContextMenu(e => this._onGalleryContextMenu(e)));
|
||||
}
|
||||
|
||||
private _readState(reader: IReader): IToolEnablementState {
|
||||
@@ -325,7 +333,10 @@ export class ToolsListWidget extends Disposable {
|
||||
}
|
||||
|
||||
private _createViewModel(): IObservable<readonly IToolSetViewModel[]> {
|
||||
// Refresh when extensions change so tool sets from an uninstalled extension drop out immediately (their tools linger in the extension host until reload).
|
||||
const extensionsChanged = observableSignalFromEvent(this, this._extensionsWorkbenchService.onChange);
|
||||
return derived(reader => {
|
||||
extensionsChanged.read(reader);
|
||||
const query = this._searchQuery.read(reader).trim();
|
||||
|
||||
const result: IToolSetViewModel[] = [];
|
||||
@@ -344,6 +355,14 @@ export class ToolsListWidget extends Disposable {
|
||||
if (ts.deprecated) {
|
||||
return undefined;
|
||||
}
|
||||
// Hide extension-provided sets whose extension is gone or being removed.
|
||||
if (ts.source.type === 'extension') {
|
||||
const extensionId = ts.source.extensionId;
|
||||
const installed = this._extensionsWorkbenchService.local.find(e => ExtensionIdentifier.equals(e.identifier.id, extensionId));
|
||||
if (!installed || installed.state === ExtensionState.Uninstalling || installed.state === ExtensionState.Uninstalled) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const memberTools = Array.from(ts.getTools(reader));
|
||||
if (memberTools.length === 0) {
|
||||
return undefined;
|
||||
@@ -604,6 +623,17 @@ export class ToolsListWidget extends Disposable {
|
||||
toggleExpand();
|
||||
}));
|
||||
|
||||
// Extension-provided tool sets can be uninstalled via the context menu.
|
||||
this._rowStore.add(DOM.addDisposableListener(row, 'contextmenu', e => {
|
||||
const extension = this._resolveExtensionForToolSet(ts);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
DOM.EventHelper.stop(e, true);
|
||||
const anchor: HTMLElement | StandardMouseEvent = e.button === 2 ? new StandardMouseEvent(DOM.getWindow(row), e) : row;
|
||||
this._showExtensionContextMenu(anchor, extension);
|
||||
}));
|
||||
|
||||
const group = DOM.append(this._treeContainer, $('.tools-list-children'));
|
||||
group.id = `tools-group-${ts.id}`;
|
||||
group.setAttribute('role', 'group');
|
||||
@@ -888,8 +918,56 @@ export class ToolsListWidget extends Disposable {
|
||||
private _currentState(): IToolEnablementState {
|
||||
return this._enablementService.getState(this._sessionType);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the installed, non-builtin extension backing an extension-provided tool set. */
|
||||
private _resolveExtensionForToolSet(ts: IToolSet): IExtension | undefined {
|
||||
if (ts.source.type !== 'extension') {
|
||||
return undefined;
|
||||
}
|
||||
const source = ts.source;
|
||||
const extension = this._extensionsWorkbenchService.local.find(e => ExtensionIdentifier.equals(e.identifier.id, source.extensionId));
|
||||
if (!extension || extension.local?.isBuiltin) {
|
||||
return undefined;
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
private _onGalleryContextMenu(e: IListContextMenuEvent<IExtension>): void {
|
||||
const extension = e.element;
|
||||
if (!extension || extension.state !== ExtensionState.Installed || extension.local?.isBuiltin) {
|
||||
return;
|
||||
}
|
||||
this._showExtensionContextMenu(e.anchor, extension);
|
||||
}
|
||||
|
||||
private _showExtensionContextMenu(anchor: HTMLElement | StandardMouseEvent | IAnchor, extension: IExtension): void {
|
||||
const disposables = new DisposableStore();
|
||||
const uninstallAction = disposables.add(new Action(
|
||||
'toolsList.uninstallExtension',
|
||||
localize('uninstallExtension', "Uninstall Extension"),
|
||||
undefined,
|
||||
true,
|
||||
() => this._uninstallExtension(extension),
|
||||
));
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => [uninstallAction],
|
||||
onHide: () => disposables.dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _uninstallExtension(extension: IExtension): Promise<void> {
|
||||
const result = await this._dialogService.confirm({
|
||||
message: localize('confirmUninstallToolExtension', "Do you want to uninstall the extension '{0}'?", extension.displayName),
|
||||
detail: localize('confirmUninstallToolExtensionDetail', "This extension may contribute more than tools. Uninstalling it removes all of its contributions."),
|
||||
primaryButton: localize('uninstallExtensionBtn', "Uninstall Extension"),
|
||||
type: 'question',
|
||||
});
|
||||
if (result.confirmed) {
|
||||
await this._extensionsWorkbenchService.uninstall(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Copilot CLI's built-in tools, surfaced read-only for reference. Mirrored from the published
|
||||
|
||||
Reference in New Issue
Block a user