Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { assertType } from '../../../../../base/common/types.js';
import { URI } from '../../../../../base/common/uri.js';
import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
import { localize } from '../../../../../nls.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
import { asCssVariable, textLinkForeground } from '../../../../../platform/theme/common/colorRegistry.js';
import { IChatResponseViewModel } from '../../common/chatViewModel.js';
Expand All @@ -34,8 +32,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar
element: IChatResponseViewModel,
renderer: MarkdownRenderer,
@IChatWidgetService chatWidgetService: IChatWidgetService,
@ICommandService commandService: ICommandService,
@IProductService productService: IProductService,
@ICommandService commandService: ICommandService
) {
super();

Expand All @@ -56,8 +53,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar

let didAddSecondary = false;
this._register(button1.onDidClick(async () => {
const url = productService.defaultChatAgent?.upgradePlanUrl;
await commandService.executeCommand('vscode.open', url ? URI.parse(url) : undefined);
await commandService.executeCommand('workbench.action.chat.upgradePlan');

if (!didAddSecondary) {
didAddSecondary = true;
Expand Down
41 changes: 5 additions & 36 deletions src/vs/workbench/contrib/chat/browser/chatQuotasService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { localize, localize2 } from '../../../../nls.js';
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import product from '../../../../platform/product/common/product.js';
import { URI } from '../../../../base/common/uri.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';

export const IChatQuotasService = createDecorator<IChatQuotasService>('chatQuotasService');

Expand Down Expand Up @@ -102,35 +101,6 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
private registerActions(): void {
const that = this;

class UpgradePlanAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.upgradePlan',
title: localize2('managePlan', "Upgrade to Copilot Pro"),
category: localize2('chat.category', 'Chat'),
f1: true,
precondition: ChatContextKeys.enabled,
menu: {
id: MenuId.ChatCommandCenter,
group: 'a_first',
order: 1,
when: ContextKeyExpr.and(
ChatContextKeys.Setup.installed,
ContextKeyExpr.or(
ChatContextKeys.chatQuotaExceeded,
ChatContextKeys.completionsQuotaExceeded
)
)
}
});
}

override async run(accessor: ServicesAccessor): Promise<void> {
const openerService = accessor.get(IOpenerService);
openerService.open(URI.parse(product.defaultChatAgent?.upgradePlanUrl ?? ''));
}
}

class ShowLimitReachedDialogAction extends Action2 {

constructor() {
Expand All @@ -141,7 +111,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
}

override async run(accessor: ServicesAccessor) {
const openerService = accessor.get(IOpenerService);
const commandService = accessor.get(ICommandService);
const dialogService = accessor.get(IDialogService);

const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });
Expand All @@ -168,7 +138,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
buttons: [
{
label: localize('managePlan', "Upgrade to Copilot Pro"),
run: () => { openerService.open(URI.parse(product.defaultChatAgent?.upgradePlanUrl ?? '')); }
run: () => commandService.executeCommand('workbench.action.chat.upgradePlan')
},
],
custom: {
Expand Down Expand Up @@ -211,7 +181,6 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
}
}

registerAction2(UpgradePlanAction);
registerAction2(ShowLimitReachedDialogAction);
if (product.quality !== 'stable') {
registerAction2(SimulateCopilotQuotaExceeded);
Expand Down
110 changes: 83 additions & 27 deletions src/vs/workbench/contrib/chat/browser/chatSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ import './media/chatViewSetup.css';
import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js';
import { Button, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js';
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { IAction, toAction } from '../../../../base/common/actions.js';
import { IAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';
import { Barrier, timeout } from '../../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { isCancellationError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Lazy } from '../../../../base/common/lazy.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
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 { 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';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js';
Expand Down Expand Up @@ -56,6 +56,9 @@ import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatView
import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js';
import { IChatQuotasService } from './chatQuotasService.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { URI } from '../../../../base/common/uri.js';
import { IHostService } from '../../../services/host/browser/host.js';

const defaultChat = {
extensionId: product.defaultChatAgent?.extensionId ?? '',
Expand All @@ -64,6 +67,7 @@ const defaultChat = {
termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '',
privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '',
skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '',
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
providerId: product.defaultChatAgent?.providerId ?? '',
providerName: product.defaultChatAgent?.providerName ?? '',
providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],
Expand Down Expand Up @@ -231,6 +235,60 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
}
}

const windowFocusListener = this._register(new MutableDisposable());
class UpgradePlanAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.upgradePlan',
title: localize2('managePlan', "Upgrade to Copilot Pro"),
category: localize2('chat.category', 'Chat'),
f1: true,
precondition: ChatContextKeys.enabled,
menu: {
id: MenuId.ChatCommandCenter,
group: 'a_first',
order: 1,
when: ContextKeyExpr.and(
ChatContextKeys.Setup.installed,
ContextKeyExpr.or(
ChatContextKeys.chatQuotaExceeded,
ChatContextKeys.completionsQuotaExceeded
)
)
}
});
}

override async run(accessor: ServicesAccessor): Promise<void> {
const openerService = accessor.get(IOpenerService);
const telemetryService = accessor.get(ITelemetryService);
const hostService = accessor.get(IHostService);
const commandService = accessor.get(ICommandService);

telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: this.desc.id, from: 'chat' });

openerService.open(URI.parse(defaultChat.upgradePlanUrl));

const entitlement = that.context.state.entitlement;
if (entitlement !== ChatEntitlement.Pro) {
// If the user is not yet Pro, we listen to window focus to refresh the token
// when the user has come back to the window assuming the user signed up.
windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService));
}
}

private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise<void> {
if (focus) {
windowFocusListener.clear();

const entitlement = await that.requests.forceResolveEntitlement(undefined);
if (entitlement === ChatEntitlement.Pro) {
commandService.executeCommand('github.copilot.refreshToken'); // ugly, but we need to signal to the extension that entitlements changed
}
}
}
}

async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise<void> {
const location = viewsDescriptorService.getViewLocationById(ChatViewId);

Expand All @@ -246,6 +304,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr

registerAction2(ChatSetupTriggerAction);
registerAction2(ChatSetupHideAction);
registerAction2(UpgradePlanAction);
}
}

Expand Down Expand Up @@ -507,7 +566,15 @@ class ChatSetupRequests extends Disposable {
}
}

async forceResolveEntitlement(session: AuthenticationSession): Promise<ChatEntitlement | undefined> {
async forceResolveEntitlement(session: AuthenticationSession | undefined): Promise<ChatEntitlement | undefined> {
if (!session) {
session = await this.findMatchingProviderSession(CancellationToken.None);
}

if (!session) {
return undefined;
}

return this.resolveEntitlement(session, CancellationToken.None);
}

Expand Down Expand Up @@ -798,23 +865,18 @@ class ChatSetupWelcomeContent extends Disposable {
}

// Limited SKU
const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}) with 50 chat messages and 2000 code completions per month.", defaultChat.skusDocumentationUrl);
const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}) with 2,000 code completions and 50 chat messages per month.", defaultChat.skusDocumentationUrl);
const limitedSkuHeaderContainer = this.element.appendChild($('p'));
limitedSkuHeaderContainer.appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true, supportThemeIcons: true }))).element);

// Terms
const terms = localize({ key: 'termsLabel', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to our [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl);
const termsContainer = this.element.appendChild($('p'));
termsContainer.classList.add('terms-container');
termsContainer.appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element);

// Setup Button
const actions: IAction[] = [];
if (this.context.state.installed) {
actions.push(toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signIn') }));
actions.push(toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signInGHE') }));
}
const buttonContainer = this.element.appendChild($('p'));
buttonContainer.classList.add('button-container');
const button = this._register(actions.length === 0 ? new Button(buttonContainer, {
supportIcons: true,
...defaultButtonStyles
Expand All @@ -827,6 +889,10 @@ class ChatSetupWelcomeContent extends Disposable {
}));
this._register(button.onDidClick(() => this.controller.setup()));

// Terms
const terms = localize({ key: 'termsLabel', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to the [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl);
this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element);

// Update based on model state
this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(limitedSkuHeaderContainer, button)));
}
Expand Down Expand Up @@ -989,23 +1055,13 @@ class ChatSetupContext extends Disposable {
this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION);
}

let changed = false;
changed = this.updateContextKey(this.signedOutContextKey, this._state.entitlement === ChatEntitlement.Unknown) || changed;
changed = this.updateContextKey(this.canSignUpContextKey, this._state.entitlement === ChatEntitlement.Available) || changed;
changed = this.updateContextKey(this.limitedContextKey, this._state.entitlement === ChatEntitlement.Limited) || changed;
changed = this.updateContextKey(this.triggeredContext, !!this._state.triggered) || changed;
changed = this.updateContextKey(this.installedContext, !!this._state.installed) || changed;
this.signedOutContextKey.set(this._state.entitlement === ChatEntitlement.Unknown);
this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available);
this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited);
this.triggeredContext.set(!!this._state.triggered);
this.installedContext.set(!!this._state.installed);

if (changed) {
this._onDidChange.fire();
}
}

private updateContextKey(contextKey: IContextKey<boolean>, value: boolean): boolean {
const current = contextKey.get();
contextKey.set(value);

return current !== value;
this._onDidChange.fire();
}

suspend(): void {
Expand Down
8 changes: 4 additions & 4 deletions src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@
background-color: var(--vscode-chat-requestBackground);
}

.terms-container {
padding-top: 5px;
}

.chat-feature-container {
display: flex;
align-items: center;
Expand All @@ -38,6 +34,10 @@
vertical-align: bottom;
}

.button-container {
padding-top: 20px;
}

/** Dropdown Button */
.monaco-button-dropdown {
width: 100%;
Expand Down