Skip to content

Commit 9a5948c

Browse files
authored
Chat setup tweaks (microsoft#235606)
* chat - resolve entitlements after potential upgrade (microsoft#235580) * chat - setup tweaks (microsoft#235600)
1 parent b18e7a7 commit 9a5948c

File tree

4 files changed

+94
-73
lines changed

4 files changed

+94
-73
lines changed

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js';
1111
import { Disposable } from '../../../../../base/common/lifecycle.js';
1212
import { ThemeIcon } from '../../../../../base/common/themables.js';
1313
import { assertType } from '../../../../../base/common/types.js';
14-
import { URI } from '../../../../../base/common/uri.js';
1514
import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
1615
import { localize } from '../../../../../nls.js';
1716
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
18-
import { IProductService } from '../../../../../platform/product/common/productService.js';
1917
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
2018
import { asCssVariable, textLinkForeground } from '../../../../../platform/theme/common/colorRegistry.js';
2119
import { IChatResponseViewModel } from '../../common/chatViewModel.js';
@@ -34,8 +32,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar
3432
element: IChatResponseViewModel,
3533
renderer: MarkdownRenderer,
3634
@IChatWidgetService chatWidgetService: IChatWidgetService,
37-
@ICommandService commandService: ICommandService,
38-
@IProductService productService: IProductService,
35+
@ICommandService commandService: ICommandService
3936
) {
4037
super();
4138

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

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

6258
if (!didAddSecondary) {
6359
didAddSecondary = true;

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

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,16 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js';
1111
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
1212
import { localize, localize2 } from '../../../../nls.js';
1313
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
14-
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
15-
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
14+
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
15+
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
1616
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
1717
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
1818
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
19-
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
2019
import product from '../../../../platform/product/common/product.js';
21-
import { URI } from '../../../../base/common/uri.js';
2220
import { IWorkbenchContribution } from '../../../common/contributions.js';
2321
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
2422
import { ChatContextKeys } from '../common/chatContextKeys.js';
23+
import { ICommandService } from '../../../../platform/commands/common/commands.js';
2524

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

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

105-
class UpgradePlanAction extends Action2 {
106-
constructor() {
107-
super({
108-
id: 'workbench.action.chat.upgradePlan',
109-
title: localize2('managePlan', "Upgrade to Copilot Pro"),
110-
category: localize2('chat.category', 'Chat'),
111-
f1: true,
112-
precondition: ChatContextKeys.enabled,
113-
menu: {
114-
id: MenuId.ChatCommandCenter,
115-
group: 'a_first',
116-
order: 1,
117-
when: ContextKeyExpr.and(
118-
ChatContextKeys.Setup.installed,
119-
ContextKeyExpr.or(
120-
ChatContextKeys.chatQuotaExceeded,
121-
ChatContextKeys.completionsQuotaExceeded
122-
)
123-
)
124-
}
125-
});
126-
}
127-
128-
override async run(accessor: ServicesAccessor): Promise<void> {
129-
const openerService = accessor.get(IOpenerService);
130-
openerService.open(URI.parse(product.defaultChatAgent?.upgradePlanUrl ?? ''));
131-
}
132-
}
133-
134104
class ShowLimitReachedDialogAction extends Action2 {
135105

136106
constructor() {
@@ -141,7 +111,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService
141111
}
142112

143113
override async run(accessor: ServicesAccessor) {
144-
const openerService = accessor.get(IOpenerService);
114+
const commandService = accessor.get(ICommandService);
145115
const dialogService = accessor.get(IDialogService);
146116

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

214-
registerAction2(UpgradePlanAction);
215184
registerAction2(ShowLimitReachedDialogAction);
216185
if (product.quality !== 'stable') {
217186
registerAction2(SimulateCopilotQuotaExceeded);

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

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@ import './media/chatViewSetup.css';
77
import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js';
88
import { Button, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js';
99
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
10-
import { IAction, toAction } from '../../../../base/common/actions.js';
10+
import { IAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';
1111
import { Barrier, timeout } from '../../../../base/common/async.js';
1212
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
1313
import { Codicon } from '../../../../base/common/codicons.js';
1414
import { isCancellationError } from '../../../../base/common/errors.js';
1515
import { Emitter, Event } from '../../../../base/common/event.js';
1616
import { MarkdownString } from '../../../../base/common/htmlContent.js';
1717
import { Lazy } from '../../../../base/common/lazy.js';
18-
import { Disposable } from '../../../../base/common/lifecycle.js';
18+
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
1919
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
2020
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
2121
import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
2222
import { localize, localize2 } from '../../../../nls.js';
2323
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
2424
import { ICommandService } from '../../../../platform/commands/common/commands.js';
2525
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
26-
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
26+
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
2727
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
2828
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
2929
import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js';
@@ -56,6 +56,9 @@ import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatView
5656
import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js';
5757
import { IChatQuotasService } from './chatQuotasService.js';
5858
import { mainWindow } from '../../../../base/browser/window.js';
59+
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
60+
import { URI } from '../../../../base/common/uri.js';
61+
import { IHostService } from '../../../services/host/browser/host.js';
5962

6063
const defaultChat = {
6164
extensionId: product.defaultChatAgent?.extensionId ?? '',
@@ -64,6 +67,7 @@ const defaultChat = {
6467
termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '',
6568
privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '',
6669
skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '',
70+
upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '',
6771
providerId: product.defaultChatAgent?.providerId ?? '',
6872
providerName: product.defaultChatAgent?.providerName ?? '',
6973
providerScopes: product.defaultChatAgent?.providerScopes ?? [[]],
@@ -231,6 +235,60 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
231235
}
232236
}
233237

238+
const windowFocusListener = this._register(new MutableDisposable());
239+
class UpgradePlanAction extends Action2 {
240+
constructor() {
241+
super({
242+
id: 'workbench.action.chat.upgradePlan',
243+
title: localize2('managePlan', "Upgrade to Copilot Pro"),
244+
category: localize2('chat.category', 'Chat'),
245+
f1: true,
246+
precondition: ChatContextKeys.enabled,
247+
menu: {
248+
id: MenuId.ChatCommandCenter,
249+
group: 'a_first',
250+
order: 1,
251+
when: ContextKeyExpr.and(
252+
ChatContextKeys.Setup.installed,
253+
ContextKeyExpr.or(
254+
ChatContextKeys.chatQuotaExceeded,
255+
ChatContextKeys.completionsQuotaExceeded
256+
)
257+
)
258+
}
259+
});
260+
}
261+
262+
override async run(accessor: ServicesAccessor): Promise<void> {
263+
const openerService = accessor.get(IOpenerService);
264+
const telemetryService = accessor.get(ITelemetryService);
265+
const hostService = accessor.get(IHostService);
266+
const commandService = accessor.get(ICommandService);
267+
268+
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: this.desc.id, from: 'chat' });
269+
270+
openerService.open(URI.parse(defaultChat.upgradePlanUrl));
271+
272+
const entitlement = that.context.state.entitlement;
273+
if (entitlement !== ChatEntitlement.Pro) {
274+
// If the user is not yet Pro, we listen to window focus to refresh the token
275+
// when the user has come back to the window assuming the user signed up.
276+
windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService));
277+
}
278+
}
279+
280+
private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise<void> {
281+
if (focus) {
282+
windowFocusListener.clear();
283+
284+
const entitlement = await that.requests.forceResolveEntitlement(undefined);
285+
if (entitlement === ChatEntitlement.Pro) {
286+
commandService.executeCommand('github.copilot.refreshToken'); // ugly, but we need to signal to the extension that entitlements changed
287+
}
288+
}
289+
}
290+
}
291+
234292
async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise<void> {
235293
const location = viewsDescriptorService.getViewLocationById(ChatViewId);
236294

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

247305
registerAction2(ChatSetupTriggerAction);
248306
registerAction2(ChatSetupHideAction);
307+
registerAction2(UpgradePlanAction);
249308
}
250309
}
251310

@@ -507,7 +566,15 @@ class ChatSetupRequests extends Disposable {
507566
}
508567
}
509568

510-
async forceResolveEntitlement(session: AuthenticationSession): Promise<ChatEntitlement | undefined> {
569+
async forceResolveEntitlement(session: AuthenticationSession | undefined): Promise<ChatEntitlement | undefined> {
570+
if (!session) {
571+
session = await this.findMatchingProviderSession(CancellationToken.None);
572+
}
573+
574+
if (!session) {
575+
return undefined;
576+
}
577+
511578
return this.resolveEntitlement(session, CancellationToken.None);
512579
}
513580

@@ -798,23 +865,18 @@ class ChatSetupWelcomeContent extends Disposable {
798865
}
799866

800867
// Limited SKU
801-
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);
868+
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);
802869
const limitedSkuHeaderContainer = this.element.appendChild($('p'));
803870
limitedSkuHeaderContainer.appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true, supportThemeIcons: true }))).element);
804871

805-
// Terms
806-
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);
807-
const termsContainer = this.element.appendChild($('p'));
808-
termsContainer.classList.add('terms-container');
809-
termsContainer.appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element);
810-
811872
// Setup Button
812873
const actions: IAction[] = [];
813874
if (this.context.state.installed) {
814875
actions.push(toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signIn') }));
815876
actions.push(toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signInGHE') }));
816877
}
817878
const buttonContainer = this.element.appendChild($('p'));
879+
buttonContainer.classList.add('button-container');
818880
const button = this._register(actions.length === 0 ? new Button(buttonContainer, {
819881
supportIcons: true,
820882
...defaultButtonStyles
@@ -827,6 +889,10 @@ class ChatSetupWelcomeContent extends Disposable {
827889
}));
828890
this._register(button.onDidClick(() => this.controller.setup()));
829891

892+
// Terms
893+
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);
894+
this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element);
895+
830896
// Update based on model state
831897
this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(limitedSkuHeaderContainer, button)));
832898
}
@@ -989,23 +1055,13 @@ class ChatSetupContext extends Disposable {
9891055
this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION);
9901056
}
9911057

992-
let changed = false;
993-
changed = this.updateContextKey(this.signedOutContextKey, this._state.entitlement === ChatEntitlement.Unknown) || changed;
994-
changed = this.updateContextKey(this.canSignUpContextKey, this._state.entitlement === ChatEntitlement.Available) || changed;
995-
changed = this.updateContextKey(this.limitedContextKey, this._state.entitlement === ChatEntitlement.Limited) || changed;
996-
changed = this.updateContextKey(this.triggeredContext, !!this._state.triggered) || changed;
997-
changed = this.updateContextKey(this.installedContext, !!this._state.installed) || changed;
1058+
this.signedOutContextKey.set(this._state.entitlement === ChatEntitlement.Unknown);
1059+
this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available);
1060+
this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited);
1061+
this.triggeredContext.set(!!this._state.triggered);
1062+
this.installedContext.set(!!this._state.installed);
9981063

999-
if (changed) {
1000-
this._onDidChange.fire();
1001-
}
1002-
}
1003-
1004-
private updateContextKey(contextKey: IContextKey<boolean>, value: boolean): boolean {
1005-
const current = contextKey.get();
1006-
contextKey.set(value);
1007-
1008-
return current !== value;
1064+
this._onDidChange.fire();
10091065
}
10101066

10111067
suspend(): void {

src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
background-color: var(--vscode-chat-requestBackground);
1818
}
1919

20-
.terms-container {
21-
padding-top: 5px;
22-
}
23-
2420
.chat-feature-container {
2521
display: flex;
2622
align-items: center;
@@ -38,6 +34,10 @@
3834
vertical-align: bottom;
3935
}
4036

37+
.button-container {
38+
padding-top: 20px;
39+
}
40+
4141
/** Dropdown Button */
4242
.monaco-button-dropdown {
4343
width: 100%;

0 commit comments

Comments
 (0)