Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatOutputItemRenderer.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { getWindow } from '../../../../base/browser/dom.js';
7
import { raceCancellationError } from '../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../base/common/cancellation.js';
9
import { matchesMimeType } from '../../../../base/common/dataTransfer.js';
10
import { CancellationError } from '../../../../base/common/errors.js';
11
import { Emitter, Event } from '../../../../base/common/event.js';
12
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
13
import { autorun } from '../../../../base/common/observable.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { generateUuid } from '../../../../base/common/uuid.js';
16
import * as nls from '../../../../nls.js';
17
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
18
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
19
import { IWebview, IWebviewService, WebviewContentPurpose } from '../../../contrib/webview/browser/webview.js';
20
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
21
import { ExtensionsRegistry, IExtensionPointUser } from '../../../services/extensions/common/extensionsRegistry.js';
22
23
export interface IChatOutputItemRenderer {
24
renderOutputPart(mime: string, data: Uint8Array, webview: IWebview, token: CancellationToken): Promise<void>;
25
}
26
27
interface RegisterOptions {
28
readonly extension?: {
29
readonly id: ExtensionIdentifier;
30
readonly location: URI;
31
};
32
}
33
34
export const IChatOutputRendererService = createDecorator<IChatOutputRendererService>('chatOutputRendererService');
35
36
export interface IChatOutputRendererService {
37
readonly _serviceBrand: undefined;
38
39
registerRenderer(mime: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable;
40
41
renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart>;
42
}
43
44
export interface RenderedOutputPart extends IDisposable {
45
readonly onDidChangeHeight: Event<number>;
46
readonly webview: IWebview;
47
48
reinitialize(): void;
49
}
50
51
interface RenderOutputPartWebviewOptions {
52
readonly origin?: string;
53
}
54
55
56
interface RendererEntry {
57
readonly renderer: IChatOutputItemRenderer;
58
readonly options: RegisterOptions;
59
}
60
61
export class ChatOutputRendererService extends Disposable implements IChatOutputRendererService {
62
_serviceBrand: undefined;
63
64
private readonly _contributions = new Map</*viewType*/ string, {
65
readonly mimes: readonly string[];
66
}>();
67
68
private readonly _renderers = new Map</*viewType*/ string, RendererEntry>();
69
70
constructor(
71
@IWebviewService private readonly _webviewService: IWebviewService,
72
@IExtensionService private readonly _extensionService: IExtensionService,
73
) {
74
super();
75
76
this._register(chatOutputRenderContributionPoint.setHandler(extensions => {
77
this.updateContributions(extensions);
78
}));
79
}
80
81
registerRenderer(viewType: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable {
82
this._renderers.set(viewType, { renderer, options });
83
return {
84
dispose: () => {
85
this._renderers.delete(viewType);
86
}
87
};
88
}
89
90
async renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart> {
91
const rendererData = await this.getRenderer(mime, token);
92
if (token.isCancellationRequested) {
93
throw new CancellationError();
94
}
95
96
if (!rendererData) {
97
throw new Error(`No renderer registered found for mime type: ${mime}`);
98
}
99
100
const store = new DisposableStore();
101
102
const webview = store.add(this._webviewService.createWebviewElement({
103
title: '',
104
origin: webviewOptions.origin ?? generateUuid(),
105
options: {
106
enableFindWidget: false,
107
purpose: WebviewContentPurpose.ChatOutputItem,
108
tryRestoreScrollPosition: false,
109
},
110
contentOptions: {},
111
extension: rendererData.options.extension ? rendererData.options.extension : undefined,
112
}));
113
114
const onDidChangeHeight = store.add(new Emitter<number>());
115
store.add(autorun(reader => {
116
const height = reader.readObservable(webview.intrinsicContentSize);
117
if (height) {
118
onDidChangeHeight.fire(height.height);
119
parent.style.height = `${height.height}px`;
120
}
121
}));
122
123
webview.mountTo(parent, getWindow(parent));
124
await rendererData.renderer.renderOutputPart(mime, data, webview, token);
125
126
return {
127
get webview() { return webview; },
128
onDidChangeHeight: onDidChangeHeight.event,
129
dispose: () => {
130
store.dispose();
131
},
132
reinitialize: () => {
133
webview.reinitializeAfterDismount();
134
},
135
};
136
}
137
138
private async getRenderer(mime: string, token: CancellationToken): Promise<RendererEntry | undefined> {
139
await raceCancellationError(this._extensionService.whenInstalledExtensionsRegistered(), token);
140
for (const [id, value] of this._contributions) {
141
if (value.mimes.some(m => matchesMimeType(m, [mime]))) {
142
await raceCancellationError(this._extensionService.activateByEvent(`onChatOutputRenderer:${id}`), token);
143
const rendererData = this._renderers.get(id);
144
if (rendererData) {
145
return rendererData;
146
}
147
}
148
}
149
150
return undefined;
151
}
152
153
private updateContributions(extensions: readonly IExtensionPointUser<readonly IChatOutputRendererContribution[]>[]) {
154
this._contributions.clear();
155
for (const extension of extensions) {
156
if (!isProposedApiEnabled(extension.description, 'chatOutputRenderer')) {
157
continue;
158
}
159
160
for (const contribution of extension.value) {
161
if (this._contributions.has(contribution.viewType)) {
162
extension.collector.error(`Chat output renderer with view type '${contribution.viewType}' already registered`);
163
continue;
164
}
165
166
this._contributions.set(contribution.viewType, {
167
mimes: contribution.mimeTypes,
168
});
169
}
170
}
171
}
172
}
173
174
interface IChatOutputRendererContribution {
175
readonly viewType: string;
176
readonly mimeTypes: readonly string[];
177
}
178
179
const chatOutputRenderContributionPoint = ExtensionsRegistry.registerExtensionPoint<IChatOutputRendererContribution[]>({
180
extensionPoint: 'chatOutputRenderers',
181
activationEventsGenerator: (contributions: IChatOutputRendererContribution[], result) => {
182
for (const contrib of contributions) {
183
result.push(`onChatOutputRenderer:${contrib.viewType}`);
184
}
185
},
186
jsonSchema: {
187
description: nls.localize('vscode.extension.contributes.chatOutputRenderer', 'Contributes a renderer for specific MIME types in chat outputs'),
188
type: 'array',
189
items: {
190
type: 'object',
191
additionalProperties: false,
192
required: ['viewType', 'mimeTypes'],
193
properties: {
194
viewType: {
195
type: 'string',
196
description: nls.localize('chatOutputRenderer.viewType', 'Unique identifier for the renderer.'),
197
},
198
mimeTypes: {
199
type: 'array',
200
description: nls.localize('chatOutputRenderer.mimeTypes', 'MIME types that this renderer can handle'),
201
items: {
202
type: 'string'
203
}
204
}
205
}
206
}
207
}
208
});
209
210
211