diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 7c11fef5b1e..5fcebbd34c6 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -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((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; diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index dc1c785356e..96259d6807e 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -278,6 +278,7 @@ export interface ICustomDialogOptions { readonly classes?: string[]; readonly icon?: ThemeIcon; readonly disableCloseAction?: boolean; + readonly closeOnLinkClick?: boolean; } export interface ICustomDialogMarkdown { diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 21f62b646c2..039233d3dbe 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -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 { + private async doShow(type: Severity | DialogType | undefined, message: string, buttons?: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInputElement[], customOptions?: ICustomDialogOptions, cancellationToken?: CancellationToken): Promise { 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, diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index f0bbf87c01d..6aecef0b148 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -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) { + 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); } }