Skip to content

Commit ae45c9d

Browse files
authored
Add welcome view with warning for chat extension with invalid API version (microsoft#227020)
* Add welcome view with warning for chat extension with invalid API version Fix microsoft#218646 * undo * Remove unused interface
1 parent 80ef8fe commit ae45c9d

File tree

5 files changed

+70
-57
lines changed

5 files changed

+70
-57
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat
3636
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
3737
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
3838
import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
39-
import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions';
39+
import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions';
4040
import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick';
4141
import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView';
4242
import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables';
@@ -261,9 +261,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlas
261261
Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer);
262262
registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup);
263263
registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore);
264-
265-
// Disabled until https://github.com/microsoft/vscode/issues/218646 is fixed
266-
// registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
264+
registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
267265

268266
registerChatActions();
269267
registerChatCopyActions();

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

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Action } from 'vs/base/common/actions';
76
import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays';
87
import { Codicon } from 'vs/base/common/codicons';
9-
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
8+
import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
109
import * as strings from 'vs/base/common/strings';
1110
import { localize, localize2 } from 'vs/nls';
12-
import { ICommandService } from 'vs/platform/commands/common/commands';
11+
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1312
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
1413
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
1514
import { ILogService } from 'vs/platform/log/common/log';
16-
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
15+
import { Severity } from 'vs/platform/notification/common/notification';
1716
import { Registry } from 'vs/platform/registry/common/platform';
1817
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
1918
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
2019
import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views';
2120
import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat';
2221
import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane';
2322
import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
23+
import { CONTEXT_CHAT_EXTENSION_INVALID, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
2424
import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes';
25+
import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
2526
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
2627
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
2728
import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
@@ -160,35 +161,6 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
160161
},
161162
});
162163

163-
export class ChatCompatibilityNotifier implements IWorkbenchContribution {
164-
static readonly ID = 'workbench.contrib.chatCompatNotifier';
165-
166-
constructor(
167-
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
168-
@INotificationService notificationService: INotificationService,
169-
@ICommandService commandService: ICommandService
170-
) {
171-
// It may be better to have some generic UI for this, for any extension that is incompatible,
172-
// but this is only enabled for Copilot Chat now and it needs to be obvious.
173-
extensionsWorkbenchService.queryLocal().then(exts => {
174-
const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat');
175-
if (chat?.local?.validations.some(v => v[0] === Severity.Error)) {
176-
notificationService.notify({
177-
severity: Severity.Error,
178-
message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."),
179-
actions: {
180-
primary: [
181-
new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => {
182-
return commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']);
183-
})
184-
]
185-
}
186-
});
187-
}
188-
});
189-
}
190-
}
191-
192164
export class ChatExtensionPointHandler implements IWorkbenchContribution {
193165

194166
static readonly ID = 'workbench.contrib.chatExtensionPointHandler';
@@ -198,9 +170,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
198170

199171
constructor(
200172
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
201-
@ILogService private readonly logService: ILogService,
173+
@ILogService private readonly logService: ILogService
202174
) {
203175
this._viewContainer = this.registerViewContainer();
176+
this.registerDefaultParticipantView();
204177
this.handleAndRegisterChatExtensions();
205178
}
206179

@@ -239,11 +212,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
239212
continue;
240213
}
241214

242-
const store = new DisposableStore();
243-
if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(ChatAgentLocation.Panel))) {
244-
store.add(this.registerDefaultParticipantView(providerDescriptor));
245-
}
246-
247215
const participantsAndCommandsDisambiguation: {
248216
categoryName: string;
249217
description: string;
@@ -260,6 +228,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
260228
}
261229
}
262230

231+
const store = new DisposableStore();
263232
store.add(this._chatAgentService.registerAgent(
264233
providerDescriptor.id,
265234
{
@@ -318,15 +287,9 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
318287
return viewContainer;
319288
}
320289

321-
private hasRegisteredDefaultParticipantView = false;
322-
private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable {
323-
if (this.hasRegisteredDefaultParticipantView) {
324-
this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`);
325-
return Disposable.None;
326-
}
327-
328-
// Register View
329-
const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name;
290+
private registerDefaultParticipantView(): IDisposable {
291+
// Register View. Name must be hardcoded because we want to show it even when the extension fails to load due to an API version incompatibility.
292+
const name = 'GitHub Copilot';
330293
const viewDescriptor: IViewDescriptor[] = [{
331294
id: CHAT_VIEW_ID,
332295
containerIcon: this._viewContainer.icon,
@@ -336,12 +299,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
336299
canToggleVisibility: false,
337300
canMoveView: true,
338301
ctorDescriptor: new SyncDescriptor(ChatViewPane),
302+
when: ContextKeyExpr.or(CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_CHAT_EXTENSION_INVALID)
339303
}];
340-
this.hasRegisteredDefaultParticipantView = true;
341304
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer);
342305

343306
return toDisposable(() => {
344-
this.hasRegisteredDefaultParticipantView = false;
345307
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer);
346308
});
347309
}
@@ -350,3 +312,39 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
350312
function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
351313
return `${extensionId.value}_${participantName}`;
352314
}
315+
316+
export class ChatCompatibilityNotifier implements IWorkbenchContribution {
317+
static readonly ID = 'workbench.contrib.chatCompatNotifier';
318+
319+
constructor(
320+
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
321+
@IContextKeyService contextKeyService: IContextKeyService,
322+
@IChatAgentService chatAgentService: IChatAgentService,
323+
) {
324+
// It may be better to have some generic UI for this, for any extension that is incompatible,
325+
// but this is only enabled for Copilot Chat now and it needs to be obvious.
326+
327+
const showExtensionLabel = localize('showExtension', "Show Extension");
328+
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
329+
viewsRegistry.registerViewWelcomeContent(CHAT_VIEW_ID, {
330+
content: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date.") + `\n\n[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([['GitHub.copilot-chat']]))})`,
331+
when: CONTEXT_CHAT_EXTENSION_INVALID,
332+
});
333+
334+
const isInvalid = CONTEXT_CHAT_EXTENSION_INVALID.bindTo(contextKeyService);
335+
extensionsWorkbenchService.queryLocal().then(exts => {
336+
const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat');
337+
if (chat?.local?.validations.some(v => v[0] === Severity.Error)) {
338+
// This catches vscode starting up with the invalid extension, but the extension may still get updated by vscode after this.
339+
isInvalid.set(true);
340+
}
341+
});
342+
343+
const listener = chatAgentService.onDidChangeAgents(() => {
344+
if (chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) {
345+
isInvalid.set(false);
346+
listener.dispose();
347+
}
348+
});
349+
}
350+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ export class ChatViewPane extends ViewPane {
8888
} else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) {
8989
// Model is initialized, and the default agent disappeared, so show welcome view
9090
this.didUnregisterProvider = true;
91-
this._onDidChangeViewWelcomeState.fire();
9291
}
92+
93+
this._onDidChangeViewWelcomeState.fire();
9394
}));
9495
}
9596

@@ -114,6 +115,10 @@ export class ChatViewPane extends ViewPane {
114115
}
115116

116117
override shouldShowWelcome(): boolean {
118+
if (!this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) {
119+
return true;
120+
}
121+
117122
const noPersistedSessions = !this.chatService.hasSessions();
118123
return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail);
119124
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ILogService } from 'vs/platform/log/common/log';
2424
import { IProductService } from 'vs/platform/product/common/productService';
2525
import { asJson, IRequestService } from 'vs/platform/request/common/request';
2626
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
27-
import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
27+
import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
2828
import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel';
2929
import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes';
3030
import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService';
@@ -233,11 +233,13 @@ export class ChatAgentService implements IChatAgentService {
233233
readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event;
234234

235235
private readonly _hasDefaultAgent: IContextKey<boolean>;
236+
private readonly _defaultAgentRegistered: IContextKey<boolean>;
236237

237238
constructor(
238239
@IContextKeyService private readonly contextKeyService: IContextKeyService,
239240
) {
240241
this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService);
242+
this._defaultAgentRegistered = CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.bindTo(this.contextKeyService);
241243
}
242244

243245
registerAgent(id: string, data: IChatAgentData): IDisposable {
@@ -246,6 +248,10 @@ export class ChatAgentService implements IChatAgentService {
246248
throw new Error(`Agent already registered: ${JSON.stringify(id)}`);
247249
}
248250

251+
if (data.isDefault) {
252+
this._defaultAgentRegistered.set(true);
253+
}
254+
249255
const that = this;
250256
const commands = data.slashCommands;
251257
data = {
@@ -258,6 +264,10 @@ export class ChatAgentService implements IChatAgentService {
258264
this._agents.set(id, entry);
259265
return toDisposable(() => {
260266
this._agents.delete(id);
267+
if (data.isDefault) {
268+
this._defaultAgentRegistered.set(false);
269+
}
270+
261271
this._onDidChangeAgents.fire(undefined);
262272
});
263273
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey<boolean>('chatInpu
2525
export const CONTEXT_IN_CHAT_INPUT = new RawContextKey<boolean>('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") });
2626
export const CONTEXT_IN_CHAT_SESSION = new RawContextKey<boolean>('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") });
2727

28-
export const CONTEXT_CHAT_ENABLED = new RawContextKey<boolean>('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is registered.") });
28+
export const CONTEXT_CHAT_ENABLED = new RawContextKey<boolean>('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") });
29+
export const CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED = new RawContextKey<boolean>('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") });
30+
export const CONTEXT_CHAT_EXTENSION_INVALID = new RawContextKey<boolean>('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") });
2931
export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey<boolean>('chatCursorAtTop', false);
3032
export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey<boolean>('chatInputHasAgent', false);
3133
export const CONTEXT_CHAT_LOCATION = new RawContextKey<ChatAgentLocation>('chatLocation', undefined);

0 commit comments

Comments
 (0)