Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugDetailPanel.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 { Orientation, Sash, SashState } from '../../../../../base/browser/ui/sash/sash.js';
9
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
10
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';
11
import { Codicon } from '../../../../../base/common/codicons.js';
12
import { Emitter } from '../../../../../base/common/event.js';
13
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
14
import { localize } from '../../../../../nls.js';
15
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
16
import { IModelService } from '../../../../../editor/common/services/model.js';
17
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
18
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
19
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
20
import { ILabelService } from '../../../../../platform/label/common/label.js';
21
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
22
import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js';
23
import { IEditorService } from '../../../../services/editor/common/editorService.js';
24
import { IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js';
25
import { formatEventDetail } from './chatDebugEventDetailRenderer.js';
26
import { renderCustomizationDiscoveryContent, fileListToPlainText, renderCustomizationSummaryContent, customizationSummaryToPlainText } from './chatCustomizationDiscoveryRenderer.js';
27
import { renderUserMessageContent, renderAgentResponseContent, messageEventToPlainText, renderResolvedMessageContent, resolvedMessageToPlainText } from './chatDebugMessageContentRenderer.js';
28
import { renderToolCallContent, toolCallContentToPlainText } from './chatDebugToolCallContentRenderer.js';
29
import { renderModelTurnContent, modelTurnContentToPlainText } from './chatDebugModelTurnContentRenderer.js';
30
import { renderHookContent, hookContentToPlainText } from './chatDebugHookContentRenderer.js';
31
32
const $ = DOM.$;
33
34
const DETAIL_PANEL_DEFAULT_WIDTH = 350;
35
const DETAIL_PANEL_MIN_WIDTH = 200;
36
const DETAIL_PANEL_MAX_WIDTH = 800;
37
38
/**
39
* Reusable detail panel that resolves and displays the content of a
40
* single {@link IChatDebugEvent}. Used by both the logs view and the
41
* flow chart view.
42
*/
43
export class ChatDebugDetailPanel extends Disposable {
44
45
private readonly _onDidHide = this._register(new Emitter<void>());
46
readonly onDidHide = this._onDidHide.event;
47
48
private readonly _onDidChangeWidth = this._register(new Emitter<number>());
49
readonly onDidChangeWidth = this._onDidChangeWidth.event;
50
51
readonly element: HTMLElement;
52
private readonly contentContainer: HTMLElement;
53
private readonly scrollable: DomScrollableElement;
54
private readonly sash: Sash;
55
private headerElement: HTMLElement | undefined;
56
private readonly detailDisposables = this._register(new DisposableStore());
57
private currentDetailText: string = '';
58
private currentDetailEventId: string | undefined;
59
private firstFocusableElement: HTMLElement | undefined;
60
private _width: number = DETAIL_PANEL_DEFAULT_WIDTH;
61
62
get width(): number {
63
return this._width;
64
}
65
66
constructor(
67
parent: HTMLElement,
68
@IChatDebugService private readonly chatDebugService: IChatDebugService,
69
@IInstantiationService private readonly instantiationService: IInstantiationService,
70
@IEditorService private readonly editorService: IEditorService,
71
@IClipboardService private readonly clipboardService: IClipboardService,
72
@IHoverService private readonly hoverService: IHoverService,
73
@IOpenerService private readonly openerService: IOpenerService,
74
@ILanguageService private readonly languageService: ILanguageService,
75
) {
76
super();
77
this.element = DOM.append(parent, $('.chat-debug-detail-panel'));
78
this.contentContainer = $('.chat-debug-detail-content');
79
this.scrollable = this._register(new DomScrollableElement(this.contentContainer, {
80
horizontal: ScrollbarVisibility.Hidden,
81
vertical: ScrollbarVisibility.Auto,
82
}));
83
this.element.style.width = `${this._width}px`;
84
DOM.hide(this.element);
85
86
// Sash on the parent container, positioned at the left edge of the detail panel
87
this.sash = this._register(new Sash(parent, {
88
getVerticalSashLeft: () => parent.offsetWidth - this._width,
89
}, { orientation: Orientation.VERTICAL }));
90
this.sash.state = SashState.Disabled;
91
92
let sashStartWidth: number | undefined;
93
this._register(this.sash.onDidStart(() => sashStartWidth = this._width));
94
this._register(this.sash.onDidEnd(() => {
95
sashStartWidth = undefined;
96
this.sash.layout();
97
}));
98
this._register(this.sash.onDidChange(e => {
99
if (sashStartWidth === undefined) {
100
return;
101
}
102
// Dragging left (negative currentX delta) should increase width
103
const delta = e.startX - e.currentX;
104
const newWidth = Math.max(DETAIL_PANEL_MIN_WIDTH, Math.min(DETAIL_PANEL_MAX_WIDTH, sashStartWidth + delta));
105
this._width = newWidth;
106
this.element.style.width = `${newWidth}px`;
107
this.sash.layout();
108
this._onDidChangeWidth.fire(newWidth);
109
}));
110
111
// Handle Ctrl+A / Cmd+A to select all within the detail panel
112
this._register(DOM.addDisposableListener(this.element, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
113
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
114
const target = e.target as HTMLElement | null;
115
if (target && this.element.contains(target)) {
116
e.preventDefault();
117
const targetWindow = DOM.getWindow(target);
118
const selection = targetWindow.getSelection();
119
if (selection) {
120
const range = targetWindow.document.createRange();
121
range.selectNodeContents(target);
122
selection.removeAllRanges();
123
selection.addRange(range);
124
}
125
}
126
}
127
}));
128
}
129
130
async show(event: IChatDebugEvent): Promise<void> {
131
// Skip re-rendering if we're already showing this event's detail
132
if (event.id && event.id === this.currentDetailEventId) {
133
return;
134
}
135
this.currentDetailEventId = event.id;
136
137
const resolved = event.id ? await this.chatDebugService.resolveEvent(event.id) : undefined;
138
139
DOM.show(this.element);
140
this.sash.state = SashState.Enabled;
141
this.sash.layout();
142
DOM.clearNode(this.element);
143
DOM.clearNode(this.contentContainer);
144
this.detailDisposables.clear();
145
146
// Header with action buttons
147
const header = DOM.append(this.element, $('.chat-debug-detail-header'));
148
this.headerElement = header;
149
this.element.appendChild(this.scrollable.getDomNode());
150
151
const fullScreenButton = this.detailDisposables.add(new Button(header, { ariaLabel: localize('chatDebug.openInEditor', "Open in Editor"), title: localize('chatDebug.openInEditor', "Open in Editor") }));
152
fullScreenButton.element.classList.add('chat-debug-detail-button');
153
fullScreenButton.icon = Codicon.goToFile;
154
this.firstFocusableElement = fullScreenButton.element;
155
this.detailDisposables.add(fullScreenButton.onDidClick(() => {
156
this.editorService.openEditor({ contents: this.currentDetailText, resource: undefined } satisfies IUntitledTextResourceEditorInput);
157
}));
158
159
const copyButton = this.detailDisposables.add(new Button(header, { ariaLabel: localize('chatDebug.copyToClipboard', "Copy"), title: localize('chatDebug.copyToClipboard', "Copy") }));
160
copyButton.element.classList.add('chat-debug-detail-button');
161
copyButton.icon = Codicon.copy;
162
this.detailDisposables.add(copyButton.onDidClick(() => {
163
this.clipboardService.writeText(this.currentDetailText);
164
}));
165
166
const closeButton = this.detailDisposables.add(new Button(header, { ariaLabel: localize('chatDebug.closeDetail', "Close"), title: localize('chatDebug.closeDetail', "Close") }));
167
closeButton.element.classList.add('chat-debug-detail-button');
168
closeButton.icon = Codicon.close;
169
this.detailDisposables.add(closeButton.onDidClick(() => {
170
this.hide();
171
}));
172
173
if (resolved && resolved.kind === 'fileList') {
174
this.currentDetailText = fileListToPlainText(resolved);
175
const { element: contentEl, disposables: contentDisposables } = this.instantiationService.invokeFunction(accessor =>
176
renderCustomizationDiscoveryContent(resolved, this.openerService, accessor.get(IModelService), this.languageService, this.hoverService, accessor.get(ILabelService), this.scrollable)
177
);
178
this.detailDisposables.add(contentDisposables);
179
this.contentContainer.appendChild(contentEl);
180
} else if (resolved && resolved.kind === 'customizationSummary') {
181
this.currentDetailText = customizationSummaryToPlainText(resolved);
182
const { element: contentEl, disposables: contentDisposables } = this.instantiationService.invokeFunction(accessor =>
183
renderCustomizationSummaryContent(resolved, this.openerService, accessor.get(IModelService), this.languageService, this.hoverService, accessor.get(ILabelService), this.scrollable)
184
);
185
this.detailDisposables.add(contentDisposables);
186
this.contentContainer.appendChild(contentEl);
187
} else if (resolved && resolved.kind === 'toolCall') {
188
this.currentDetailText = toolCallContentToPlainText(resolved);
189
const { element: contentEl, disposables: contentDisposables } = await renderToolCallContent(resolved, this.languageService, this.clipboardService, this.scrollable);
190
if (this.currentDetailEventId !== event.id) {
191
// Another event was selected while we were rendering
192
contentDisposables.dispose();
193
return;
194
}
195
this.detailDisposables.add(contentDisposables);
196
this.contentContainer.appendChild(contentEl);
197
} else if (resolved && resolved.kind === 'message') {
198
this.currentDetailText = resolvedMessageToPlainText(resolved);
199
const { element: contentEl, disposables: contentDisposables } = await renderResolvedMessageContent(resolved, this.languageService, this.clipboardService, this.scrollable);
200
if (this.currentDetailEventId !== event.id) {
201
contentDisposables.dispose();
202
return;
203
}
204
this.detailDisposables.add(contentDisposables);
205
this.contentContainer.appendChild(contentEl);
206
} else if (resolved && resolved.kind === 'modelTurn') {
207
this.currentDetailText = modelTurnContentToPlainText(resolved);
208
const { element: contentEl, disposables: contentDisposables } = await renderModelTurnContent(resolved, this.languageService, this.clipboardService, this.scrollable);
209
if (this.currentDetailEventId !== event.id) {
210
// Another event was selected while we were rendering
211
contentDisposables.dispose();
212
return;
213
}
214
this.detailDisposables.add(contentDisposables);
215
this.contentContainer.appendChild(contentEl);
216
} else if (resolved && resolved.kind === 'hook') {
217
this.currentDetailText = hookContentToPlainText(resolved);
218
const { element: contentEl, disposables: contentDisposables } = await renderHookContent(resolved, this.languageService, this.clipboardService, this.scrollable);
219
if (this.currentDetailEventId !== event.id) {
220
// Another event was selected while we were rendering
221
contentDisposables.dispose();
222
return;
223
}
224
this.detailDisposables.add(contentDisposables);
225
this.contentContainer.appendChild(contentEl);
226
} else if (event.kind === 'userMessage') {
227
this.currentDetailText = messageEventToPlainText(event);
228
const { element: contentEl, disposables: contentDisposables } = await renderUserMessageContent(event, this.languageService, this.clipboardService, this.scrollable);
229
if (this.currentDetailEventId !== event.id) {
230
contentDisposables.dispose();
231
return;
232
}
233
this.detailDisposables.add(contentDisposables);
234
this.contentContainer.appendChild(contentEl);
235
} else if (event.kind === 'agentResponse') {
236
this.currentDetailText = messageEventToPlainText(event);
237
const { element: contentEl, disposables: contentDisposables } = await renderAgentResponseContent(event, this.languageService, this.clipboardService, this.scrollable);
238
if (this.currentDetailEventId !== event.id) {
239
contentDisposables.dispose();
240
return;
241
}
242
this.detailDisposables.add(contentDisposables);
243
this.contentContainer.appendChild(contentEl);
244
} else {
245
const pre = DOM.append(this.contentContainer, $('pre'));
246
pre.tabIndex = 0;
247
if (resolved) {
248
this.currentDetailText = resolved.value;
249
} else {
250
this.currentDetailText = formatEventDetail(event);
251
}
252
pre.textContent = this.currentDetailText;
253
}
254
255
// Compute height from the parent container and set explicit
256
// dimensions so the scrollable element can show proper scrollbars.
257
const parentHeight = this.element.parentElement?.clientHeight ?? 0;
258
if (parentHeight > 0) {
259
this.layout(parentHeight);
260
} else {
261
this.scrollable.scanDomNode();
262
}
263
}
264
265
get isVisible(): boolean {
266
return this.element.style.display !== 'none';
267
}
268
269
focus(): void {
270
this.firstFocusableElement?.focus();
271
}
272
273
/**
274
* Set explicit dimensions on the scrollable element so the scrollbar
275
* can compute its size. Call after the panel is shown and whenever
276
* the available space changes.
277
*/
278
layout(height: number): void {
279
const headerHeight = this.headerElement?.offsetHeight ?? 0;
280
const scrollableHeight = Math.max(0, height - headerHeight);
281
// Preserve scroll position across layout changes (e.g. when opening
282
// an editor causes the workbench to re-layout this panel).
283
const scrollPos = this.scrollable.getScrollPosition();
284
this.contentContainer.style.height = `${scrollableHeight}px`;
285
this.scrollable.scanDomNode();
286
this.scrollable.setScrollPosition({ scrollTop: scrollPos.scrollTop });
287
this.sash.layout();
288
}
289
290
layoutSash(): void {
291
this.sash.layout();
292
}
293
294
hide(): void {
295
this.currentDetailEventId = undefined;
296
this.firstFocusableElement = undefined;
297
this.headerElement = undefined;
298
DOM.hide(this.element);
299
this.sash.state = SashState.Disabled;
300
DOM.clearNode(this.element);
301
DOM.clearNode(this.contentContainer);
302
this.detailDisposables.clear();
303
this._onDidHide.fire();
304
}
305
}
306
307