Path: blob/main/extensions/mermaid-chat-features/src/chatOutputRenderer.ts
5223 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*--------------------------------------------------------------------------------------------*/4import * as vscode from 'vscode';5import { MermaidEditorManager } from './editorManager';6import { MermaidWebviewManager } from './webviewManager';7import { escapeHtmlText } from './util/html';8import { generateUuid } from './util/uuid';9import { disposeAll } from './util/dispose';1011/**12* Mime type used to identify Mermaid diagram data in chat output.13*/14const mime = 'text/vnd.mermaid';1516/**17* View type that uniquely identifies the Mermaid chat output renderer.18*/19const viewType = 'vscode.chat-mermaid-features.chatOutputItem';2021class MermaidChatOutputRenderer implements vscode.ChatOutputRenderer {2223constructor(24private readonly _extensionUri: vscode.Uri,25private readonly _webviewManager: MermaidWebviewManager26) { }2728async renderChatOutput({ value }: vscode.ChatOutputDataItem, chatOutputWebview: vscode.ChatOutputWebview, _ctx: unknown, _token: vscode.CancellationToken): Promise<void> {29const webview = chatOutputWebview.webview;30const decoded = decodeMermaidData(value);31const mermaidSource = decoded.source;32const title = decoded.title;3334// Generate unique ID for this webview35const webviewId = generateUuid();3637const disposables: vscode.Disposable[] = [];3839// Register and set as active40disposables.push(this._webviewManager.registerWebview(webviewId, webview, mermaidSource, title, 'chat'));4142// Listen for messages from the webview43disposables.push(webview.onDidReceiveMessage(message => {44if (message.type === 'openInEditor') {45vscode.commands.executeCommand('_mermaid-chat.openInEditor', { mermaidWebviewId: webviewId });46}47}));4849// Dispose resources when webview is disposed50chatOutputWebview.onDidDispose(() => {51disposeAll(disposables);52});5354// Set the options for the webview55const mediaRoot = vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out');56webview.options = {57enableScripts: true,58localResourceRoots: [mediaRoot],59};6061// Set the HTML content for the webview62const nonce = generateUuid();63const mermaidScript = vscode.Uri.joinPath(mediaRoot, 'index.js');64const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'codicon.css'));6566webview.html = `67<!DOCTYPE html>68<html lang="en">6970<head>71<meta charset="UTF-8">72<meta name="viewport" content="width=device-width, initial-scale=1.0">73<title>Mermaid Diagram</title>74<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline'; font-src data:;" />75<link rel="stylesheet" type="text/css" href="${codiconsUri}">7677<style>78body {79padding: 0;80}81.mermaid {82visibility: hidden;83}84.mermaid.rendered {85visibility: visible;86}87.open-in-editor-btn {88position: absolute;89top: 8px;90right: 8px;91display: flex;92align-items: center;93justify-content: center;94width: 26px;95height: 26px;96background: var(--vscode-editorWidget-background);97color: var(--vscode-icon-foreground);98border: 1px solid var(--vscode-editorWidget-border);99border-radius: 6px;100cursor: pointer;101z-index: 100;102opacity: 0;103transition: opacity 0.2s;104}105body:hover .open-in-editor-btn {106opacity: 1;107}108.open-in-editor-btn:hover {109opacity: 1;110background: var(--vscode-toolbar-hoverBackground);111}112</style>113</head>114115<body data-vscode-context='${JSON.stringify({ preventDefaultContextMenuItems: true, mermaidWebviewId: webviewId })}' data-vscode-mermaid-webview-id="${webviewId}">116<button class="open-in-editor-btn" title="${vscode.l10n.t('Open in Editor')}"><i class="codicon codicon-open-preview"></i></button>117<pre class="mermaid">118${escapeHtmlText(mermaidSource)}119</pre>120121<script type="module" nonce="${nonce}" src="${webview.asWebviewUri(mermaidScript)}"></script>122</body>123</html>`;124}125}126127128export function registerChatSupport(129context: vscode.ExtensionContext,130webviewManager: MermaidWebviewManager,131editorManager: MermaidEditorManager132): vscode.Disposable {133const disposables: vscode.Disposable[] = [];134135disposables.push(136vscode.commands.registerCommand('_mermaid-chat.openInEditor', (ctx?: { mermaidWebviewId?: string }) => {137const webviewInfo = ctx?.mermaidWebviewId ? webviewManager.getWebview(ctx.mermaidWebviewId) : webviewManager.activeWebview;138if (webviewInfo) {139editorManager.openPreview(webviewInfo.mermaidSource, webviewInfo.title);140}141})142);143144// Register lm tools145disposables.push(146vscode.lm.registerTool<{ markup: string; title?: string }>('renderMermaidDiagram', {147invoke: async (options, _token) => {148const sourceCode = options.input.markup;149const title = options.input.title;150return writeMermaidToolOutput(sourceCode, title);151},152})153);154155// Register the chat output renderer for Mermaid diagrams.156// This will be invoked with the data generated by the tools.157// It can also be invoked when rendering old Mermaid diagrams in the chat history.158const renderer = new MermaidChatOutputRenderer(context.extensionUri, webviewManager);159disposables.push(vscode.chat.registerChatOutputRenderer(viewType, renderer));160161return vscode.Disposable.from(...disposables);162}163164function writeMermaidToolOutput(sourceCode: string, title: string | undefined): vscode.LanguageModelToolResult {165// Expose the source code as a markdown mermaid code block166const fence = getFenceForContent(sourceCode);167const result = new vscode.LanguageModelToolResult([168new vscode.LanguageModelTextPart(`${fence}mermaid\n${sourceCode}\n${fence}`)169]);170171// And store custom data in the tool result details to indicate that a custom renderer should be used for it.172// Encode source and optional title as JSON.173const data = JSON.stringify({ source: sourceCode, title });174// Add cast to use proposed API175(result as vscode.ExtendedLanguageModelToolResult2).toolResultDetails2 = {176mime,177value: new TextEncoder().encode(data),178};179180return result;181}182183function getFenceForContent(content: string): string {184const backtickMatch = content.matchAll(/`+/g);185if (!backtickMatch) {186return '```';187}188189const maxBackticks = Math.max(...Array.from(backtickMatch, s => s[0].length));190return '`'.repeat(Math.max(3, maxBackticks + 1));191}192193interface MermaidData {194readonly title: string | undefined;195readonly source: string;196}197198function decodeMermaidData(value: Uint8Array): MermaidData {199const text = new TextDecoder().decode(value);200201// Try to parse as JSON (new format with title), fall back to plain text (legacy format)202try {203const parsed = JSON.parse(text);204if (typeof parsed === 'object' && typeof parsed.source === 'string') {205return { title: parsed.title, source: parsed.source };206}207} catch {208// Not JSON, treat as legacy plain text format209}210211return { title: undefined, source: text };212}213214215