Skip to content

Commit 0557387

Browse files
authored
chat - improve state handling of setup (microsoft#234095)
1 parent d2d60bc commit 0557387

File tree

3 files changed

+100
-82
lines changed

3 files changed

+100
-82
lines changed

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

Lines changed: 90 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js';
3737
import { IChatViewsWelcomeContributionRegistry, ChatViewsWelcomeExtensions } from './viewsWelcome/chatViewsWelcome.js';
3838
import { MarkdownString } from '../../../../base/common/htmlContent.js';
3939
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
40+
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
4041

4142
const defaultChat = {
4243
extensionId: product.defaultChatAgent?.extensionId ?? '',
@@ -75,11 +76,11 @@ type InstallChatEvent = {
7576

7677
class ChatSetupContribution extends Disposable implements IWorkbenchContribution {
7778

78-
private static readonly CHAT_EXTENSION_INSTALLED_KEY = 'chat.extensionInstalled';
79-
8079
private readonly chatSetupSignedInContextKey = ChatContextKeys.ChatSetup.signedIn.bindTo(this.contextKeyService);
8180
private readonly chatSetupEntitledContextKey = ChatContextKeys.ChatSetup.entitled.bindTo(this.contextKeyService);
8281

82+
private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState);
83+
8384
private resolvedEntitlement: boolean | undefined = undefined;
8485

8586
constructor(
@@ -90,7 +91,6 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution
9091
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
9192
@IExtensionService private readonly extensionService: IExtensionService,
9293
@IRequestService private readonly requestService: IRequestService,
93-
@IStorageService private readonly storageService: IStorageService,
9494
@IInstantiationService private readonly instantiationService: IInstantiationService
9595
) {
9696
super();
@@ -100,82 +100,44 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution
100100
return;
101101
}
102102

103-
this.checkExtensionInstallation(entitlement);
104-
105103
this.registerChatWelcome();
106104

107105
this.registerEntitlementListeners(entitlement);
108106
this.registerAuthListeners(entitlement);
109-
}
110-
111-
private async checkExtensionInstallation(entitlement: IGitHubEntitlement): Promise<void> {
112-
const extensions = await this.extensionManagementService.getInstalled();
113107

114-
const installed = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, entitlement.extensionId));
115-
this.updateExtensionInstalled(installed ? true : false);
108+
this.checkExtensionInstallation(entitlement);
116109
}
117110

118111
private registerChatWelcome(): void {
119112

120-
// Action to hide setup
121-
const chatSetupTrigger = this.instantiationService.createInstance(ChatSetupTrigger);
122-
const disableChatSetupActionId = 'workbench.action.chat.disableSetup';
123-
124-
registerAction2(class extends Action2 {
125-
126-
constructor() {
127-
super({
128-
id: disableChatSetupActionId,
129-
title: localize2('hideChatSetup', "Hide Chat Setup"),
130-
f1: false
131-
});
132-
}
133-
134-
override async run(accessor: ServicesAccessor): Promise<void> {
135-
const viewsDescriptorService = accessor.get(IViewDescriptorService);
136-
const layoutService = accessor.get(IWorkbenchLayoutService);
137-
138-
const location = viewsDescriptorService.getViewLocationById(ChatViewId);
139-
140-
chatSetupTrigger.update(false);
141-
142-
if (location === ViewContainerLocation.AuxiliaryBar) {
143-
const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0);
144-
if (activeContainers.length === 0) {
145-
layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar
146-
}
147-
}
148-
}
149-
});
150-
151113
// Setup: Triggered (signed-out)
152114
Registry.as<IChatViewsWelcomeContributionRegistry>(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({
153115
title: defaultChat.welcomeTitle,
154116
when: ContextKeyExpr.and(
155-
ChatContextKeys.ChatSetup.triggering,
117+
ChatContextKeys.ChatSetup.triggered,
156118
ChatContextKeys.ChatSetup.signedIn.negate(),
157119
ChatContextKeys.ChatSetup.signingIn.negate(),
158120
ChatContextKeys.ChatSetup.installing.negate(),
159121
ChatContextKeys.extensionInvalid.negate(),
160122
ChatContextKeys.panelParticipantRegistered.negate()
161123
)!,
162124
icon: defaultChat.icon,
163-
content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}\n\n[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${disableChatSetupActionId})`, { isTrusted: true }),
125+
content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}\n\n[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, { isTrusted: true }),
164126
});
165127

166128
// Setup: Triggered (signed-in)
167129
Registry.as<IChatViewsWelcomeContributionRegistry>(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({
168130
title: defaultChat.welcomeTitle,
169131
when: ContextKeyExpr.and(
170-
ChatContextKeys.ChatSetup.triggering,
132+
ChatContextKeys.ChatSetup.triggered,
171133
ChatContextKeys.ChatSetup.signedIn,
172134
ChatContextKeys.ChatSetup.signingIn.negate(),
173135
ChatContextKeys.ChatSetup.installing.negate(),
174136
ChatContextKeys.extensionInvalid.negate(),
175137
ChatContextKeys.panelParticipantRegistered.negate()
176138
)!,
177139
icon: defaultChat.icon,
178-
content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}\n\n[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${disableChatSetupActionId})`, { isTrusted: true }),
140+
content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}\n\n[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, { isTrusted: true }),
179141
});
180142

181143
// Setup: Signing-in
@@ -188,7 +150,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution
188150
)!,
189151
icon: defaultChat.icon,
190152
progress: localize('setupChatSigningIn', "Signing in to {0}...", defaultChat.providerName),
191-
content: new MarkdownString(`\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, { isTrusted: true }),
153+
content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}`, { isTrusted: true }),
192154
});
193155

194156
// Setup: Installing
@@ -197,22 +159,22 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution
197159
when: ChatContextKeys.ChatSetup.installing,
198160
icon: defaultChat.icon,
199161
progress: localize('setupChatInstalling', "Setting up Chat for you..."),
200-
content: new MarkdownString(`\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, { isTrusted: true }),
162+
content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}`, { isTrusted: true }),
201163
});
202164
}
203165

204166
private registerEntitlementListeners(entitlement: IGitHubEntitlement): void {
205167
this._register(this.extensionService.onDidChangeExtensions(result => {
206168
for (const extension of result.removed) {
207169
if (ExtensionIdentifier.equals(entitlement.extensionId, extension.identifier)) {
208-
this.updateExtensionInstalled(false);
170+
this.chatSetupState.update({ chatInstalled: false });
209171
break;
210172
}
211173
}
212174

213175
for (const extension of result.added) {
214176
if (ExtensionIdentifier.equals(entitlement.extensionId, extension.identifier)) {
215-
this.updateExtensionInstalled(true);
177+
this.chatSetupState.update({ chatInstalled: true });
216178
break;
217179
}
218180
}
@@ -314,8 +276,59 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution
314276
return this.resolvedEntitlement;
315277
}
316278

317-
private updateExtensionInstalled(isExtensionInstalled: boolean): void {
318-
this.storageService.store(ChatSetupContribution.CHAT_EXTENSION_INSTALLED_KEY, isExtensionInstalled, StorageScope.PROFILE, StorageTarget.MACHINE);
279+
private async checkExtensionInstallation(entitlement: IGitHubEntitlement): Promise<void> {
280+
const extensions = await this.extensionManagementService.getInstalled();
281+
282+
const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, entitlement.extensionId));
283+
this.chatSetupState.update({ chatInstalled });
284+
}
285+
}
286+
287+
class ChatSetupState {
288+
289+
private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered';
290+
private static readonly CHAT_EXTENSION_INSTALLED = 'chat.extensionInstalled';
291+
292+
private readonly chatSetupTriggeredContext = ChatContextKeys.ChatSetup.triggered.bindTo(this.contextKeyService);
293+
294+
constructor(
295+
@IContextKeyService private readonly contextKeyService: IContextKeyService,
296+
@IStorageService private readonly storageService: IStorageService,
297+
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService
298+
) {
299+
this.updateContext();
300+
}
301+
302+
update(context: { triggered: boolean }): void;
303+
update(context: { chatInstalled?: boolean }): void;
304+
update(context: { triggered?: boolean; chatInstalled?: boolean }): void {
305+
if (typeof context.chatInstalled === 'boolean') {
306+
this.storageService.store(ChatSetupState.CHAT_EXTENSION_INSTALLED, context.chatInstalled, StorageScope.PROFILE, StorageTarget.MACHINE);
307+
}
308+
309+
if (typeof context.triggered === 'boolean') {
310+
if (context.triggered) {
311+
this.storageService.store(ChatSetupState.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE);
312+
} else {
313+
this.storageService.remove(ChatSetupState.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE);
314+
}
315+
}
316+
317+
this.updateContext();
318+
}
319+
320+
private updateContext(): void {
321+
const chatSetupTriggered = this.storageService.getBoolean(ChatSetupState.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false);
322+
const chatInstalled = this.storageService.getBoolean(ChatSetupState.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false);
323+
324+
const showChatSetup = chatSetupTriggered && !chatInstalled;
325+
if (showChatSetup) {
326+
// this is ugly but fixes flicker from a previous chat install
327+
this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION);
328+
this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION);
329+
}
330+
331+
this.chatSetupTriggeredContext.set(showChatSetup);
319332
}
320333
}
321334

@@ -336,36 +349,40 @@ class ChatSetupTriggerAction extends Action2 {
336349
const viewsService = accessor.get(IViewsService);
337350
const instantiationService = accessor.get(IInstantiationService);
338351

339-
instantiationService.createInstance(ChatSetupTrigger).update(true);
352+
instantiationService.createInstance(ChatSetupState).update({ triggered: true });
340353

341354
showChatView(viewsService);
342355
}
343356
}
344357

345-
class ChatSetupTrigger {
346-
347-
private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered';
358+
class ChatSetupHideAction extends Action2 {
348359

349-
private readonly chatSetupTriggeringContext = ChatContextKeys.ChatSetup.triggering.bindTo(this.contextKeyService);
360+
static readonly ID = 'workbench.action.chat.hideSetup';
361+
static readonly TITLE = localize2('hideChatSetup', "Hide Chat Setup");
350362

351-
constructor(
352-
@IContextKeyService private readonly contextKeyService: IContextKeyService,
353-
@IStorageService private readonly storageService: IStorageService
354-
) {
355-
if (this.storageService.getBoolean(ChatSetupTrigger.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE)) {
356-
this.update(true);
357-
}
363+
constructor() {
364+
super({
365+
id: ChatSetupHideAction.ID,
366+
title: ChatSetupHideAction.TITLE,
367+
f1: false
368+
});
358369
}
359370

360-
update(enabled: boolean): void {
361-
if (enabled) {
362-
this.storageService.store(ChatSetupTrigger.CHAT_SETUP_TRIGGERD, enabled, StorageScope.PROFILE, StorageTarget.MACHINE);
363-
this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); // fixes flicker issues with cached welcome from previous install
364-
} else {
365-
this.storageService.remove(ChatSetupTrigger.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE);
366-
}
371+
override async run(accessor: ServicesAccessor): Promise<void> {
372+
const viewsDescriptorService = accessor.get(IViewDescriptorService);
373+
const layoutService = accessor.get(IWorkbenchLayoutService);
374+
const instantiationService = accessor.get(IInstantiationService);
367375

368-
this.chatSetupTriggeringContext.set(enabled);
376+
const location = viewsDescriptorService.getViewLocationById(ChatViewId);
377+
378+
instantiationService.createInstance(ChatSetupState).update({ triggered: false });
379+
380+
if (location === ViewContainerLocation.AuxiliaryBar) {
381+
const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0);
382+
if (activeContainers.length === 0) {
383+
layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar
384+
}
385+
}
369386
}
370387
}
371388

@@ -491,6 +508,7 @@ class ChatSetupSignInAndInstallChatAction extends Action2 {
491508
}
492509

493510
registerAction2(ChatSetupTriggerAction);
511+
registerAction2(ChatSetupHideAction);
494512
registerAction2(ChatSetupInstallAction);
495513
registerAction2(ChatSetupSignInAndInstallChatAction);
496514

src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,6 @@ export class ChatViewWelcomePart extends Disposable {
135135
featureIndicator.textContent = localize('preview', 'PREVIEW');
136136
}
137137

138-
if (content.progress) {
139-
const progress = dom.append(this.element, $('.chat-welcome-view-progress'));
140-
progress.appendChild(renderIcon(spinningLoading));
141-
142-
const progressLabel = dom.append(progress, $('span'));
143-
progressLabel.textContent = content.progress;
144-
}
145-
146138
const message = dom.append(this.element, $('.chat-welcome-view-message'));
147139

148140
if (content.icon) {
@@ -167,6 +159,14 @@ export class ChatViewWelcomePart extends Disposable {
167159

168160
dom.append(message, messageResult.element);
169161

162+
if (content.progress) {
163+
const progress = dom.append(this.element, $('.chat-welcome-view-progress'));
164+
progress.appendChild(renderIcon(spinningLoading));
165+
166+
const progressLabel = dom.append(progress, $('span'));
167+
progressLabel.textContent = content.progress;
168+
}
169+
170170
if (content.tips) {
171171
const tips = dom.append(this.element, $('.chat-welcome-view-tips'));
172172
const tipsResult = this._register(renderer.render(content.tips));

src/vs/workbench/contrib/chat/common/chatContextKeys.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export namespace ChatContextKeys {
4545
signedIn: new RawContextKey<boolean>('chatSetupSignedIn', false, { type: 'boolean', description: localize('chatSetupSignedIn', "True when chat setup is offered for a signed-in user.") }),
4646
entitled: new RawContextKey<boolean>('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }),
4747

48-
triggering: new RawContextKey<boolean>('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }),
48+
triggered: new RawContextKey<boolean>('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }),
4949
installing: new RawContextKey<boolean>('chatSetupInstalling', false, { type: 'boolean', description: localize('chatSetupInstalling', "True when chat setup is installing chat.") }),
5050
signingIn: new RawContextKey<boolean>('chatSetupSigningIn', false, { type: 'boolean', description: localize('chatSetupSigningIn', "True when chat setup is waiting for signing in.") })
5151
};
52-
export const setupRunning = ContextKeyExpr.or(ChatSetup.triggering, ChatSetup.signingIn, ChatSetup.installing);
52+
export const setupRunning = ContextKeyExpr.or(ChatSetup.triggered, ChatSetup.signingIn, ChatSetup.installing);
5353
}

0 commit comments

Comments
 (0)