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