Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugToolCallContentRenderer.ts
13406 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 * as DOM from '../../../../../base/browser/dom.js';6import { Button } from '../../../../../base/browser/ui/button/button.js';7import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';8import { createTrustedTypesPolicy } from '../../../../../base/browser/trustedTypes.js';9import { Codicon } from '../../../../../base/common/codicons.js';10import { DisposableStore } from '../../../../../base/common/lifecycle.js';11import { localize } from '../../../../../nls.js';12import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';13import { ILanguageService } from '../../../../../editor/common/languages/language.js';14import { tokenizeToString } from '../../../../../editor/common/languages/textToHtmlTokenizer.js';15import { IChatDebugEventToolCallContent } from '../../common/chatDebugService.js';16import { setupCollapsibleToggle } from './chatDebugCollapsible.js';1718const $ = DOM.$;1920const _ttpPolicy = createTrustedTypesPolicy('chatDebugTokenizer', {21createHTML(html: string) {22return html;23}24});2526export function tryParseJSON(text: string): { parsed: unknown; isJSON: true } | { isJSON: false } {27try {28return { parsed: JSON.parse(text), isJSON: true };29} catch {30return { isJSON: false };31}32}3334/**35* Format and syntax-highlight a content string.36* When the content is valid JSON it is pretty-printed and tokenized as JSON;37* otherwise it is tokenized as markdown.38*/39export async function tokenizeContent(text: string, languageService: ILanguageService): Promise<{ plainText: string; tokenizedHtml: string }> {40const result = tryParseJSON(text);41const plainText = result.isJSON ? JSON.stringify(result.parsed, null, 2) : text;42const language = result.isJSON ? 'json' : 'markdown';43const tokenizedHtml = await tokenizeToString(languageService, plainText, language);44return { plainText, tokenizedHtml };45}4647/**48* Render a collapsible section. When `tokenizedHtml` is provided the content49* is rendered as syntax-highlighted HTML; otherwise plain-text is used.50* Optionally adds a copy button when `clipboardService` is provided.51*/52export function renderSection(53parent: HTMLElement,54label: string,55plainText: string,56tokenizedHtml: string | undefined,57disposables: DisposableStore,58initiallyCollapsed: boolean = false,59clipboardService?: IClipboardService,60scrollable?: { scanDomNode(): void },61): void {62const sectionEl = DOM.append(parent, $('div.chat-debug-message-section'));63const header = DOM.append(sectionEl, $('div.chat-debug-message-section-header'));64const chevron = DOM.append(header, $('span.chat-debug-message-section-chevron'));65DOM.append(header, $('span.chat-debug-message-section-title', undefined, label));6667if (clipboardService) {68const copyBtn = disposables.add(new Button(header, {69title: localize('chatDebug.section.copy', "Copy"),70ariaLabel: localize('chatDebug.section.copy', "Copy"),71hoverDelegate: getDefaultHoverDelegate('mouse'),72}));73copyBtn.icon = Codicon.copy;74copyBtn.element.classList.add('chat-debug-section-copy-btn');75disposables.add(DOM.addDisposableListener(copyBtn.element, DOM.EventType.MOUSE_ENTER, () => {76header.classList.add('chat-debug-section-copy-header-passthrough');77}));78disposables.add(DOM.addDisposableListener(copyBtn.element, DOM.EventType.MOUSE_LEAVE, () => {79header.classList.remove('chat-debug-section-copy-header-passthrough');80}));81disposables.add(copyBtn.onDidClick(e => {82if (e) {83DOM.EventHelper.stop(e, true);84}85clipboardService.writeText(plainText);86}));87}8889const wrapper = DOM.append(sectionEl, $('div.chat-debug-message-section-content-wrapper'));90const contentEl = DOM.append(wrapper, $('pre.chat-debug-message-section-content'));91contentEl.tabIndex = 0;9293if (tokenizedHtml) {94const trustedHtml = _ttpPolicy?.createHTML(tokenizedHtml) ?? tokenizedHtml;95contentEl.innerHTML = trustedHtml as string;96} else {97contentEl.textContent = plainText;98}99100setupCollapsibleToggle(chevron, header, wrapper, disposables, initiallyCollapsed, scrollable);101}102103/**104* Render a resolved tool call content with structured sections for105* tool name, status, duration, arguments, and output.106* Reuses the existing message content and collapsible section components.107* When JSON is detected in input/output, renders it with syntax highlighting108* using the editor's tokenization.109*/110export async function renderToolCallContent(content: IChatDebugEventToolCallContent, languageService: ILanguageService, clipboardService?: IClipboardService, scrollable?: { scanDomNode(): void }): Promise<{ element: HTMLElement; disposables: DisposableStore }> {111const disposables = new DisposableStore();112const container = $('div.chat-debug-message-content');113container.tabIndex = 0;114115// Header: tool name116DOM.append(container, $('div.chat-debug-message-content-title', undefined, content.toolName));117118// Status summary line119const statusParts: string[] = [];120if (content.result) {121statusParts.push(content.result === 'success'122? localize('chatDebug.toolCall.success', "Success")123: localize('chatDebug.toolCall.error', "Error"));124}125if (content.durationInMillis !== undefined) {126statusParts.push(localize('chatDebug.toolCall.duration', "{0}ms", content.durationInMillis));127}128if (statusParts.length > 0) {129DOM.append(container, $('div.chat-debug-message-content-summary', undefined, statusParts.join(' \u00b7 ')));130}131132// Build collapsible sections for arguments and output133const sectionsContainer = DOM.append(container, $('div.chat-debug-message-sections'));134135if (content.input) {136const { plainText, tokenizedHtml } = await tokenizeContent(content.input, languageService);137renderSection(sectionsContainer, localize('chatDebug.toolCall.arguments', "Arguments"), plainText, tokenizedHtml, disposables, false, clipboardService, scrollable);138}139140if (content.output) {141const { plainText, tokenizedHtml } = await tokenizeContent(content.output, languageService);142renderSection(sectionsContainer, localize('chatDebug.toolCall.output', "Output"), plainText, tokenizedHtml, disposables, false, clipboardService, scrollable);143}144145return { element: container, disposables };146}147148/**149* Convert a resolved tool call content to plain text for clipboard / editor output.150*/151export function toolCallContentToPlainText(content: IChatDebugEventToolCallContent): string {152const lines: string[] = [];153lines.push(localize('chatDebug.toolCall.toolLabel', "Tool: {0}", content.toolName));154155if (content.result) {156lines.push(localize('chatDebug.toolCall.statusLabel', "Status: {0}", content.result));157}158if (content.durationInMillis !== undefined) {159lines.push(localize('chatDebug.toolCall.durationLabel', "Duration: {0}ms", content.durationInMillis));160}161162if (content.input) {163lines.push('');164lines.push(`[${localize('chatDebug.toolCall.arguments', "Arguments")}]`);165try {166const parsed = JSON.parse(content.input);167lines.push(JSON.stringify(parsed, null, 2));168} catch {169lines.push(content.input);170}171}172173if (content.output) {174lines.push('');175lines.push(`[${localize('chatDebug.toolCall.output', "Output")}]`);176try {177const parsed = JSON.parse(content.output);178lines.push(JSON.stringify(parsed, null, 2));179} catch {180lines.push(content.output);181}182}183184return lines.join('\n');185}186187188