Skip to content

Commit 35770a1

Browse files
authored
chat - support an input mode for auth dialog (microsoft#249571)
1 parent d76a8c2 commit 35770a1

File tree

6 files changed

+272
-32
lines changed

6 files changed

+272
-32
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "code-oss-dev",
33
"version": "1.101.0",
4-
"distro": "78645790610c3a0c4bee8b854a43a6cdcaac2223",
4+
"distro": "c0d702f8f75399581ffbd28b76c7eb7d06aee169",
55
"author": {
66
"name": "Microsoft Corporation"
77
},

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface IDialogOptions {
5353
readonly renderBody?: (container: HTMLElement) => void;
5454
readonly renderFooter?: (container: HTMLElement) => void;
5555
readonly icon?: ThemeIcon;
56-
readonly buttonOptions?: Array<undefined | { sublabel?: string; extraClasses?: string[] }>;
56+
readonly buttonOptions?: Array<undefined | { sublabel?: string; styleButton?: (button: IButton) => void }>;
5757
readonly primaryButtonDropdown?: IButtonWithDropdownOptions;
5858
readonly disableCloseAction?: boolean;
5959
readonly disableCloseButton?: boolean;
@@ -277,7 +277,7 @@ export class Dialog extends Disposable {
277277
});
278278
};
279279

280-
// Handle button clicks
280+
// Buttons
281281
buttonMap.forEach((_, index) => {
282282
const primary = buttonMap[index].index === 0;
283283

@@ -304,8 +304,8 @@ export class Dialog extends Disposable {
304304
button = this._register(buttonBar.addButton({ secondary: !primary, ...this.buttonStyles }));
305305
}
306306

307-
if (buttonOptions?.extraClasses) {
308-
button.element.classList.add(...buttonOptions.extraClasses);
307+
if (buttonOptions?.styleButton) {
308+
buttonOptions.styleButton(button);
309309
}
310310

311311
button.label = mnemonicButtonLabel(buttonMap[index].label, true);

src/vs/base/common/product.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ export interface IDefaultChatAgent {
334334
readonly managePlanUrl: string;
335335
readonly manageOverageUrl: string;
336336
readonly upgradePlanUrl: string;
337+
readonly signUpUrl: string;
337338

338339
readonly providerId: string;
339340
readonly providerName: string;

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ configurationRegistry.registerConfiguration({
439439
},
440440
'chat.setup.signInDialogVariant': { // TODO@bpasero remove me eventually
441441
type: 'string',
442-
enum: ['default', 'brand-gh', 'brand-vsc', 'style-glow', 'alt-first'],
442+
enum: ['default', 'modern', 'brand-gh', 'brand-vsc', 'style-glow', 'alt-first', 'input-email', 'account-create'],
443443
description: nls.localize('chat.signInDialogVariant', "Control variations of the sign-in dialog."),
444444
default: 'default',
445445
tags: ['onExp', 'experimental']

src/vs/workbench/contrib/chat/browser/chatSetup.ts

Lines changed: 172 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
import './media/chatSetup.css';
77
import { $ } from '../../../../base/browser/dom.js';
8-
import { Dialog, DialogContentsAlignment } from '../../../../base/browser/ui/dialog/dialog.js';
9-
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';
8+
import { Dialog, DialogContentsAlignment, IDialogInputOptions } from '../../../../base/browser/ui/dialog/dialog.js';
9+
import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';
1010
import { timeout } from '../../../../base/common/async.js';
1111
import { CancellationToken } from '../../../../base/common/cancellation.js';
1212
import { Codicon } from '../../../../base/common/codicons.js';
@@ -68,6 +68,8 @@ import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js';
6868
import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js';
6969
import { coalesce } from '../../../../base/common/arrays.js';
7070
import { ThemeIcon } from '../../../../base/common/themables.js';
71+
import { IButton } from '../../../../base/browser/ui/button/button.js';
72+
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
7173

7274
const defaultChat = {
7375
extensionId: product.defaultChatAgent?.extensionId ?? '',
@@ -77,6 +79,7 @@ const defaultChat = {
7779
publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '',
7880
manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '',
7981
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
82+
signUpUrl: product.defaultChatAgent?.signUpUrl ?? '',
8083
providerName: product.defaultChatAgent?.providerName ?? '',
8184
enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '',
8285
enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '',
@@ -565,7 +568,8 @@ enum ChatSetupStrategy {
565568
Canceled = 0,
566569
DefaultSetup = 1,
567570
SetupWithoutEnterpriseProvider = 2,
568-
SetupWithEnterpriseProvider = 3
571+
SetupWithEnterpriseProvider = 3,
572+
SetupWithAccountCreate = 4
569573
}
570574

571575
type ChatSetupResultValue = boolean /* success */ | undefined /* canceled */;
@@ -582,7 +586,7 @@ class ChatSetup {
582586
let instance = ChatSetup.instance;
583587
if (!instance) {
584588
instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => {
585-
return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IViewsService), accessor.get(IProductService));
589+
return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService), accessor.get(IViewsService), accessor.get(IProductService), accessor.get(IOpenerService), accessor.get(IContextMenuService));
586590
});
587591
}
588592

@@ -604,7 +608,9 @@ class ChatSetup {
604608
@ILogService private readonly logService: ILogService,
605609
@IConfigurationService private readonly configurationService: IConfigurationService,
606610
@IViewsService private readonly viewsService: IViewsService,
607-
@IProductService private readonly productService: IProductService
611+
@IProductService private readonly productService: IProductService,
612+
@IOpenerService private readonly openerService: IOpenerService,
613+
@IContextMenuService private readonly contextMenuService: IContextMenuService
608614
) { }
609615

610616
skipDialog(): void {
@@ -658,6 +664,9 @@ class ChatSetup {
658664
case ChatSetupStrategy.DefaultSetup:
659665
success = await this.controller.value.setup();
660666
break;
667+
case ChatSetupStrategy.SetupWithAccountCreate:
668+
this.openerService.open(URI.parse(defaultChat.signUpUrl));
669+
return this.doRun(options); // open dialog again
661670
case ChatSetupStrategy.Canceled:
662671
this.telemetryService.publicLog2<InstallChatEvent, InstallChatClassification>('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined });
663672
break;
@@ -671,9 +680,16 @@ class ChatSetup {
671680
}
672681

673682
private async showDialog(): Promise<ChatSetupStrategy> {
674-
const disposables = new DisposableStore();
683+
let dialogVariant = this.configurationService.getValue<'default' | 'modern' | 'brand-gh' | 'brand-vsc' | 'style-glow' | 'alt-first' | 'input-email' | 'account-create' | unknown>('chat.setup.signInDialogVariant');
684+
if (this.context.state.entitlement !== ChatEntitlement.Unknown && (dialogVariant === 'input-email' || dialogVariant === 'account-create')) {
685+
dialogVariant = 'default'; // fallback to default for users that are signed in already
686+
}
675687

676-
const dialogVariant = this.configurationService.getValue<'default' | 'brand-gh' | 'brand-vsc' | 'style-glow' | 'alt-first' | unknown>('chat.setup.signInDialogVariant');
688+
if (dialogVariant === 'default') {
689+
return this.showLegacyDialog();
690+
}
691+
692+
const disposables = new DisposableStore();
677693

678694
const buttons = this.getButtons(dialogVariant);
679695

@@ -696,11 +712,16 @@ class ChatSetup {
696712
buttons.map(button => button[0]),
697713
createWorkbenchDialogOptions({
698714
type: 'none',
699-
extraClasses: coalesce(['chat-setup-dialog', dialogVariant === 'style-glow' ? 'chat-setup-glow' : undefined]),
715+
extraClasses: coalesce([
716+
'chat-setup-dialog',
717+
dialogVariant === 'style-glow' ? 'chat-setup-glow' : undefined,
718+
dialogVariant === 'input-email' ? 'chat-setup-input-email' : undefined
719+
]),
700720
detail: ' ', // workaround allowing us to render the message in large
701721
icon,
702722
alignment: DialogContentsAlignment.Vertical,
703723
cancelId: buttons.length - 1,
724+
inputs: this.getInputs(dialogVariant),
704725
disableCloseButton: true,
705726
renderFooter: this.telemetryService.telemetryLevel !== TelemetryLevel.NONE ? footer => footer.appendChild(this.createDialogFooter(disposables)) : undefined,
706727
buttonOptions: buttons.map(button => button[2])
@@ -713,34 +734,84 @@ class ChatSetup {
713734
return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled;
714735
}
715736

716-
private getButtons(variant: 'default' | 'brand-gh' | 'brand-vsc' | 'style-glow' | 'alt-first' | unknown): Array<[string, ChatSetupStrategy, { extraClasses: string[] } | undefined]> {
717-
let buttons: Array<[string, ChatSetupStrategy, { extraClasses: string[] } | undefined]>;
737+
private getInputs(variant: 'input-email' | unknown): IDialogInputOptions[] | undefined {
738+
if (variant !== 'input-email') {
739+
return undefined;
740+
}
741+
742+
return [{ placeholder: localize('emailOrUserIdPlaceholder', "Enter your email or {0} username", defaultChat.providerName) }];
743+
}
744+
745+
private getButtons(variant: 'modern' | 'alt-first' | 'input-email' | 'account-create' | unknown): Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]> {
746+
let buttons: Array<[string, ChatSetupStrategy, { styleButton?: (button: IButton) => void } | undefined]>;
718747

719748
if (this.context.state.entitlement === ChatEntitlement.Unknown) {
720749
const supportAlternateProvider = this.configurationService.getValue('chat.setup.signInWithAlternateProvider') === true && defaultChat.alternativeProviderId;
721750

722-
if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) {
723-
buttons = coalesce([
724-
[localize('continueWithProvider', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, undefined],
725-
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
726-
[localize('signInWithProvider', "Sign in with a {0} account", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, { extraClasses: ['link-button'] }]
727-
]);
728-
} else {
729-
buttons = coalesce([
730-
[localize('continueWithProvider', "Continue with {0}", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined],
731-
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
732-
[localize('signInWithProvider', "Sign in with a {0} account", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, { extraClasses: ['link-button'] }]
733-
]);
734-
}
751+
switch (variant) {
752+
case 'input-email':
753+
buttons = coalesce([
754+
[localize('continueWithEmailOrUserId', "Continue"), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined],
755+
[localize('createAccount', "Create a New Account"), ChatSetupStrategy.SetupWithAccountCreate, {
756+
styleButton: button => {
757+
button.element.classList.add('link-button');
758+
759+
const separator = button.element.parentElement?.appendChild($('.buttons-separator'));
760+
separator?.appendChild($('.buttons-separator-left'));
761+
separator?.appendChild($('.buttons-separator-center', undefined, localize('or', "Or")));
762+
separator?.appendChild($('.buttons-separator-right'));
763+
}
764+
}],
765+
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
766+
[localize('continueWithProvider', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, undefined]
767+
]);
768+
break;
769+
default:
770+
if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) {
771+
buttons = coalesce([
772+
[localize('continueWithProvider', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, undefined],
773+
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
774+
[variant !== 'account-create' ? localize('signInWithProvider', "Sign in with a {0} account", defaultChat.providerName) : localize('continueWithProvider', "Continue with {0}", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, {
775+
styleButton: button => {
776+
if (variant !== 'account-create') {
777+
button.element.classList.add('link-button');
778+
}
779+
}
780+
}]
781+
]);
782+
} else {
783+
buttons = coalesce([
784+
[localize('continueWithProvider', "Continue with {0}", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined],
785+
supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined,
786+
[variant !== 'account-create' ? localize('signInWithProvider', "Sign in with a {0} account", defaultChat.enterpriseProviderName) : localize('continueWithProvider', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, {
787+
styleButton: button => {
788+
if (variant !== 'account-create') {
789+
button.element.classList.add('link-button');
790+
}
791+
}
792+
}]
793+
]);
794+
}
795+
796+
if (supportAlternateProvider && variant === 'alt-first') {
797+
[buttons[0], buttons[1]] = [buttons[1], buttons[0]];
798+
}
735799

736-
if (supportAlternateProvider && variant === 'alt-first') {
737-
[buttons[0], buttons[1]] = [buttons[1], buttons[0]];
800+
if (variant === 'account-create') {
801+
buttons.push([localize('createAccount', "Create a New Account"), ChatSetupStrategy.SetupWithAccountCreate, {
802+
styleButton: button => {
803+
button.element.classList.add('link-button');
804+
}
805+
}]);
806+
}
807+
808+
break;
738809
}
739810
} else {
740811
buttons = [[localize('setupCopilotButton', "Set up Copilot"), ChatSetupStrategy.DefaultSetup, undefined]];
741812
}
742813

743-
buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, { extraClasses: ['link-button', 'skip-button'] }]);
814+
buttons.push([localize('skipForNow', "Skip for now"), ChatSetupStrategy.Canceled, { styleButton: button => button.element.classList.add('link-button', 'skip-button') }]);
744815

745816
return buttons;
746817
}
@@ -778,6 +849,81 @@ class ChatSetup {
778849

779850
return element;
780851
}
852+
853+
private async showLegacyDialog(): Promise<ChatSetupStrategy> {
854+
const disposables = new DisposableStore();
855+
856+
let result: ChatSetupStrategy | undefined = undefined;
857+
858+
const buttons = [this.getLegacyPrimaryButton(), localize('maybeLater', "Maybe Later")];
859+
860+
const dialog = disposables.add(new Dialog(
861+
this.layoutService.activeContainer,
862+
this.getLegacyDialogTitle(),
863+
buttons,
864+
createWorkbenchDialogOptions({
865+
type: 'none',
866+
icon: Codicon.copilotLarge,
867+
cancelId: buttons.length - 1,
868+
renderBody: body => body.appendChild(this.createLegacyDialog(disposables)),
869+
primaryButtonDropdown: {
870+
contextMenuProvider: this.contextMenuService,
871+
addPrimaryActionToDropdown: false,
872+
actions: [
873+
toAction({ id: 'setupWithProvider', label: localize('setupWithProvider', "Sign in with a {0} Account", defaultChat.providerName), run: () => result = ChatSetupStrategy.SetupWithoutEnterpriseProvider }),
874+
toAction({ id: 'setupWithEnterpriseProvider', label: localize('setupWithEnterpriseProvider', "Sign in with a {0} Account", defaultChat.enterpriseProviderName), run: () => result = ChatSetupStrategy.SetupWithEnterpriseProvider }),
875+
]
876+
}
877+
}, this.keybindingService, this.layoutService)
878+
));
879+
880+
const { button } = await dialog.show();
881+
disposables.dispose();
882+
883+
return button === 0 ? result ?? ChatSetupStrategy.DefaultSetup : ChatSetupStrategy.Canceled;
884+
}
885+
886+
private getLegacyPrimaryButton(): string {
887+
if (this.context.state.entitlement === ChatEntitlement.Unknown) {
888+
if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) {
889+
return localize('setupWithProviderShort', "Sign in with {0}", defaultChat.enterpriseProviderName);
890+
}
891+
892+
return localize('signInButton', "Sign in");
893+
}
894+
895+
return localize('useCopilotButton', "Use Copilot");
896+
}
897+
898+
private getLegacyDialogTitle(): string {
899+
if (this.context.state.entitlement === ChatEntitlement.Unknown) {
900+
return this.context.state.registered ? localize('signUp', "Sign in to use Copilot") : localize('signUpFree', "Sign in to use Copilot for free");
901+
}
902+
903+
if (isProUser(this.context.state.entitlement)) {
904+
return localize('copilotProTitle', "Start using Copilot Pro");
905+
}
906+
907+
return this.context.state.registered ? localize('copilotTitle', "Start using Copilot") : localize('copilotFreeTitle', "Start using Copilot for free");
908+
}
909+
910+
private createLegacyDialog(disposables: DisposableStore): HTMLElement {
911+
const element = $('.chat-setup-dialog-legacy');
912+
913+
const markdown = this.instantiationService.createInstance(MarkdownRenderer, {});
914+
915+
// Header
916+
const header = localize({ key: 'headerDialog', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer. Write code faster with completions, fix bugs and build new features across multiple files, and learn about your codebase through chat.", defaultChat.documentationUrl);
917+
element.appendChild($('p.setup-header', undefined, disposables.add(markdown.render(new MarkdownString(header, { isTrusted: true }))).element));
918+
919+
// SKU Settings
920+
if (this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) {
921+
const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "{0} Copilot Free, Pro and Pro+ may show [public code]({1}) suggestions and we may use your data for product improvement. You can change these [settings]({2}) at any time.", defaultChat.providerName, defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl);
922+
element.appendChild($('p.setup-settings', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element));
923+
}
924+
925+
return element;
926+
}
781927
}
782928

783929
export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {

0 commit comments

Comments
 (0)