From ae45c9d4b0f71d53151edc6d18be09107903c229 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 28 Aug 2024 21:21:48 -0700 Subject: [PATCH] Add welcome view with warning for chat extension with invalid API version (#227020) * Add welcome view with warning for chat extension with invalid API version Fix #218646 * undo * Remove unused interface --- .../contrib/chat/browser/chat.contribution.ts | 6 +- .../browser/chatParticipantContributions.ts | 98 +++++++++---------- .../contrib/chat/browser/chatViewPane.ts | 7 +- .../contrib/chat/common/chatAgents.ts | 12 ++- .../contrib/chat/common/chatContextKeys.ts | 4 +- 5 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d5cf3c355e0..be6c4955294 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -36,7 +36,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; -import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; +import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; @@ -261,9 +261,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlas Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); - -// Disabled until https://github.com/microsoft/vscode/issues/218646 is fixed -// registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 09a83daf318..2bf50f4a8cf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { localize, localize2 } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -21,7 +20,9 @@ import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_EXTENSION_INVALID, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -160,35 +161,6 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi }, }); -export class ChatCompatibilityNotifier implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.chatCompatNotifier'; - - constructor( - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @INotificationService notificationService: INotificationService, - @ICommandService commandService: ICommandService - ) { - // It may be better to have some generic UI for this, for any extension that is incompatible, - // but this is only enabled for Copilot Chat now and it needs to be obvious. - extensionsWorkbenchService.queryLocal().then(exts => { - const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat'); - if (chat?.local?.validations.some(v => v[0] === Severity.Error)) { - notificationService.notify({ - severity: Severity.Error, - message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), - actions: { - primary: [ - new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { - return commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); - }) - ] - } - }); - } - }); - } -} - export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; @@ -198,9 +170,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { constructor( @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @ILogService private readonly logService: ILogService, + @ILogService private readonly logService: ILogService ) { this._viewContainer = this.registerViewContainer(); + this.registerDefaultParticipantView(); this.handleAndRegisterChatExtensions(); } @@ -239,11 +212,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - const store = new DisposableStore(); - if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(ChatAgentLocation.Panel))) { - store.add(this.registerDefaultParticipantView(providerDescriptor)); - } - const participantsAndCommandsDisambiguation: { categoryName: string; description: string; @@ -260,6 +228,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } } + const store = new DisposableStore(); store.add(this._chatAgentService.registerAgent( providerDescriptor.id, { @@ -318,15 +287,9 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { return viewContainer; } - private hasRegisteredDefaultParticipantView = false; - private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { - if (this.hasRegisteredDefaultParticipantView) { - this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`); - return Disposable.None; - } - - // Register View - const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; + private registerDefaultParticipantView(): IDisposable { + // 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. + const name = 'GitHub Copilot'; const viewDescriptor: IViewDescriptor[] = [{ id: CHAT_VIEW_ID, containerIcon: this._viewContainer.icon, @@ -336,12 +299,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane), + when: ContextKeyExpr.or(CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_CHAT_EXTENSION_INVALID) }]; - this.hasRegisteredDefaultParticipantView = true; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); return toDisposable(() => { - this.hasRegisteredDefaultParticipantView = false; Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); }); } @@ -350,3 +312,39 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { return `${extensionId.value}_${participantName}`; } + +export class ChatCompatibilityNotifier implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.chatCompatNotifier'; + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService, + ) { + // It may be better to have some generic UI for this, for any extension that is incompatible, + // but this is only enabled for Copilot Chat now and it needs to be obvious. + + const showExtensionLabel = localize('showExtension', "Show Extension"); + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + viewsRegistry.registerViewWelcomeContent(CHAT_VIEW_ID, { + 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']]))})`, + when: CONTEXT_CHAT_EXTENSION_INVALID, + }); + + const isInvalid = CONTEXT_CHAT_EXTENSION_INVALID.bindTo(contextKeyService); + extensionsWorkbenchService.queryLocal().then(exts => { + const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat'); + if (chat?.local?.validations.some(v => v[0] === Severity.Error)) { + // This catches vscode starting up with the invalid extension, but the extension may still get updated by vscode after this. + isInvalid.set(true); + } + }); + + const listener = chatAgentService.onDidChangeAgents(() => { + if (chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { + isInvalid.set(false); + listener.dispose(); + } + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 0cb92990dff..aa2655cba87 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -88,8 +88,9 @@ export class ChatViewPane extends ViewPane { } else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) { // Model is initialized, and the default agent disappeared, so show welcome view this.didUnregisterProvider = true; - this._onDidChangeViewWelcomeState.fire(); } + + this._onDidChangeViewWelcomeState.fire(); })); } @@ -114,6 +115,10 @@ export class ChatViewPane extends ViewPane { } override shouldShowWelcome(): boolean { + if (!this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { + return true; + } + const noPersistedSessions = !this.chatService.hasSessions(); return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail); } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 3ff7e3311bb..f565b5b9266 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -24,7 +24,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; @@ -233,11 +233,13 @@ export class ChatAgentService implements IChatAgentService { readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; private readonly _hasDefaultAgent: IContextKey; + private readonly _defaultAgentRegistered: IContextKey; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); + this._defaultAgentRegistered = CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.bindTo(this.contextKeyService); } registerAgent(id: string, data: IChatAgentData): IDisposable { @@ -246,6 +248,10 @@ export class ChatAgentService implements IChatAgentService { throw new Error(`Agent already registered: ${JSON.stringify(id)}`); } + if (data.isDefault) { + this._defaultAgentRegistered.set(true); + } + const that = this; const commands = data.slashCommands; data = { @@ -258,6 +264,10 @@ export class ChatAgentService implements IChatAgentService { this._agents.set(id, entry); return toDisposable(() => { this._agents.delete(id); + if (data.isDefault) { + this._defaultAgentRegistered.set(false); + } + this._onDidChangeAgents.fire(undefined); }); } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 5930a97566c..f651a114a44 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -25,7 +25,9 @@ export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey('chatInpu export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); -export const CONTEXT_CHAT_ENABLED = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is registered.") }); +export const CONTEXT_CHAT_ENABLED = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); +export const CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED = new RawContextKey('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") }); +export const CONTEXT_CHAT_EXTENSION_INVALID = new RawContextKey('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") }); export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey('chatCursorAtTop', false); export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey('chatInputHasAgent', false); export const CONTEXT_CHAT_LOCATION = new RawContextKey('chatLocation', undefined);