Skip to content

Commit 17b6ed8

Browse files
authored
Merge pull request microsoft#257898 from mjbvz/inland-snake
Better align chat output renders api with existing apis
2 parents f5e06c1 + 0865841 commit 17b6ed8

File tree

7 files changed

+116
-51
lines changed

7 files changed

+116
-51
lines changed

src/vs/workbench/api/browser/mainThreadChatOutputRenderer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class MainThreadChatOutputRenderer extends Disposable implements MainThre
1818

1919
private _webviewHandlePool = 0;
2020

21-
private readonly registeredRenderers = new Map<string, IDisposable>();
21+
private readonly registeredRenderers = new Map</* viewType */ string, IDisposable>();
2222

2323
constructor(
2424
extHostContext: IExtHostContext,
@@ -36,23 +36,23 @@ export class MainThreadChatOutputRenderer extends Disposable implements MainThre
3636
this.registeredRenderers.clear();
3737
}
3838

39-
$registerChatOutputRenderer(mime: string, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void {
40-
this._rendererService.registerRenderer(mime, {
39+
$registerChatOutputRenderer(viewType: string, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void {
40+
this._rendererService.registerRenderer(viewType, {
4141
renderOutputPart: async (mime, data, webview, token) => {
4242
const webviewHandle = `chat-output-${++this._webviewHandlePool}`;
4343

4444
this._mainThreadWebview.addWebview(webviewHandle, webview, {
4545
serializeBuffersForPostMessage: true,
4646
});
4747

48-
this._proxy.$renderChatOutput(mime, VSBuffer.wrap(data), webviewHandle, token);
48+
this._proxy.$renderChatOutput(viewType, mime, VSBuffer.wrap(data), webviewHandle, token);
4949
},
5050
}, {
5151
extension: { id: extensionId, location: URI.revive(extensionLocation) }
5252
});
5353
}
5454

55-
$unregisterChatOutputRenderer(mime: string): void {
56-
this.registeredRenderers.get(mime)?.dispose();
55+
$unregisterChatOutputRenderer(viewType: string): void {
56+
this.registeredRenderers.get(viewType)?.dispose();
5757
}
5858
}

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,9 +1519,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
15191519
checkProposedApiEnabled(extension, 'chatSessionsProvider');
15201520
return extHostChatSessions.registerChatSessionContentProvider(extension, chatSessionType, provider);
15211521
},
1522-
registerChatOutputRenderer: (mime: string, renderer: vscode.ChatOutputRenderer) => {
1522+
registerChatOutputRenderer: (viewType: string, renderer: vscode.ChatOutputRenderer) => {
15231523
checkProposedApiEnabled(extension, 'chatOutputRenderer');
1524-
return extHostChatOutputRenderer.registerChatOutputRenderer(extension, mime, renderer);
1524+
return extHostChatOutputRenderer.registerChatOutputRenderer(extension, viewType, renderer);
15251525
},
15261526
};
15271527

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,12 +1455,12 @@ export interface ExtHostUriOpenersShape {
14551455
}
14561456

14571457
export interface MainThreadChatOutputRendererShape extends IDisposable {
1458-
$registerChatOutputRenderer(mime: string, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void;
1459-
$unregisterChatOutputRenderer(mime: string): void;
1458+
$registerChatOutputRenderer(viewType: string, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void;
1459+
$unregisterChatOutputRenderer(viewType: string): void;
14601460
}
14611461

14621462
export interface ExtHostChatOutputRendererShape {
1463-
$renderChatOutput(mime: string, valueData: VSBuffer, webviewHandle: string, token: CancellationToken): Promise<void>;
1463+
$renderChatOutput(viewType: string, mime: string, valueData: VSBuffer, webviewHandle: string, token: CancellationToken): Promise<void>;
14641464
}
14651465

14661466
export interface MainThreadProfileContentHandlersShape {

src/vs/workbench/api/common/extHostChatOutputRenderer.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class ExtHostChatOutputRenderer implements ExtHostChatOutputRendererShape
1515

1616
private readonly _proxy: MainThreadChatOutputRendererShape;
1717

18-
private readonly _renderers = new Map</*mime*/ string, {
18+
private readonly _renderers = new Map</*viewType*/ string, {
1919
readonly renderer: vscode.ChatOutputRenderer;
2020
readonly extension: IExtensionDescription;
2121
}>();
@@ -27,27 +27,27 @@ export class ExtHostChatOutputRenderer implements ExtHostChatOutputRendererShape
2727
this._proxy = mainContext.getProxy(MainContext.MainThreadChatOutputRenderer);
2828
}
2929

30-
registerChatOutputRenderer(extension: IExtensionDescription, mime: string, renderer: vscode.ChatOutputRenderer): vscode.Disposable {
31-
if (this._renderers.has(mime)) {
32-
throw new Error(`Chat output renderer already registered for mime type: ${mime}`);
30+
registerChatOutputRenderer(extension: IExtensionDescription, viewType: string, renderer: vscode.ChatOutputRenderer): vscode.Disposable {
31+
if (this._renderers.has(viewType)) {
32+
throw new Error(`Chat output renderer already registered for: ${viewType}`);
3333
}
3434

35-
this._renderers.set(mime, { extension, renderer });
36-
this._proxy.$registerChatOutputRenderer(mime, extension.identifier, extension.extensionLocation);
35+
this._renderers.set(viewType, { extension, renderer });
36+
this._proxy.$registerChatOutputRenderer(viewType, extension.identifier, extension.extensionLocation);
3737

3838
return new Disposable(() => {
39-
this._renderers.delete(mime);
40-
this._proxy.$unregisterChatOutputRenderer(mime);
39+
this._renderers.delete(viewType);
40+
this._proxy.$unregisterChatOutputRenderer(viewType);
4141
});
4242
}
4343

44-
async $renderChatOutput(mime: string, valueData: VSBuffer, webviewHandle: string, token: CancellationToken): Promise<void> {
45-
const entry = this._renderers.get(mime);
44+
async $renderChatOutput(viewType: string, mime: string, valueData: VSBuffer, webviewHandle: string, token: CancellationToken): Promise<void> {
45+
const entry = this._renderers.get(viewType);
4646
if (!entry) {
47-
throw new Error(`No chat output renderer registered for mime type: ${mime}`);
47+
throw new Error(`No chat output renderer registered for: ${viewType}`);
4848
}
4949

5050
const webview = this.webviews.createNewWebview(webviewHandle, {}, entry.extension);
51-
return entry.renderer.renderChatOutput(valueData.buffer, webview, {}, token);
51+
return entry.renderer.renderChatOutput(Object.freeze({ mime, value: valueData.buffer }), webview, {}, token);
5252
}
5353
}

src/vs/workbench/contrib/chat/browser/chatOutputItemRenderer.ts

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { getWindow } from '../../../../base/browser/dom.js';
7+
import { raceCancellationError } from '../../../../base/common/async.js';
78
import { CancellationToken } from '../../../../base/common/cancellation.js';
9+
import { matchesMimeType } from '../../../../base/common/dataTransfer.js';
10+
import { CancellationError } from '../../../../base/common/errors.js';
811
import { Emitter, Event } from '../../../../base/common/event.js';
912
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
1013
import { autorun } from '../../../../base/common/observable.js';
@@ -14,8 +17,8 @@ import * as nls from '../../../../nls.js';
1417
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
1518
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
1619
import { IWebview, IWebviewService, WebviewContentPurpose } from '../../../contrib/webview/browser/webview.js';
17-
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
18-
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
20+
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
21+
import { ExtensionsRegistry, IExtensionPointUser } from '../../../services/extensions/common/extensionsRegistry.js';
1922

2023
export interface IChatOutputItemRenderer {
2124
renderOutputPart(mime: string, data: Uint8Array, webview: IWebview, token: CancellationToken): Promise<void>;
@@ -50,37 +53,48 @@ interface RenderOutputPartWebviewOptions {
5053
}
5154

5255

56+
interface RendererEntry {
57+
readonly renderer: IChatOutputItemRenderer;
58+
readonly options: RegisterOptions;
59+
}
60+
5361
export class ChatOutputRendererService extends Disposable implements IChatOutputRendererService {
5462
_serviceBrand: undefined;
5563

56-
private readonly _renderers = new Map<string, {
57-
readonly renderer: IChatOutputItemRenderer;
58-
readonly options: RegisterOptions;
64+
private readonly _contributions = new Map</*viewType*/ string, {
65+
readonly mimes: readonly string[];
5966
}>();
6067

68+
private readonly _renderers = new Map</*viewType*/ string, RendererEntry>();
69+
6170
constructor(
6271
@IWebviewService private readonly _webviewService: IWebviewService,
6372
@IExtensionService private readonly _extensionService: IExtensionService,
6473
) {
6574
super();
75+
76+
this._register(chatOutputRenderContributionPoint.setHandler(extensions => {
77+
this.updateContributions(extensions);
78+
}));
6679
}
6780

68-
registerRenderer(mime: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable {
69-
this._renderers.set(mime, { renderer, options });
81+
registerRenderer(viewType: string, renderer: IChatOutputItemRenderer, options: RegisterOptions): IDisposable {
82+
this._renderers.set(viewType, { renderer, options });
7083
return {
7184
dispose: () => {
72-
this._renderers.delete(mime);
85+
this._renderers.delete(viewType);
7386
}
7487
};
7588
}
7689

7790
async renderOutputPart(mime: string, data: Uint8Array, parent: HTMLElement, webviewOptions: RenderOutputPartWebviewOptions, token: CancellationToken): Promise<RenderedOutputPart> {
78-
// Activate extensions that contribute to chatOutputRenderer for this mime type
79-
await this._extensionService.activateByEvent(`onChatOutputRenderer:${mime}`);
91+
const rendererData = await this.getRenderer(mime, token);
92+
if (token.isCancellationRequested) {
93+
throw new CancellationError();
94+
}
8095

81-
const rendererData = this._renderers.get(mime);
8296
if (!rendererData) {
83-
throw new Error(`No renderer registered for mime type: ${mime}`);
97+
throw new Error(`No renderer registered found for mime type: ${mime}`);
8498
}
8599

86100
const store = new DisposableStore();
@@ -120,19 +134,53 @@ export class ChatOutputRendererService extends Disposable implements IChatOutput
120134
},
121135
};
122136
}
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+
}
123172
}
124173

125174
interface IChatOutputRendererContribution {
175+
readonly viewType: string;
126176
readonly mimeTypes: readonly string[];
127177
}
128178

129-
ExtensionsRegistry.registerExtensionPoint<IChatOutputRendererContribution[]>({
130-
extensionPoint: 'chatOutputRenderer',
179+
const chatOutputRenderContributionPoint = ExtensionsRegistry.registerExtensionPoint<IChatOutputRendererContribution[]>({
180+
extensionPoint: 'chatOutputRenderers',
131181
activationEventsGenerator: (contributions: IChatOutputRendererContribution[], result) => {
132182
for (const contrib of contributions) {
133-
for (const mime of contrib.mimeTypes) {
134-
result.push(`onChatOutputRenderer:${mime}`);
135-
}
183+
result.push(`onChatOutputRenderer:${contrib.viewType}`);
136184
}
137185
},
138186
jsonSchema: {
@@ -141,11 +189,15 @@ ExtensionsRegistry.registerExtensionPoint<IChatOutputRendererContribution[]>({
141189
items: {
142190
type: 'object',
143191
additionalProperties: false,
144-
required: ['mimeTypes'],
192+
required: ['viewType', 'mimeTypes'],
145193
properties: {
194+
viewType: {
195+
type: 'string',
196+
description: nls.localize('chatOutputRenderer.viewType', 'Unique identifier for the renderer.'),
197+
},
146198
mimeTypes: {
147-
description: nls.localize('chatOutputRenderer.mimeTypes', 'MIME types that this renderer can handle'),
148199
type: 'array',
200+
description: nls.localize('chatOutputRenderer.mimeTypes', 'MIME types that this renderer can handle'),
149201
items: {
150202
type: 'string'
151203
}

src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export class ContributedCustomEditors extends Disposable {
3333
this.add(new CustomEditorInfo(info));
3434
}
3535

36-
customEditorsExtensionPoint.setHandler(extensions => {
36+
this._register(customEditorsExtensionPoint.setHandler(extensions => {
3737
this.update(extensions);
38-
});
38+
}));
3939
}
4040

4141
private readonly _onChange = this._register(new Emitter<void>());

src/vscode-dts/vscode.proposed.chatOutputRenderer.d.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,26 @@ declare module 'vscode' {
2929
toolResultDetails2?: Array<Uri | Location> | ToolResultDataOutput;
3030
}
3131

32+
/**
33+
* The data to be rendered by a {@link ChatOutputRenderer}.
34+
*/
35+
export interface ChatOutputDataItem {
36+
/**
37+
* The MIME type of the data.
38+
*/
39+
readonly mime: string;
40+
41+
/**
42+
* The contents of the data.
43+
*/
44+
readonly value: Uint8Array;
45+
}
46+
3247
export interface ChatOutputRenderer {
3348
/**
3449
* Given an output, render it into the provided webview.
3550
*
36-
* TODO:Should this take an object instead of Uint8Array? That would let you get the original mime. Useful
37-
* if we ever support registering for multiple mime types or using image/*.
38-
*
39-
* TODO: Figure out what to pass as context?
51+
* TODO: Figure out what to pass as context? Probably at least basic info such as chat location.
4052
*
4153
* @param data The data to render.
4254
* @param webview The webview to render the data into.
@@ -45,7 +57,7 @@ declare module 'vscode' {
4557
*
4658
* @returns A promise that resolves when the webview has been initialized and is ready to be presented to the user.
4759
*/
48-
renderChatOutput(data: Uint8Array, webview: Webview, ctx: {}, token: CancellationToken): Thenable<void>;
60+
renderChatOutput(data: ChatOutputDataItem, webview: Webview, ctx: {}, token: CancellationToken): Thenable<void>;
4961
}
5062

5163
export namespace chat {
@@ -59,15 +71,16 @@ declare module 'vscode' {
5971
* "contributes": {
6072
* "chatOutputRenderer": [
6173
* {
74+
* "viewType": "myExt.myChatOutputRenderer",
6275
* "mimeTypes": ["application/your-mime-type"]
6376
* }
6477
* ]
6578
* }
6679
* ```
6780
*
68-
* @param mime The MIME type of the output that this renderer can handle.
81+
* @param viewType Unique identifier for the renderer. This should match the `viewType` in your contribution point.
6982
* @param renderer The renderer to register.
7083
*/
71-
export function registerChatOutputRenderer(mime: string, renderer: ChatOutputRenderer): Disposable;
84+
export function registerChatOutputRenderer(viewType: string, renderer: ChatOutputRenderer): Disposable;
7285
}
7386
}

0 commit comments

Comments
 (0)