Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugOverviewView.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 { BreadcrumbsWidget } from '../../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';
8
import { Button } from '../../../../../base/browser/ui/button/button.js';
9
import { RunOnceScheduler } from '../../../../../base/common/async.js';
10
import { Codicon } from '../../../../../base/common/codicons.js';
11
import { Emitter } from '../../../../../base/common/event.js';
12
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
13
import { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { URI } from '../../../../../base/common/uri.js';
15
import { localize } from '../../../../../nls.js';
16
import { defaultBreadcrumbsWidgetStyles, defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
17
import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js';
18
import { safeIntl } from '../../../../../base/common/date.js';
19
import { IChatService } from '../../common/chatService/chatService.js';
20
import { ChatAgentLocation } from '../../common/constants.js';
21
import { IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
22
import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js';
23
import { IChatWidgetService } from '../chat.js';
24
import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem } from './chatDebugTypes.js';
25
26
const $ = DOM.$;
27
const numberFormatter = safeIntl.NumberFormat();
28
29
export const enum OverviewNavigation {
30
Home = 'home',
31
Logs = 'logs',
32
FlowChart = 'flowchart',
33
}
34
35
export class ChatDebugOverviewView extends Disposable {
36
37
private readonly _onNavigate = this._register(new Emitter<OverviewNavigation>());
38
readonly onNavigate = this._onNavigate.event;
39
40
readonly container: HTMLElement;
41
private readonly content: HTMLElement;
42
private readonly breadcrumbWidget: BreadcrumbsWidget;
43
private readonly loadDisposables = this._register(new DisposableStore());
44
45
private currentSessionResource: URI | undefined;
46
private metricsContainer: HTMLElement | undefined;
47
private isFirstLoad: boolean = true;
48
private readonly refreshScheduler: RunOnceScheduler;
49
50
constructor(
51
parent: HTMLElement,
52
@IChatService private readonly chatService: IChatService,
53
@IChatDebugService private readonly chatDebugService: IChatDebugService,
54
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
55
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
56
) {
57
super();
58
this.container = DOM.append(parent, $('.chat-debug-overview'));
59
DOM.hide(this.container);
60
61
this.refreshScheduler = this._register(new RunOnceScheduler(() => this.doRefresh(), 100));
62
63
// Breadcrumb
64
const breadcrumbContainer = DOM.append(this.container, $('.chat-debug-breadcrumb'));
65
this.breadcrumbWidget = this._register(new BreadcrumbsWidget(breadcrumbContainer, 3, undefined, Codicon.chevronRight, defaultBreadcrumbsWidgetStyles));
66
this._register(setupBreadcrumbKeyboardNavigation(breadcrumbContainer, this.breadcrumbWidget));
67
this._register(this.breadcrumbWidget.onDidSelectItem(e => {
68
if (e.type === 'select' && e.item instanceof TextBreadcrumbItem) {
69
this.breadcrumbWidget.setSelection(undefined);
70
const items = this.breadcrumbWidget.getItems();
71
const idx = items.indexOf(e.item);
72
if (idx === 0) {
73
this._onNavigate.fire(OverviewNavigation.Home);
74
}
75
}
76
}));
77
78
this.content = DOM.append(this.container, $('.chat-debug-overview-content'));
79
}
80
81
setSession(sessionResource: URI): void {
82
this.currentSessionResource = sessionResource;
83
this.isFirstLoad = true;
84
}
85
86
show(): void {
87
DOM.show(this.container);
88
this.load();
89
}
90
91
hide(): void {
92
DOM.hide(this.container);
93
this.refreshScheduler.cancel();
94
}
95
96
refresh(): void {
97
if (this.container.style.display !== 'none') {
98
if (!this.refreshScheduler.isScheduled()) {
99
this.refreshScheduler.schedule();
100
}
101
}
102
}
103
104
private doRefresh(): void {
105
// On refresh, only update the metrics section in-place
106
if (this.metricsContainer && this.currentSessionResource) {
107
DOM.clearNode(this.metricsContainer);
108
const events = this.chatDebugService.getEvents(this.currentSessionResource);
109
this.renderMetricsContent(this.metricsContainer, events);
110
this.isFirstLoad = false;
111
} else {
112
this.load();
113
}
114
}
115
116
updateBreadcrumb(): void {
117
if (!this.currentSessionResource) {
118
return;
119
}
120
const sessionTitle = this.chatService.getSessionTitle(this.currentSessionResource) || LocalChatSessionUri.parseLocalSessionId(this.currentSessionResource) || this.currentSessionResource.toString();
121
this.breadcrumbWidget.setItems([
122
new TextBreadcrumbItem(localize('chatDebug.title', "Agent Debug Logs"), true),
123
new TextBreadcrumbItem(sessionTitle),
124
]);
125
}
126
127
private load(): void {
128
DOM.clearNode(this.content);
129
this.loadDisposables.clear();
130
this.updateBreadcrumb();
131
132
if (!this.currentSessionResource) {
133
return;
134
}
135
136
const sessionTitle = this.chatService.getSessionTitle(this.currentSessionResource) || LocalChatSessionUri.parseLocalSessionId(this.currentSessionResource) || this.currentSessionResource.toString();
137
138
const titleRow = DOM.append(this.content, $('.chat-debug-overview-title-row'));
139
const titleEl = DOM.append(titleRow, $('h2.chat-debug-overview-title'));
140
DOM.append(titleEl, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`));
141
titleEl.append(sessionTitle);
142
143
const titleActions = DOM.append(titleRow, $('.chat-debug-overview-title-actions'));
144
145
const revealSessionBtn = this.loadDisposables.add(new Button(titleActions, { ariaLabel: localize('chatDebug.revealChatSession', "Reveal Chat Session"), title: localize('chatDebug.revealChatSession', "Reveal Chat Session") }));
146
revealSessionBtn.element.classList.add('chat-debug-icon-button');
147
revealSessionBtn.icon = Codicon.goToFile;
148
this.loadDisposables.add(revealSessionBtn.onDidClick(() => {
149
if (this.currentSessionResource) {
150
this.chatWidgetService.openSession(this.currentSessionResource);
151
}
152
}));
153
154
// Session details section
155
this.renderSessionDetails(this.currentSessionResource);
156
157
// Derived overview metrics — show shimmer only on the very first load
158
// AND when there are no events yet. If events were already streamed
159
// (e.g. while viewing logs), render them immediately so the shimmer
160
// doesn't get stuck forever waiting for an event that already fired.
161
const events = this.chatDebugService.getEvents(this.currentSessionResource);
162
this.renderDerivedOverview(events, this.isFirstLoad && events.length === 0);
163
this.isFirstLoad = false;
164
}
165
166
private renderSessionDetails(sessionUri: URI): void {
167
const model = this.chatService.getSession(sessionUri);
168
169
interface DetailItem { label: string; value: string }
170
const details: DetailItem[] = [];
171
172
// Session type
173
const sessionType = getChatSessionType(sessionUri);
174
const contribution = this.chatSessionsService.getChatSessionContribution(sessionType);
175
const sessionTypeName = contribution?.displayName || (sessionType === localChatSessionType
176
? localize('chatDebug.sessionType.local', "Local")
177
: sessionType);
178
details.push({ label: localize('chatDebug.detail.sessionType', "Session Type"), value: sessionTypeName });
179
180
if (model) {
181
const locationLabel = this.getLocationLabel(model.initialLocation);
182
details.push({ label: localize('chatDebug.detail.location', "Location"), value: locationLabel });
183
184
const inProgress = model.requestInProgress.get();
185
const statusLabel = inProgress
186
? localize('chatDebug.status.inProgress', "In Progress")
187
: localize('chatDebug.status.idle', "Idle");
188
details.push({ label: localize('chatDebug.detail.status', "Status"), value: statusLabel });
189
190
const timing = model.timing;
191
details.push({ label: localize('chatDebug.detail.created', "Created"), value: new Date(timing.created).toLocaleString() });
192
193
if (timing.lastRequestEnded) {
194
details.push({ label: localize('chatDebug.detail.lastActivity', "Last Activity"), value: new Date(timing.lastRequestEnded).toLocaleString() });
195
} else if (timing.lastRequestStarted) {
196
details.push({ label: localize('chatDebug.detail.lastActivity', "Last Activity"), value: new Date(timing.lastRequestStarted).toLocaleString() });
197
}
198
}
199
200
if (details.length > 0) {
201
const section = DOM.append(this.content, $('.chat-debug-overview-section'));
202
DOM.append(section, $('h3.chat-debug-overview-section-label', undefined, localize('chatDebug.sessionDetails', "Session Details")));
203
204
const detailsGrid = DOM.append(section, $('.chat-debug-overview-details'));
205
for (const detail of details) {
206
const row = DOM.append(detailsGrid, $('.chat-debug-overview-detail-row'));
207
DOM.append(row, $('span.chat-debug-overview-detail-label', undefined, detail.label));
208
DOM.append(row, $('span.chat-debug-overview-detail-value', undefined, detail.value));
209
}
210
}
211
}
212
213
private getLocationLabel(location: ChatAgentLocation): string {
214
switch (location) {
215
case ChatAgentLocation.Chat: return localize('chatDebug.location.chat', "Chat Panel");
216
case ChatAgentLocation.Terminal: return localize('chatDebug.location.terminal', "Terminal");
217
case ChatAgentLocation.Notebook: return localize('chatDebug.location.notebook', "Notebook");
218
case ChatAgentLocation.EditorInline: return localize('chatDebug.location.editor', "Editor Inline");
219
default: return String(location);
220
}
221
}
222
223
private renderDerivedOverview(events: readonly IChatDebugEvent[], showShimmer: boolean): void {
224
const metricsSection = DOM.append(this.content, $('.chat-debug-overview-section'));
225
DOM.append(metricsSection, $('h3.chat-debug-overview-section-label', undefined, localize('chatDebug.summary', "Summary")));
226
227
this.metricsContainer = DOM.append(metricsSection, $('.chat-debug-overview-metrics'));
228
229
if (showShimmer) {
230
this.renderMetricsShimmer(this.metricsContainer);
231
} else {
232
this.renderMetricsContent(this.metricsContainer, events);
233
}
234
235
// Explore actions
236
const actionsSection = DOM.append(this.content, $('.chat-debug-overview-section'));
237
DOM.append(actionsSection, $('h3.chat-debug-overview-section-label', undefined, localize('chatDebug.exploreTraceData', "Explore Trace Data")));
238
239
const row = DOM.append(actionsSection, $('.chat-debug-overview-actions'));
240
241
const viewLogsBtn = this.loadDisposables.add(new Button(row, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: localize('chatDebug.viewLogs', "View Logs") }));
242
viewLogsBtn.element.classList.add('chat-debug-overview-action-button');
243
viewLogsBtn.label = `$(list-flat) ${localize('chatDebug.viewLogs', "View Logs")}`;
244
this.loadDisposables.add(viewLogsBtn.onDidClick(() => {
245
this._onNavigate.fire(OverviewNavigation.Logs);
246
}));
247
248
const flowChartBtn = this.loadDisposables.add(new Button(row, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: localize('chatDebug.agentFlowChart', "Agent Flow Chart") }));
249
flowChartBtn.element.classList.add('chat-debug-overview-action-button');
250
flowChartBtn.label = `$(type-hierarchy) ${localize('chatDebug.agentFlowChart', "Agent Flow Chart")}`;
251
this.loadDisposables.add(flowChartBtn.onDidClick(() => {
252
this._onNavigate.fire(OverviewNavigation.FlowChart);
253
}));
254
255
}
256
257
private renderMetricsShimmer(container: HTMLElement): void {
258
// Show placeholder shimmer cards while provider data is loading
259
const placeholderLabels = [
260
localize('chatDebug.metric.modelTurns', "Model Turns"),
261
localize('chatDebug.metric.toolCalls', "Tool Calls"),
262
localize('chatDebug.metric.totalInputTokens', "Total Input Tokens"),
263
localize('chatDebug.metric.totalOutputTokens', "Total Output Tokens"),
264
localize('chatDebug.metric.totalCachedInputTokens', "Total Cached Input Tokens"),
265
localize('chatDebug.metric.totalTokens', "Total Tokens"),
266
localize('chatDebug.metric.errors', "Errors"),
267
];
268
for (const label of placeholderLabels) {
269
const card = DOM.append(container, $('.chat-debug-overview-metric-card'));
270
DOM.append(card, $('div.chat-debug-overview-metric-label', undefined, label));
271
const valueEl = DOM.append(card, $('div.chat-debug-overview-metric-value'));
272
const shimmer = DOM.append(valueEl, $('span.chat-debug-overview-metric-shimmer'));
273
shimmer.textContent = '\u00A0'; // non-breaking space for height
274
}
275
}
276
277
private renderMetricsContent(container: HTMLElement, events: readonly IChatDebugEvent[]): void {
278
const modelTurns = events.filter(e => e.kind === 'modelTurn');
279
const toolCalls = events.filter(e => e.kind === 'toolCall');
280
const errors = events.filter(e =>
281
(e.kind === 'generic' && e.level === ChatDebugLogLevel.Error) ||
282
(e.kind === 'toolCall' && e.result === 'error')
283
);
284
285
const fmt = numberFormatter.value;
286
const totalInputTokens = modelTurns.reduce((sum, e) => sum + (e.inputTokens ?? 0), 0);
287
const totalOutputTokens = modelTurns.reduce((sum, e) => sum + (e.outputTokens ?? 0), 0);
288
const totalCachedTokens = modelTurns.reduce((sum, e) => sum + (e.cachedTokens ?? 0), 0);
289
const totalTokens = modelTurns.reduce((sum, e) => sum + (e.totalTokens ?? 0), 0);
290
291
interface OverviewMetric { label: string; value: string }
292
const metrics: OverviewMetric[] = [
293
{ label: localize('chatDebug.metric.modelTurns', "Model Turns"), value: fmt.format(modelTurns.length) },
294
{ label: localize('chatDebug.metric.toolCalls', "Tool Calls"), value: fmt.format(toolCalls.length) },
295
{ label: localize('chatDebug.metric.totalInputTokens', "Total Input Tokens"), value: fmt.format(totalInputTokens) },
296
{ label: localize('chatDebug.metric.totalOutputTokens', "Total Output Tokens"), value: fmt.format(totalOutputTokens) },
297
{ label: localize('chatDebug.metric.totalCachedInputTokens', "Total Cached Input Tokens"), value: fmt.format(totalCachedTokens) },
298
{ label: localize('chatDebug.metric.totalTokens', "Total Tokens"), value: fmt.format(totalTokens) },
299
{ label: localize('chatDebug.metric.errors', "Errors"), value: fmt.format(errors.length) },
300
];
301
302
for (const metric of metrics) {
303
const card = DOM.append(container, $('.chat-debug-overview-metric-card'));
304
DOM.append(card, $('div.chat-debug-overview-metric-label', undefined, metric.label));
305
DOM.append(card, $('div.chat-debug-overview-metric-value', undefined, metric.value));
306
}
307
}
308
}
309
310