Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugToolCallContentRenderer.ts
13406 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 * as DOM from '../../../../../base/browser/dom.js';
7
import { Button } from '../../../../../base/browser/ui/button/button.js';
8
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
9
import { createTrustedTypesPolicy } from '../../../../../base/browser/trustedTypes.js';
10
import { Codicon } from '../../../../../base/common/codicons.js';
11
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
12
import { localize } from '../../../../../nls.js';
13
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
14
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
15
import { tokenizeToString } from '../../../../../editor/common/languages/textToHtmlTokenizer.js';
16
import { IChatDebugEventToolCallContent } from '../../common/chatDebugService.js';
17
import { setupCollapsibleToggle } from './chatDebugCollapsible.js';
18
19
const $ = DOM.$;
20
21
const _ttpPolicy = createTrustedTypesPolicy('chatDebugTokenizer', {
22
createHTML(html: string) {
23
return html;
24
}
25
});
26
27
export function tryParseJSON(text: string): { parsed: unknown; isJSON: true } | { isJSON: false } {
28
try {
29
return { parsed: JSON.parse(text), isJSON: true };
30
} catch {
31
return { isJSON: false };
32
}
33
}
34
35
/**
36
* Format and syntax-highlight a content string.
37
* When the content is valid JSON it is pretty-printed and tokenized as JSON;
38
* otherwise it is tokenized as markdown.
39
*/
40
export async function tokenizeContent(text: string, languageService: ILanguageService): Promise<{ plainText: string; tokenizedHtml: string }> {
41
const result = tryParseJSON(text);
42
const plainText = result.isJSON ? JSON.stringify(result.parsed, null, 2) : text;
43
const language = result.isJSON ? 'json' : 'markdown';
44
const tokenizedHtml = await tokenizeToString(languageService, plainText, language);
45
return { plainText, tokenizedHtml };
46
}
47
48
/**
49
* Render a collapsible section. When `tokenizedHtml` is provided the content
50
* is rendered as syntax-highlighted HTML; otherwise plain-text is used.
51
* Optionally adds a copy button when `clipboardService` is provided.
52
*/
53
export function renderSection(
54
parent: HTMLElement,
55
label: string,
56
plainText: string,
57
tokenizedHtml: string | undefined,
58
disposables: DisposableStore,
59
initiallyCollapsed: boolean = false,
60
clipboardService?: IClipboardService,
61
scrollable?: { scanDomNode(): void },
62
): void {
63
const sectionEl = DOM.append(parent, $('div.chat-debug-message-section'));
64
const header = DOM.append(sectionEl, $('div.chat-debug-message-section-header'));
65
const chevron = DOM.append(header, $('span.chat-debug-message-section-chevron'));
66
DOM.append(header, $('span.chat-debug-message-section-title', undefined, label));
67
68
if (clipboardService) {
69
const copyBtn = disposables.add(new Button(header, {
70
title: localize('chatDebug.section.copy', "Copy"),
71
ariaLabel: localize('chatDebug.section.copy', "Copy"),
72
hoverDelegate: getDefaultHoverDelegate('mouse'),
73
}));
74
copyBtn.icon = Codicon.copy;
75
copyBtn.element.classList.add('chat-debug-section-copy-btn');
76
disposables.add(DOM.addDisposableListener(copyBtn.element, DOM.EventType.MOUSE_ENTER, () => {
77
header.classList.add('chat-debug-section-copy-header-passthrough');
78
}));
79
disposables.add(DOM.addDisposableListener(copyBtn.element, DOM.EventType.MOUSE_LEAVE, () => {
80
header.classList.remove('chat-debug-section-copy-header-passthrough');
81
}));
82
disposables.add(copyBtn.onDidClick(e => {
83
if (e) {
84
DOM.EventHelper.stop(e, true);
85
}
86
clipboardService.writeText(plainText);
87
}));
88
}
89
90
const wrapper = DOM.append(sectionEl, $('div.chat-debug-message-section-content-wrapper'));
91
const contentEl = DOM.append(wrapper, $('pre.chat-debug-message-section-content'));
92
contentEl.tabIndex = 0;
93
94
if (tokenizedHtml) {
95
const trustedHtml = _ttpPolicy?.createHTML(tokenizedHtml) ?? tokenizedHtml;
96
contentEl.innerHTML = trustedHtml as string;
97
} else {
98
contentEl.textContent = plainText;
99
}
100
101
setupCollapsibleToggle(chevron, header, wrapper, disposables, initiallyCollapsed, scrollable);
102
}
103
104
/**
105
* Render a resolved tool call content with structured sections for
106
* tool name, status, duration, arguments, and output.
107
* Reuses the existing message content and collapsible section components.
108
* When JSON is detected in input/output, renders it with syntax highlighting
109
* using the editor's tokenization.
110
*/
111
export async function renderToolCallContent(content: IChatDebugEventToolCallContent, languageService: ILanguageService, clipboardService?: IClipboardService, scrollable?: { scanDomNode(): void }): Promise<{ element: HTMLElement; disposables: DisposableStore }> {
112
const disposables = new DisposableStore();
113
const container = $('div.chat-debug-message-content');
114
container.tabIndex = 0;
115
116
// Header: tool name
117
DOM.append(container, $('div.chat-debug-message-content-title', undefined, content.toolName));
118
119
// Status summary line
120
const statusParts: string[] = [];
121
if (content.result) {
122
statusParts.push(content.result === 'success'
123
? localize('chatDebug.toolCall.success', "Success")
124
: localize('chatDebug.toolCall.error', "Error"));
125
}
126
if (content.durationInMillis !== undefined) {
127
statusParts.push(localize('chatDebug.toolCall.duration', "{0}ms", content.durationInMillis));
128
}
129
if (statusParts.length > 0) {
130
DOM.append(container, $('div.chat-debug-message-content-summary', undefined, statusParts.join(' \u00b7 ')));
131
}
132
133
// Build collapsible sections for arguments and output
134
const sectionsContainer = DOM.append(container, $('div.chat-debug-message-sections'));
135
136
if (content.input) {
137
const { plainText, tokenizedHtml } = await tokenizeContent(content.input, languageService);
138
renderSection(sectionsContainer, localize('chatDebug.toolCall.arguments', "Arguments"), plainText, tokenizedHtml, disposables, false, clipboardService, scrollable);
139
}
140
141
if (content.output) {
142
const { plainText, tokenizedHtml } = await tokenizeContent(content.output, languageService);
143
renderSection(sectionsContainer, localize('chatDebug.toolCall.output', "Output"), plainText, tokenizedHtml, disposables, false, clipboardService, scrollable);
144
}
145
146
return { element: container, disposables };
147
}
148
149
/**
150
* Convert a resolved tool call content to plain text for clipboard / editor output.
151
*/
152
export function toolCallContentToPlainText(content: IChatDebugEventToolCallContent): string {
153
const lines: string[] = [];
154
lines.push(localize('chatDebug.toolCall.toolLabel', "Tool: {0}", content.toolName));
155
156
if (content.result) {
157
lines.push(localize('chatDebug.toolCall.statusLabel', "Status: {0}", content.result));
158
}
159
if (content.durationInMillis !== undefined) {
160
lines.push(localize('chatDebug.toolCall.durationLabel', "Duration: {0}ms", content.durationInMillis));
161
}
162
163
if (content.input) {
164
lines.push('');
165
lines.push(`[${localize('chatDebug.toolCall.arguments', "Arguments")}]`);
166
try {
167
const parsed = JSON.parse(content.input);
168
lines.push(JSON.stringify(parsed, null, 2));
169
} catch {
170
lines.push(content.input);
171
}
172
}
173
174
if (content.output) {
175
lines.push('');
176
lines.push(`[${localize('chatDebug.toolCall.output', "Output")}]`);
177
try {
178
const parsed = JSON.parse(content.output);
179
lines.push(JSON.stringify(parsed, null, 2));
180
} catch {
181
lines.push(content.output);
182
}
183
}
184
185
return lines.join('\n');
186
}
187
188