Path: blob/main/src/vs/workbench/contrib/chat/browser/chatOutputItemRenderer.ts
5262 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getWindow } from '../../../../base/browser/dom.js';6import { raceCancellationError } from '../../../../base/common/async.js';7import { CancellationToken } from '../../../../base/common/cancellation.js';8import { matchesMimeType } from '../../../../base/common/dataTransfer.js';9import { CancellationError } from '../../../../base/common/errors.js';10import { Emitter, Event } from '../../../../base/common/event.js';11import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js';12import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';13import { autorun } from '../../../../base/common/observable.js';14import { URI } from '../../../../base/common/uri.js';15import { generateUuid } from '../../../../base/common/uuid.js';16import * as nls from '../../../../nls.js';17import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';18import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';19import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';20import { IWebview, IWebviewService, WebviewContentPurpose } from '../../../contrib/webview/browser/webview.js';21import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';22import { ExtensionsRegistry, IExtensionPointUser } from '../../../services/extensions/common/extensionsRegistry.js';2324export interface IChatOutputItemRenderer {25renderOutputPart(mime: string, data: Uint8Array, webview: IWebview, token: CancellationToken): Promise<void>;26}2728interface RegisterOptions {29readonly extension?: {30readonly id: ExtensionIdentifier;31readonly location: URI;32};33}3435export const IChatOutputRendererService = createDecorator<IChatOutputRendererService>('chatOutputRendererService');3637export interface IChatOutputRendererService {38readonly _serviceBrand: undefined;3940registerRenderer(mime: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable;4142renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart>;43}4445export interface RenderedOutputPart extends IDisposable {46readonly onDidChangeHeight: Event<number>;47readonly webview: IWebview;4849reinitialize(): void;50}5152interface RenderOutputPartWebviewOptions {53readonly origin?: string;54readonly webviewState?: string;55}565758interface RendererEntry {59readonly viewType: string;60readonly renderer: IChatOutputItemRenderer;61readonly options: RegisterOptions;62}6364export class ChatOutputRendererService extends Disposable implements IChatOutputRendererService {65_serviceBrand: undefined;6667private readonly _contributions = new Map</*viewType*/ string, {68readonly mimes: readonly string[];69}>();7071private readonly _renderers = new Map</*viewType*/ string, RendererEntry>();7273constructor(74@IContextKeyService private readonly _contextKeyService: IContextKeyService,75@IExtensionService private readonly _extensionService: IExtensionService,76@IWebviewService private readonly _webviewService: IWebviewService,77) {78super();7980this._register(chatOutputRenderContributionPoint.setHandler(extensions => {81this.updateContributions(extensions);82}));83}8485registerRenderer(viewType: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable {86this._renderers.set(viewType, { viewType, renderer, options });87return {88dispose: () => {89this._renderers.delete(viewType);90}91};92}9394async renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart> {95const rendererData = await this.getRenderer(mime, token);96if (token.isCancellationRequested) {97throw new CancellationError();98}99100if (!rendererData) {101throw new Error(`No renderer registered found for mime type: ${mime}`);102}103104const store = new DisposableStore();105106const webview = store.add(this._webviewService.createWebviewElement({107title: '',108origin: webviewOptions.origin ?? generateUuid(),109providedViewType: rendererData.viewType,110options: {111enableFindWidget: false,112purpose: WebviewContentPurpose.ChatOutputItem,113tryRestoreScrollPosition: false,114},115contentOptions: {},116extension: rendererData.options.extension ? rendererData.options.extension : undefined,117}));118webview.setContextKeyService(store.add(this._contextKeyService.createScoped(parent)));119120const onDidChangeHeight = store.add(new Emitter<number>());121store.add(autorun(reader => {122const height = reader.readObservable(webview.intrinsicContentSize);123if (height) {124onDidChangeHeight.fire(height.height);125parent.style.height = `${height.height}px`;126}127}));128129if (webviewOptions.webviewState) {130webview.state = webviewOptions.webviewState;131}132133webview.mountTo(parent, getWindow(parent));134await rendererData.renderer.renderOutputPart(mime, data, webview, token);135136return {137get webview() { return webview; },138onDidChangeHeight: onDidChangeHeight.event,139dispose: () => {140store.dispose();141},142reinitialize: () => {143webview.reinitializeAfterDismount();144},145};146}147148private async getRenderer(mime: string, token: CancellationToken): Promise<RendererEntry | undefined> {149await raceCancellationError(this._extensionService.whenInstalledExtensionsRegistered(), token);150for (const [id, value] of this._contributions) {151if (value.mimes.some(m => matchesMimeType(m, [mime]))) {152await raceCancellationError(this._extensionService.activateByEvent(`onChatOutputRenderer:${id}`), token);153const rendererData = this._renderers.get(id);154if (rendererData) {155return rendererData;156}157}158}159160return undefined;161}162163private updateContributions(extensions: readonly IExtensionPointUser<readonly IChatOutputRendererContribution[]>[]) {164this._contributions.clear();165for (const extension of extensions) {166if (!isProposedApiEnabled(extension.description, 'chatOutputRenderer')) {167continue;168}169170for (const contribution of extension.value) {171if (this._contributions.has(contribution.viewType)) {172extension.collector.error(`Chat output renderer with view type '${contribution.viewType}' already registered`);173continue;174}175176this._contributions.set(contribution.viewType, {177mimes: contribution.mimeTypes,178});179}180}181}182}183184const chatOutputRendererContributionSchema = {185type: 'object',186additionalProperties: false,187required: ['viewType', 'mimeTypes'],188properties: {189viewType: {190type: 'string',191description: nls.localize('chatOutputRenderer.viewType', 'Unique identifier for the renderer.'),192},193mimeTypes: {194type: 'array',195description: nls.localize('chatOutputRenderer.mimeTypes', 'MIME types that this renderer can handle'),196items: {197type: 'string'198}199}200}201} as const satisfies IJSONSchema;202203type IChatOutputRendererContribution = TypeFromJsonSchema<typeof chatOutputRendererContributionSchema>;204205const chatOutputRenderContributionPoint = ExtensionsRegistry.registerExtensionPoint<IChatOutputRendererContribution[]>({206extensionPoint: 'chatOutputRenderers',207activationEventsGenerator: function* (contributions) {208for (const contrib of contributions) {209yield `onChatOutputRenderer:${contrib.viewType}`;210}211},212jsonSchema: {213description: nls.localize('vscode.extension.contributes.chatOutputRenderer', 'Contributes a renderer for specific MIME types in chat outputs'),214type: 'array',215items: chatOutputRendererContributionSchema,216}217});218219220221