Skip to content

Commit

Permalink
Add welcome view with warning for chat extension with invalid API ver…
Browse files Browse the repository at this point in the history
…sion

Fix #218646
  • Loading branch information
roblourens committed Aug 28, 2024
1 parent 78f4b54 commit 13b385e
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ const _allApiProposals = {
},
lmTools: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.lmTools.d.ts',
version: 6
version: 5
},
mappedEditsProvider: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts',
Expand Down
6 changes: 2 additions & 4 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
106 changes: 56 additions & 50 deletions src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,30 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Action } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
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';
import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views';
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 { 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';
import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';

const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
extensionPoint: 'chatParticipants',
Expand Down Expand Up @@ -160,35 +162,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';
Expand All @@ -198,9 +171,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();
}

Expand Down Expand Up @@ -239,11 +213,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;
Expand All @@ -260,6 +229,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
}
}

const store = new DisposableStore();
store.add(this._chatAgentService.registerAgent(
providerDescriptor.id,
{
Expand Down Expand Up @@ -318,15 +288,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,
Expand All @@ -336,12 +300,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);
});
}
Expand All @@ -350,3 +313,46 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
return `${extensionId.value}_${participantName}`;
}

export interface IChatCompatibilityService {
_serviceBrand: undefined;

readonly isChatExtensionIncompatible: boolean;
readonly onDidChatExtensionIncompatibilityChange: Event<void>;
}

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();
}
});
}
}
7 changes: 6 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}));
}

Expand All @@ -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);
}
Expand Down
12 changes: 11 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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 = {
Expand All @@ -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);
});
}
Expand Down
4 changes: 3 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatContextKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/vscode-dts/vscode.proposed.lmTools.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// version: 6
// version: 5
// https://github.com/microsoft/vscode/issues/213274

declare module 'vscode' {
Expand Down

0 comments on commit 13b385e

Please sign in to comment.