Skip to content

Commit 7e00e48

Browse files
authored
disable continue button until user installs the extensions (microsoft#250124)
1 parent 3f8c263 commit 7e00e48

File tree

4 files changed

+31
-27
lines changed

4 files changed

+31
-27
lines changed

src/vs/base/browser/ui/button/button.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface IButtonOptions extends Partial<IButtonStyles> {
3636
readonly supportShortLabel?: boolean;
3737
readonly secondary?: boolean;
3838
readonly hoverDelegate?: IHoverDelegate;
39+
readonly disabled?: boolean;
3940
}
4041

4142
export interface IButtonStyles {
@@ -131,6 +132,7 @@ export class Button extends Disposable implements IButton {
131132
this._element.setAttribute('aria-label', options.ariaLabel);
132133
}
133134
container.appendChild(this._element);
135+
this.enabled = !options.disabled;
134136

135137
this._register(Gesture.addTarget(this._element));
136138

src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface IChatConfirmationButton {
2424
isSecondary?: boolean;
2525
tooltip?: string;
2626
data: any;
27+
disabled?: boolean;
28+
onDidChangeDisablement?: Event<boolean>;
2729
moreActions?: IChatConfirmationButton[];
2830
}
2931

@@ -139,7 +141,7 @@ abstract class BaseChatConfirmationWidget extends Disposable {
139141

140142
this.messageElement = elements.message;
141143
buttons.forEach(buttonData => {
142-
const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip };
144+
const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled };
143145

144146
let button: IButton;
145147
if (buttonData.moreActions) {
@@ -151,7 +153,7 @@ abstract class BaseChatConfirmationWidget extends Disposable {
151153
action.label,
152154
action.label,
153155
undefined,
154-
true,
156+
!action.disabled,
155157
() => {
156158
this._onDidClick.fire(action);
157159
return Promise.resolve();
@@ -165,6 +167,9 @@ abstract class BaseChatConfirmationWidget extends Disposable {
165167
this._register(button);
166168
button.label = buttonData.label;
167169
this._register(button.onDidClick(() => this._onDidClick.fire(buttonData)));
170+
if (buttonData.onDidChangeDisablement) {
171+
this._register(buttonData.onDidChangeDisablement(disabled => button.enabled = !disabled));
172+
}
168173
});
169174
}
170175

src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from '../../../../../../base/browser/dom.js';
7+
import { Emitter } from '../../../../../../base/common/event.js';
78
import { localize } from '../../../../../../nls.js';
89
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
10+
import { IExtensionManagementService } from '../../../../../../platform/extensionManagement/common/extensionManagement.js';
11+
import { areSameExtensions } from '../../../../../../platform/extensionManagement/common/extensionManagementUtil.js';
912
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
1013
import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js';
1114
import { ChatContextKeys } from '../../../common/chatContextKeys.js';
@@ -28,6 +31,7 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo
2831
@IKeybindingService keybindingService: IKeybindingService,
2932
@IContextKeyService contextKeyService: IContextKeyService,
3033
@IChatWidgetService chatWidgetService: IChatWidgetService,
34+
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
3135
@IInstantiationService instantiationService: IInstantiationService,
3236
) {
3337
super(toolInvocation);
@@ -36,8 +40,9 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo
3640
throw new Error('Tool specific data is missing or not of kind extensions');
3741
}
3842

43+
const extensionsContent = toolInvocation.toolSpecificData;
3944
this.domNode = dom.$('');
40-
const chatExtensionsContentPart = this._register(instantiationService.createInstance(ChatExtensionsContentPart, toolInvocation.toolSpecificData));
45+
const chatExtensionsContentPart = this._register(instantiationService.createInstance(ChatExtensionsContentPart, extensionsContent));
4146
this._register(chatExtensionsContentPart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
4247
dom.append(this.domNode, chatExtensionsContentPart.domNode);
4348

@@ -49,12 +54,15 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo
4954
const cancelLabel = localize('cancel', "Cancel");
5055
const cancelKeybinding = keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel();
5156
const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel;
57+
const enableContinueButtonEvent = this._register(new Emitter<boolean>());
5258

5359
const buttons: IChatConfirmationButton[] = [
5460
{
5561
label: continueLabel,
5662
data: true,
57-
tooltip: continueTooltip
63+
tooltip: continueTooltip,
64+
disabled: true,
65+
onDidChangeDisablement: enableContinueButtonEvent.event
5866
},
5967
{
6068
label: cancelLabel,
@@ -82,6 +90,12 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo
8290
ChatContextKeys.Editing.hasToolConfirmation.bindTo(contextKeyService).set(false);
8391
this._onNeedsRerender.fire();
8492
});
93+
const disposable = this._register(extensionManagementService.onInstallExtension(e => {
94+
if (extensionsContent.extensions.some(id => areSameExtensions({ id }, e.identifier))) {
95+
disposable.dispose();
96+
enableContinueButtonEvent.fire(false);
97+
}
98+
}));
8599
}
86100

87101
}

src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationToken } from '../../../../base/common/cancellation.js';
7+
import { MarkdownString } from '../../../../base/common/htmlContent.js';
78
import { localize } from '../../../../nls.js';
89
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
910
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/languageModelToolsService.js';
@@ -16,7 +17,7 @@ export const InstallExtensionsToolData: IToolData = {
1617
toolReferenceName: 'installExtensions',
1718
canBeReferencedInPrompt: true,
1819
displayName: localize('installExtensionsTool.displayName', 'Install Extensions'),
19-
modelDescription: localize('installExtensionsTool.modelDescription', "This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install, and the confirmation message that is shown to the user. The identifier of an extension is '\${ publisher }.\${ name }' for example: 'vscode.csharp'."),
20+
modelDescription: localize('installExtensionsTool.modelDescription', "This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is '\${ publisher }.\${ name }' for example: 'vscode.csharp'."),
2021
userDescription: localize('installExtensionsTool.userDescription', 'Tool for installing extensions'),
2122
source: ToolDataSource.Internal,
2223
inputSchema: {
@@ -29,30 +30,12 @@ export const InstallExtensionsToolData: IToolData = {
2930
},
3031
description: 'The ids of the extensions to search for. The identifier of an extension is \'\${ publisher }.\${ name }\' for example: \'vscode.csharp\'.',
3132
},
32-
confirmation: {
33-
type: 'object',
34-
description: 'Defines the confirmation dialog shown to the user. This appears after displaying the list of extensions with their install buttons. The title and message should explain the purpose of the suggested extensions and guide the user to press the Install button on each extension they wish to install. The message should clearly instruct the user to click the Continue button after they have finished installing their chosen extensions to proceed. Extensions are not installed automatically - the user must explicitly install them.',
35-
properties: {
36-
title: {
37-
type: 'string',
38-
description: 'The title to display to the user.',
39-
},
40-
message: {
41-
type: 'string',
42-
description: 'The message to display to the user.',
43-
}
44-
}
45-
}
46-
},
33+
}
4734
}
4835
};
4936

5037
type InputParams = {
5138
ids: string[];
52-
confirmation: {
53-
title: string;
54-
message: string;
55-
};
5639
};
5740

5841
export class InstallExtensionsTool implements IToolImpl {
@@ -64,8 +47,8 @@ export class InstallExtensionsTool implements IToolImpl {
6447
async prepareToolInvocation(parameters: InputParams, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
6548
return {
6649
confirmationMessages: {
67-
title: parameters.confirmation?.title ?? localize('installExtensionsTool.confirmationTitle', 'Install Extensions'),
68-
message: parameters.confirmation?.message ?? localize('installExtensionsTool.confirmationMessage', 'These extensions are recommeded for you by Copilot.'),
50+
title: localize('installExtensionsTool.confirmationTitle', 'Install Extensions'),
51+
message: new MarkdownString(localize('installExtensionsTool.confirmationMessage', "Review the suggested extensions and click the **Install** button for each extension you wish to add. Once you have finished installing the selected extensions, click **Continue** to proceed.")),
6952
},
7053
toolSpecificData: {
7154
kind: 'extensions',
@@ -80,7 +63,7 @@ export class InstallExtensionsTool implements IToolImpl {
8063
return {
8164
content: [{
8265
kind: 'text',
83-
value: localize('installExtensionsTool.resultMessage', 'Following extensions are installed: {0}', installed.map(e => e.identifier.id).join(', ')),
66+
value: installed.length ? localize('installExtensionsTool.resultMessage', 'Following extensions are installed: {0}', installed.map(e => e.identifier.id).join(', ')) : localize('installExtensionsTool.noResultMessage', 'No extensions were installed.'),
8467
}]
8568
};
8669
}

0 commit comments

Comments
 (0)