add chat dialog (#235152)

* wip: quota limits hit

* polish copy

* split into 3 actions
This commit is contained in:
João Moreno
2024-12-03 17:33:11 +01:00
committed by GitHub
parent 6019edf7d1
commit 6bbfa33603
4 changed files with 108 additions and 6 deletions
+26 -4
View File
@@ -37,6 +37,7 @@ export interface IDialogOptions {
readonly icon?: ThemeIcon;
readonly buttonDetails?: string[];
readonly disableCloseAction?: boolean;
readonly closeOnLinkClick?: boolean;
readonly disableDefaultAction?: boolean;
readonly buttonStyles: IButtonStyles;
readonly checkboxStyles: ICheckboxStyles;
@@ -203,6 +204,28 @@ export class Dialog extends Disposable {
return new Promise<IDialogResult>((resolve) => {
clearNode(this.buttonsContainer);
const close = () => {
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
return;
};
if (this.options.closeOnLinkClick) {
for (const el of this.messageContainer.getElementsByTagName('a')) {
this._register(addDisposableListener(el, EventType.CLICK, () => {
setTimeout(close); // HACK to ensure the link action is triggered before the dialog is closed
}));
this._register(addDisposableListener(el, EventType.KEY_DOWN, (e: KeyboardEvent) => {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Enter)) {
setTimeout(close); // HACK to ensure the link action is triggered before the dialog is closed
}
}));
}
}
const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer));
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
@@ -337,10 +360,7 @@ export class Dialog extends Disposable {
const evt = new StandardKeyboardEvent(e);
if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape)) {
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
close();
}
}, true));
@@ -450,6 +470,8 @@ export class Dialog extends Disposable {
let color;
switch (this.options.type) {
case 'none':
break;
case 'error':
color = style.errorIconForeground;
break;
@@ -278,6 +278,7 @@ export interface ICustomDialogOptions {
readonly classes?: string[];
readonly icon?: ThemeIcon;
readonly disableCloseAction?: boolean;
readonly closeOnLinkClick?: boolean;
}
export interface ICustomDialogMarkdown {
@@ -20,6 +20,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
export class BrowserDialogHandler extends AbstractDialogHandler {
@@ -105,7 +106,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
}
}
private async doShow(type: Severity | DialogType | undefined, message: string, buttons?: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInputElement[], customOptions?: ICustomDialogOptions): Promise<IDialogResult> {
private async doShow(type: Severity | DialogType | undefined, message: string, buttons?: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInputElement[], customOptions?: ICustomDialogOptions, cancellationToken?: CancellationToken): Promise<IDialogResult> {
const dialogDisposables = new DisposableStore();
const renderBody = customOptions ? (parent: HTMLElement) => {
@@ -137,6 +138,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
renderBody,
icon: customOptions?.icon,
disableCloseAction: customOptions?.disableCloseAction,
closeOnLinkClick: customOptions?.closeOnLinkClick,
buttonDetails: customOptions?.buttonDetails,
checkboxLabel: checkbox?.label,
checkboxChecked: checkbox?.checked,
@@ -19,7 +19,7 @@ import { IRequestContext } from '../../../../base/parts/request/common/request.j
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
import { localize, localize2 } from '../../../../nls.js';
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
@@ -67,6 +67,7 @@ const defaultChat = {
providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],
entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '',
entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '',
managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '',
};
enum ChatEntitlement {
@@ -239,8 +240,84 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution
}
}
const outOfFreeChatResponses = localize('out of free chat responses', "You've run out of free chat responses, but free code completions are still available as part of the Copilot Free plan.");
const outOfCompletions = localize('out of completions', "You've run out of free code completions, but free chat responses are still available as part of the Copilot Free plan.");
const outOfLimits = localize('out of limits', "You've reached the limits of the Copilot Free plan.");
const limitReset = localize('limit reset', "Your limits will reset on {0}.", 'January 13, 2025 at 3:35 PM');
const upgradeToPro = localize('upgradeToPro', "Here's what you can expect when upgrading to Copilot Pro:\n- Unlimited code completions\n- Unlimited chat interactions\n- 30 day free trial");
abstract class AbstractShowLimitReachedDialogAction extends Action2 {
constructor(private readonly message: string, desc: Readonly<IAction2Options>) {
super(desc);
}
override async run(accessor: ServicesAccessor, ...args: any[]) {
const commandService = accessor.get(ICommandService);
await accessor.get(IDialogService).prompt({
type: 'none',
message: localize('limit reached', "Copilot Free"),
cancelButton: {
label: localize('dismiss', "Dismiss"),
run: () => { /* noop */ }
},
buttons: [
{
label: localize('managePlan', "Upgrade to Copilot Pro"),
run: () => commandService.executeCommand('workbench.action.chat.managePlan')
},
],
custom: {
closeOnLinkClick: true,
icon: Codicon.copilot,
markdownDetails: [
{ markdown: new MarkdownString(`${this.message} ${limitReset}`, true) },
{ markdown: new MarkdownString(upgradeToPro, true) }
]
}
});
}
}
class ShowOutOfFreeChatResponsesDialogAction extends AbstractShowLimitReachedDialogAction {
constructor() {
super(outOfFreeChatResponses, {
id: 'workbench.action.chat.showOutOfFreeChatResponsesDialog',
title: localize2('showLimitReachedDialog', "Show Out of Free Chat Responses Dialog"),
// f1: true,
category: CHAT_CATEGORY
});
}
}
class ShowOutOfCompletionsDialogAction extends AbstractShowLimitReachedDialogAction {
constructor() {
super(outOfCompletions, {
id: 'workbench.action.chat.showOutOfCompletions',
title: localize2('showOutOfCompletions', "Show Out of Completions Dialog"),
// f1: true,
category: CHAT_CATEGORY
});
}
}
class ShowOutOfLimitsDialogAction extends AbstractShowLimitReachedDialogAction {
constructor() {
super(outOfLimits, {
id: 'workbench.action.chat.showOutOfLimits',
title: localize2('showOutOfLimits', "Show Out of Limits Dialog"),
// f1: true,
category: CHAT_CATEGORY
});
}
}
registerAction2(ChatSetupTriggerAction);
registerAction2(ChatSetupHideAction);
registerAction2(ShowOutOfFreeChatResponsesDialogAction);
registerAction2(ShowOutOfCompletionsDialogAction);
registerAction2(ShowOutOfLimitsDialogAction);
}
}