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
pull/227048/head
Rob Lourens 2024-08-28 21:21:48 -07:00 committed by GitHub
parent 80ef8fe939
commit ae45c9d4b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 70 additions and 57 deletions

View File

@ -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<IEditorFactoryRegistry>(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();

View File

@ -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<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer);
return toDisposable(() => {
this.hasRegisteredDefaultParticipantView = false;
Registry.as<IViewsRegistry>(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<IViewsRegistry>(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();
}
});
}
}

View File

@ -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);
}

View File

@ -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<IChatAgent | undefined> = this._onDidChangeAgents.event;
private readonly _hasDefaultAgent: IContextKey<boolean>;
private readonly _defaultAgentRegistered: IContextKey<boolean>;
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);
});
}

View File

@ -25,7 +25,9 @@ export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey<boolean>('chatInpu
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.") });
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.") });
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.") });
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.") });
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.") });
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.") });
export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey<boolean>('chatCursorAtTop', false);
export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey<boolean>('chatInputHasAgent', false);
export const CONTEXT_CHAT_LOCATION = new RawContextKey<ChatAgentLocation>('chatLocation', undefined);