From 18fcd0256516926d76dc20deab2b88dd845e0980 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 15 Apr 2025 23:30:19 +0200 Subject: [PATCH] support rendering extensions in chat (#246666) --- src/vs/base/browser/ui/list/listPaging.ts | 1 + .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostChatAgents2.ts | 1 + .../api/common/extHostTypeConverters.ts | 15 +- src/vs/workbench/api/common/extHostTypes.ts | 7 + .../chatExtensionsContentPart.ts | 61 +++++++++ .../media/chatExtensionsContent.css | 14 ++ .../contrib/chat/browser/chatListRenderer.ts | 11 +- .../contrib/chat/common/chatModel.ts | 7 +- .../contrib/chat/common/chatService.ts | 6 + .../extensions/browser/extensionsViewer.ts | 129 +++++++++++++++++- .../extensions/browser/extensionsViews.ts | 122 +---------------- ...ode.proposed.chatParticipantAdditions.d.ts | 9 +- 13 files changed, 258 insertions(+), 126 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index e2608993f70..bb01170201c 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -110,6 +110,7 @@ export interface IPagedListOptions { readonly horizontalScrolling?: boolean; readonly scrollByPage?: boolean; readonly paddingBottom?: number; + readonly alwaysConsumeMouseWheel?: boolean; } function fromPagedListOptions(modelProvider: () => IPagedModel, options: IPagedListOptions): IListOptions { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f0141e6eaca..0db23694cd0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1797,6 +1797,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatResponseMovePart: extHostTypes.ChatResponseMovePart, + ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 4afd5db26ee..d7824ff4f0c 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -255,6 +255,7 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseConfirmationPart || part instanceof extHostTypes.ChatResponseCodeCitationPart || part instanceof extHostTypes.ChatResponseMovePart || + part instanceof extHostTypes.ChatResponseExtensionsPart || part instanceof extHostTypes.ChatResponseProgressPart2 ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 3a4664fb5fd..75c7f54d600 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -42,7 +42,7 @@ import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; @@ -2655,6 +2655,15 @@ export namespace ChatResponseWarningPart { } } +export namespace ChatResponseExtensionsPart { + export function from(part: vscode.ChatResponseExtensionsPart): Dto { + return { + kind: 'extensions', + extensions: part.extensions + }; + } +} + export namespace ChatResponseMovePart { export function from(part: vscode.ChatResponseMovePart): Dto { return { @@ -2814,7 +2823,7 @@ export namespace ChatResponseCodeCitationPart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseNotebookEditPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseNotebookEditPart | vscode.ChatResponseExtensionsPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2843,6 +2852,8 @@ export namespace ChatResponsePart { return ChatResponseCodeCitationPart.from(part); } else if (part instanceof types.ChatResponseMovePart) { return ChatResponseMovePart.from(part); + } else if (part instanceof types.ChatResponseExtensionsPart) { + return ChatResponseExtensionsPart.from(part); } return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d14292e920f..9546d32946a 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4680,6 +4680,13 @@ export class ChatResponseMovePart { } } +export class ChatResponseExtensionsPart { + constructor( + public readonly extensions: string[], + ) { + } +} + export class ChatResponseTextEditPart implements vscode.ChatResponseTextEditPart { uri: vscode.Uri; edits: vscode.TextEdit[]; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts new file mode 100644 index 00000000000..6ac6be2c970 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatExtensionsContent.css'; +import * as dom from '../../../../../base/browser/dom.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ExtensionsList, getExtensions } from '../../../extensions/browser/extensionsViewer.js'; +import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; +import { IChatExtensionsContent } from '../../common/chatService.js'; +import { IChatRendererContent } from '../../common/chatViewModel.js'; +import { ChatTreeItem, ChatViewId, IChatCodeBlockInfo } from '../chat.js'; +import { IChatContentPart } from './chatContentParts.js'; +import { PagedModel } from '../../../../../base/common/paging.js'; + +export class ChatExtensionsContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public get codeblocks(): IChatCodeBlockInfo[] { + return []; + } + + public get codeblocksPartId(): string | undefined { + return undefined; + } + + constructor( + private readonly extensionsContent: IChatExtensionsContent, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this.domNode = dom.$('.chat-extensions-content-part'); + + const extensionsList = dom.append(this.domNode, dom.$('.extensions-list')); + const list = this._register(instantiationService.createInstance(ExtensionsList, extensionsList, ChatViewId, { alwaysConsumeMouseWheel: false }, { onFocus: Event.None, onBlur: Event.None, filters: {} })); + getExtensions(extensionsContent.extensions, extensionsWorkbenchService).then(extensions => { + if (this._store.isDisposed) { + return; + } + list.setModel(new PagedModel(extensions)); + list.layout(); + this._onDidChangeHeight.fire(); + }); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'extensions' && other.extensions.length === this.extensionsContent.extensions.length && other.extensions.every(ext => this.extensionsContent.extensions.includes(ext)); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css new file mode 100644 index 00000000000..61a66ee4bc3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-extensions-content-part { + border: 1px solid var(--vscode-chat-requestBorder); + border-bottom: none; + border-radius: 4px; +} + +.chat-extensions-content-part .extension-list-item { + border-bottom: 1px solid var(--vscode-chat-requestBorder); +} diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 0f6194ed65a..3e0faa1bd19 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -48,7 +48,7 @@ import { IChatAgentMetadata } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; import { chatSubcommandLeader } from '../common/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { getNWords } from '../common/chatWordCounter.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; @@ -62,6 +62,7 @@ import { ChatCodeCitationContentPart } from './chatContentParts/chatCodeCitation import { ChatCommandButtonContentPart } from './chatContentParts/chatCommandContentPart.js'; import { ChatConfirmationContentPart } from './chatContentParts/chatConfirmationContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts/chatContentParts.js'; +import { ChatExtensionsContentPart } from './chatContentParts/chatExtensionsContentPart.js'; import { ChatMarkdownContentPart, EditorPool } from './chatContentParts/chatMarkdownContentPart.js'; import { ChatProgressContentPart, ChatWorkingProgressContentPart } from './chatContentParts/chatProgressContentPart.js'; import { ChatQuotaExceededPart } from './chatContentParts/chatQuotaExceededPart.js'; @@ -901,6 +902,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); + return part; + } + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { if (!isResponseVM(context.element)) { return; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 75c8b155f3b..78e65838760 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -27,7 +27,7 @@ import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommo import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -285,7 +285,8 @@ export type IChatProgressHistoryResponseContent = | IChatTask | IChatTextEditGroup | IChatNotebookEditGroup - | IChatConfirmation; + | IChatConfirmation + | IChatExtensionsContent; /** * "Normal" progress kinds that are rendered as parts of the stream of content. @@ -509,6 +510,7 @@ class AbstractResponse implements IResponse { case 'codeblockUri': case 'toolInvocation': case 'toolInvocationSerialized': + case 'extensions': case 'undoStop': // Ignore continue; @@ -1675,6 +1677,7 @@ export class ChatModel extends Disposable implements IChatModel { progress.kind === 'warning' || progress.kind === 'progressTask' || progress.kind === 'confirmation' || + progress.kind === 'extensions' || progress.kind === 'toolInvocation' ) { request.response.updateContent(progress, quiet); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5a8f1e0bdd0..a092b07cd7c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -258,6 +258,11 @@ export interface IChatToolInvocationSerialized { kind: 'toolInvocationSerialized'; } +export interface IChatExtensionsContent { + extensions: string[]; + kind: 'extensions'; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -278,6 +283,7 @@ export type IChatProgress = | IChatConfirmation | IChatToolInvocation | IChatToolInvocationSerialized + | IChatExtensionsContent | IChatUndoStop; export interface IChatFollowup { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 82f9f53d501..f327b08b0d5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -5,17 +5,17 @@ import * as dom from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; -import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Action } from '../../../../base/common/actions.js'; -import { IExtensionsWorkbenchService, IExtension } from '../common/extensions.js'; +import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; +import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; +import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js'; import { Event } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IListService, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; +import { IListService, IWorkbenchPagedListOptions, WorkbenchAsyncDataTree, WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; -import { IListVirtualDelegate, IListRenderer } from '../../../../base/browser/ui/list/list.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Delegate, Renderer } from './extensionsList.js'; @@ -27,6 +27,125 @@ import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IStyleOverride } from '../../../../platform/theme/browser/defaultStyles.js'; import { getAriaLabelForExtension } from './extensionsViews.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; +import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { ExtensionAction, getContextMenuActions, ManageExtensionAction } from './extensionsActions.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js'; +import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js'; + +export class ExtensionsList extends Disposable { + + readonly list: WorkbenchPagedList; + private readonly contextMenuActionRunner = this._register(new ActionRunner()); + + constructor( + parent: HTMLElement, + viewId: string, + options: Partial>, + extensionsViewState: IExtensionsViewState, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @INotificationService notificationService: INotificationService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error))); + const delegate = new Delegate(); + const renderer = instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = viewDescriptorService.getViewLocationById(viewId); + if (viewLocation === ViewContainerLocation.Sidebar) { + return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); + this.list = instantiationService.createInstance(WorkbenchPagedList, `${viewId}-Extensions`, parent, delegate, [renderer], { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(extension: IExtension | null): string { + return getAriaLabelForExtension(extension); + }, + getWidgetAriaLabel(): string { + return localize('extensions', "Extensions"); + } + }, + overrideStyles: getLocationBasedViewColors(viewDescriptorService.getViewLocationById(viewId)).listOverrideStyles, + openOnSingleClick: true, + ...options + }) as WorkbenchPagedList; + this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); + this._register(this.list); + + this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { + this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); + })); + } + + setModel(model: IPagedModel) { + this.list.model = new DelayedPagedModel(model); + } + + layout(height?: number, width?: number): void { + this.list.layout(height, width); + } + + private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { + extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; + this.extensionsWorkbenchService.open(extension, options); + } + + private async onContextMenu(e: IListContextMenuEvent): Promise { + if (e.element) { + const disposables = new DisposableStore(); + const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); + const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element + : e.element; + manageExtensionAction.extension = extension; + let groups: IAction[][] = []; + if (manageExtensionAction.enabled) { + groups = await manageExtensionAction.getActionGroups(); + } else if (extension) { + groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); + groups.forEach(group => group.forEach(extensionAction => { + if (extensionAction instanceof ExtensionAction) { + extensionAction.extension = extension; + } + })); + } + const actions: IAction[] = []; + for (const menuActions of groups) { + for (const menuAction of menuActions) { + actions.push(menuAction); + if (isDisposable(menuAction)) { + disposables.add(menuAction); + } + } + actions.push(new Separator()); + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + actionRunner: this.contextMenuActionRunner, + onHide: () => disposables.dispose() + }); + } + } +} export class ExtensionsGridView extends Disposable { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index dd378644f9c..f6ece8cd747 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { isCancellationError, getErrorMessage, CancellationError } from '../../../../base/common/errors.js'; -import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; import { PagedModel, IPagedModel, DelayedPagedModel, IPager } from '../../../../base/common/paging.js'; import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo, ExtensionGalleryErrorCode, ExtensionGalleryError } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -17,7 +16,6 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { append, $ } from '../../../../base/browser/dom.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Delegate, Renderer } from './extensionsList.js'; import { ExtensionResultsListFocused, ExtensionState, IExtension, IExtensionsViewState, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js'; import { Query } from '../common/extensionQuery.js'; import { IExtensionService, toExtension } from '../../../services/extensions/common/extensions.js'; @@ -25,7 +23,6 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; -import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from './extensionsActions.js'; import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; @@ -33,23 +30,19 @@ import { ViewPane, IViewPaneOptions, ViewPaneShowActions } from '../../../browse import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { coalesce, distinct, range } from '../../../../base/common/arrays.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { IAction, Action, Separator, ActionRunner } from '../../../../base/common/actions.js'; +import { ActionRunner } from '../../../../base/common/actions.js'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, IExtensionIdentifier, isLanguagePackExtension } from '../../../../platform/extensions/common/extensions.js'; import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from '../../../../base/common/async.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IViewDescriptorService } from '../../../common/views.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; -import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { isOfflineError } from '../../../../base/parts/request/common/request.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; @@ -59,6 +52,7 @@ import { URI } from '../../../../base/common/uri.js'; import { isString } from '../../../../base/common/types.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { ExtensionsList } from './extensionsViewer.js'; export const NONE_CATEGORY = 'none'; @@ -156,11 +150,9 @@ export class ExtensionsListView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, - @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService @@ -196,46 +188,10 @@ export class ExtensionsListView extends ViewPane { const messageSeverityIcon = append(messageContainer, $('')); const messageBox = append(messageContainer, $('.message')); const extensionsList = append(container, $('.extensions-list')); - const delegate = new Delegate(); - this.extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, this.extensionsViewState, { - hoverOptions: { - position: () => { - const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); - if (viewLocation === ViewContainerLocation.Sidebar) { - return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; - } - if (viewLocation === ViewContainerLocation.AuxiliaryBar) { - return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; - } - return HoverPosition.RIGHT; - } - } - }); - this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { - multipleSelectionSupport: false, - setRowLineHeight: false, - horizontalScrolling: false, - accessibilityProvider: { - getAriaLabel(extension: IExtension | null): string { - return getAriaLabelForExtension(extension); - }, - getWidgetAriaLabel(): string { - return localize('extensions', "Extensions"); - } - }, - overrideStyles: this.getLocationBasedColors().listOverrideStyles, - openOnSingleClick: true - }) as WorkbenchPagedList; + this.extensionsViewState = this._register(new ExtensionsViewState()); + this.list = this._register(this.instantiationService.createInstance(ExtensionsList, extensionsList, this.id, {}, this.extensionsViewState)).list; ExtensionResultsListFocused.bindTo(this.list.contextKeyService); - this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); this._register(this.list.onDidChangeFocus(e => this.extensionsViewState?.onFocusChange(coalesce(e.elements)), this)); - this._register(this.list); - this._register(this.extensionsViewState); - - this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { - this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); - })); this.bodyTemplate = { extensionsList, @@ -327,44 +283,6 @@ export class ExtensionsListView extends ViewPane { return Promise.resolve(emptyModel); } - private async onContextMenu(e: IListContextMenuEvent): Promise { - if (e.element) { - const disposables = new DisposableStore(); - const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); - const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element - : e.element; - manageExtensionAction.extension = extension; - let groups: IAction[][] = []; - if (manageExtensionAction.enabled) { - groups = await manageExtensionAction.getActionGroups(); - } else if (extension) { - groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); - groups.forEach(group => group.forEach(extensionAction => { - if (extensionAction instanceof ExtensionAction) { - extensionAction.extension = extension; - } - })); - } - const actions: IAction[] = []; - for (const menuActions of groups) { - for (const menuAction of menuActions) { - actions.push(menuAction); - if (isDisposable(menuAction)) { - disposables.add(menuAction); - } - } - actions.push(new Separator()); - } - actions.pop(); - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions, - actionRunner: this.contextMenuActionRunner, - onHide: () => disposables.dispose() - }); - } - } - private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise { const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g; const ids: string[] = []; @@ -1194,30 +1112,6 @@ export class ExtensionsListView extends ViewPane { } } - private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { - extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; - this.extensionsWorkbenchService.open(extension, options).then(undefined, err => this.onError(err)); - } - - private onError(err: any): void { - if (isCancellationError(err)) { - return; - } - - const message = err && err.message || ''; - - if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ]); - - this.notificationService.error(error); - return; - } - - this.notificationService.error(err); - } - override dispose(): void { super.dispose(); if (this.queryRequest) { @@ -1454,11 +1348,9 @@ export class StaticQueryExtensionsView extends ExtensionsListView { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, - @IPreferencesService preferencesService: IPreferencesService, @IStorageService storageService: IStorageService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILogService logService: ILogService @@ -1466,7 +1358,7 @@ export class StaticQueryExtensionsView extends ExtensionsListView { super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, extensionRecommendationsService, telemetryService, hoverService, configurationService, contextService, extensionManagementServerService, extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService, - preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, + storageService, workspaceTrustManagementService, extensionEnablementService, extensionFeaturesManagementService, uriIdentityService, logService); } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 9f242727688..710ea45fdbe 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -79,7 +79,7 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart; export class ChatResponseWarningPart { value: MarkdownString; @@ -159,6 +159,13 @@ declare module 'vscode' { resolve?(token: CancellationToken): Thenable; } + export class ChatResponseExtensionsPart { + + readonly extensions: string[]; + + constructor(extensions: string[]); + } + export interface ChatResponseStream { /**