Path: blob/main/src/vs/workbench/contrib/chat/browser/chatOutputItemRenderer.ts
3296 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 { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';12import { autorun } from '../../../../base/common/observable.js';13import { URI } from '../../../../base/common/uri.js';14import { generateUuid } from '../../../../base/common/uuid.js';15import * as nls from '../../../../nls.js';16import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';17import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';18import { IWebview, IWebviewService, WebviewContentPurpose } from '../../../contrib/webview/browser/webview.js';19import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';20import { ExtensionsRegistry, IExtensionPointUser } from '../../../services/extensions/common/extensionsRegistry.js';2122export interface IChatOutputItemRenderer {23renderOutputPart(mime: string, data: Uint8Array, webview: IWebview, token: CancellationToken): Promise<void>;24}2526interface RegisterOptions {27readonly extension?: {28readonly id: ExtensionIdentifier;29readonly location: URI;30};31}3233export const IChatOutputRendererService = createDecorator<IChatOutputRendererService>('chatOutputRendererService');3435export interface IChatOutputRendererService {36readonly _serviceBrand: undefined;3738registerRenderer(mime: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable;3940renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart>;41}4243export interface RenderedOutputPart extends IDisposable {44readonly onDidChangeHeight: Event<number>;45readonly webview: IWebview;4647reinitialize(): void;48}4950interface RenderOutputPartWebviewOptions {51readonly origin?: string;52}535455interface RendererEntry {56readonly renderer: IChatOutputItemRenderer;57readonly options: RegisterOptions;58}5960export class ChatOutputRendererService extends Disposable implements IChatOutputRendererService {61_serviceBrand: undefined;6263private readonly _contributions = new Map</*viewType*/ string, {64readonly mimes: readonly string[];65}>();6667private readonly _renderers = new Map</*viewType*/ string, RendererEntry>();6869constructor(70@IWebviewService private readonly _webviewService: IWebviewService,71@IExtensionService private readonly _extensionService: IExtensionService,72) {73super();7475this._register(chatOutputRenderContributionPoint.setHandler(extensions => {76this.updateContributions(extensions);77}));78}7980registerRenderer(viewType: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable {81this._renderers.set(viewType, { renderer, options });82return {83dispose: () => {84this._renderers.delete(viewType);85}86};87}8889async renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart> {90const rendererData = await this.getRenderer(mime, token);91if (token.isCancellationRequested) {92throw new CancellationError();93}9495if (!rendererData) {96throw new Error(`No renderer registered found for mime type: ${mime}`);97}9899const store = new DisposableStore();100101const webview = store.add(this._webviewService.createWebviewElement({102title: '',103origin: webviewOptions.origin ?? generateUuid(),104options: {105enableFindWidget: false,106purpose: WebviewContentPurpose.ChatOutputItem,107tryRestoreScrollPosition: false,108},109contentOptions: {},110extension: rendererData.options.extension ? rendererData.options.extension : undefined,111}));112113const onDidChangeHeight = store.add(new Emitter<number>());114store.add(autorun(reader => {115const height = reader.readObservable(webview.intrinsicContentSize);116if (height) {117onDidChangeHeight.fire(height.height);118parent.style.height = `${height.height}px`;119}120}));121122webview.mountTo(parent, getWindow(parent));123await rendererData.renderer.renderOutputPart(mime, data, webview, token);124125return {126get webview() { return webview; },127onDidChangeHeight: onDidChangeHeight.event,128dispose: () => {129store.dispose();130},131reinitialize: () => {132webview.reinitializeAfterDismount();133},134};135}136137private async getRenderer(mime: string, token: CancellationToken): Promise<RendererEntry | undefined> {138await raceCancellationError(this._extensionService.whenInstalledExtensionsRegistered(), token);139for (const [id, value] of this._contributions) {140if (value.mimes.some(m => matchesMimeType(m, [mime]))) {141await raceCancellationError(this._extensionService.activateByEvent(`onChatOutputRenderer:${id}`), token);142const rendererData = this._renderers.get(id);143if (rendererData) {144return rendererData;145}146}147}148149return undefined;150}151152private updateContributions(extensions: readonly IExtensionPointUser<readonly IChatOutputRendererContribution[]>[]) {153this._contributions.clear();154for (const extension of extensions) {155if (!isProposedApiEnabled(extension.description, 'chatOutputRenderer')) {156continue;157}158159for (const contribution of extension.value) {160if (this._contributions.has(contribution.viewType)) {161extension.collector.error(`Chat output renderer with view type '${contribution.viewType}' already registered`);162continue;163}164165this._contributions.set(contribution.viewType, {166mimes: contribution.mimeTypes,167});168}169}170}171}172173interface IChatOutputRendererContribution {174readonly viewType: string;175readonly mimeTypes: readonly string[];176}177178const chatOutputRenderContributionPoint = ExtensionsRegistry.registerExtensionPoint<IChatOutputRendererContribution[]>({179extensionPoint: 'chatOutputRenderers',180activationEventsGenerator: (contributions: IChatOutputRendererContribution[], result) => {181for (const contrib of contributions) {182result.push(`onChatOutputRenderer:${contrib.viewType}`);183}184},185jsonSchema: {186description: nls.localize('vscode.extension.contributes.chatOutputRenderer', 'Contributes a renderer for specific MIME types in chat outputs'),187type: 'array',188items: {189type: 'object',190additionalProperties: false,191required: ['viewType', 'mimeTypes'],192properties: {193viewType: {194type: 'string',195description: nls.localize('chatOutputRenderer.viewType', 'Unique identifier for the renderer.'),196},197mimeTypes: {198type: 'array',199description: nls.localize('chatOutputRenderer.mimeTypes', 'MIME types that this renderer can handle'),200items: {201type: 'string'202}203}204}205}206}207});208209210211