Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.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 { Dimension } from '../../../../../base/browser/dom.js';
8
import { BreadcrumbsWidget } from '../../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';
9
import { Button } from '../../../../../base/browser/ui/button/button.js';
10
import { ProgressBar } from '../../../../../base/browser/ui/progressbar/progressbar.js';
11
import { IObjectTreeElement } from '../../../../../base/browser/ui/tree/tree.js';
12
import { Codicon } from '../../../../../base/common/codicons.js';
13
import { Emitter } from '../../../../../base/common/event.js';
14
import { combinedDisposable, Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
15
import { autorun } from '../../../../../base/common/observable.js';
16
import { RunOnceScheduler } from '../../../../../base/common/async.js';
17
import { ThemeIcon } from '../../../../../base/common/themables.js';
18
import { URI } from '../../../../../base/common/uri.js';
19
import { localize } from '../../../../../nls.js';
20
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
21
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
22
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
23
import { WorkbenchList, WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js';
24
import { defaultBreadcrumbsWidgetStyles, defaultButtonStyles, defaultProgressBarStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
25
import { FilterWidget } from '../../../../browser/parts/views/viewFilter.js';
26
import { IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js';
27
import { debugEventMatchesText } from '../../common/chatDebugEvents.js';
28
import { IChatService } from '../../common/chatService/chatService.js';
29
import { LocalChatSessionUri } from '../../common/model/chatUri.js';
30
import { ChatDebugEventRenderer, ChatDebugEventDelegate, ChatDebugEventTreeRenderer, getEventCreatedText, getEventNameText, getEventDetailsText } from './chatDebugEventList.js';
31
import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem, LogsViewMode } from './chatDebugTypes.js';
32
import { ChatDebugFilterState, bindFilterContextKeys } from './chatDebugFilters.js';
33
import { ChatDebugDetailPanel } from './chatDebugDetailPanel.js';
34
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
35
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
36
import { Action, Separator } from '../../../../../base/common/actions.js';
37
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
38
39
const $ = DOM.$;
40
41
const PAGE_SIZE = 1000;
42
43
export const enum LogsNavigation {
44
Home = 'home',
45
Overview = 'overview',
46
}
47
48
export class ChatDebugLogsView extends Disposable {
49
50
private readonly _onNavigate = this._register(new Emitter<LogsNavigation>());
51
readonly onNavigate = this._onNavigate.event;
52
53
readonly container: HTMLElement;
54
private readonly breadcrumbWidget: BreadcrumbsWidget;
55
private readonly headerContainer: HTMLElement;
56
private readonly tableHeader: HTMLElement;
57
private readonly bodyContainer: HTMLElement;
58
private readonly listContainer: HTMLElement;
59
private readonly treeContainer: HTMLElement;
60
private readonly detailPanel: ChatDebugDetailPanel;
61
private readonly filterWidget: FilterWidget;
62
private readonly viewModeToggle: Button;
63
64
private list: WorkbenchList<IChatDebugEvent>;
65
private tree: WorkbenchObjectTree<IChatDebugEvent, void>;
66
67
private currentSessionResource: URI | undefined;
68
private logsViewMode: LogsViewMode = LogsViewMode.Tree;
69
private events: IChatDebugEvent[] = [];
70
private filteredEvents: IChatDebugEvent[] = [];
71
private filterDirty = true;
72
private cachedIncludeTerms: string[] = [];
73
private cachedExcludeTerms: string[] = [];
74
private cachedTextFilter: string | undefined;
75
private currentDimension: Dimension | undefined;
76
private readonly eventListener = this._register(new MutableDisposable());
77
private readonly sessionStateDisposable = this._register(new MutableDisposable());
78
private readonly refreshScheduler: RunOnceScheduler;
79
private readonly progressBar: ProgressBar;
80
private readonly showMoreContainer: HTMLElement;
81
private readonly showMoreDisposables = this._register(new DisposableStore());
82
private showMoreStatusLabel: HTMLElement | undefined;
83
private showMoreBtn: Button | undefined;
84
private showMoreVisible = false;
85
private visibleLimit = PAGE_SIZE;
86
87
constructor(
88
parent: HTMLElement,
89
private readonly filterState: ChatDebugFilterState,
90
@IChatService private readonly chatService: IChatService,
91
@IChatDebugService private readonly chatDebugService: IChatDebugService,
92
@IInstantiationService private readonly instantiationService: IInstantiationService,
93
@IContextKeyService private readonly contextKeyService: IContextKeyService,
94
@IClipboardService private readonly clipboardService: IClipboardService,
95
@IContextMenuService private readonly contextMenuService: IContextMenuService,
96
) {
97
super();
98
this.refreshScheduler = this._register(new RunOnceScheduler(() => this.refreshList(), 50));
99
this.container = DOM.append(parent, $('.chat-debug-logs'));
100
DOM.hide(this.container);
101
102
// Breadcrumb
103
const breadcrumbContainer = DOM.append(this.container, $('.chat-debug-breadcrumb'));
104
this.breadcrumbWidget = this._register(new BreadcrumbsWidget(breadcrumbContainer, 3, undefined, Codicon.chevronRight, defaultBreadcrumbsWidgetStyles));
105
this._register(setupBreadcrumbKeyboardNavigation(breadcrumbContainer, this.breadcrumbWidget));
106
this._register(this.breadcrumbWidget.onDidSelectItem(e => {
107
if (e.type === 'select' && e.item instanceof TextBreadcrumbItem) {
108
this.breadcrumbWidget.setSelection(undefined);
109
const items = this.breadcrumbWidget.getItems();
110
const idx = items.indexOf(e.item);
111
if (idx === 0) {
112
this._onNavigate.fire(LogsNavigation.Home);
113
} else if (idx === 1) {
114
this._onNavigate.fire(LogsNavigation.Overview);
115
}
116
}
117
}));
118
119
// Header (filter)
120
this.headerContainer = DOM.append(this.container, $('.chat-debug-editor-header'));
121
122
// Scoped context key service for filter menu items
123
const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.headerContainer));
124
const syncContextKeys = bindFilterContextKeys(this.filterState, scopedContextKeyService);
125
syncContextKeys();
126
127
const childInstantiationService = this._register(this.instantiationService.createChild(
128
new ServiceCollection([IContextKeyService, scopedContextKeyService])
129
));
130
this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, {
131
placeholder: localize('chatDebug.search', "Filter (e.g. text, !exclude, before:YYYY-MM-DDTHH:MM:SS)"),
132
ariaLabel: localize('chatDebug.filterAriaLabel', "Filter debug events"),
133
}));
134
135
// View mode toggle
136
this.viewModeToggle = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.toggleViewMode', "Toggle between list and tree view") }));
137
this.viewModeToggle.element.classList.add('chat-debug-view-mode-toggle', 'monaco-text-button');
138
this.updateViewModeToggle();
139
this._register(this.viewModeToggle.onDidClick(() => {
140
this.toggleViewMode();
141
}));
142
143
const filterContainer = DOM.append(this.headerContainer, $('.viewpane-filter-container'));
144
filterContainer.appendChild(this.filterWidget.element);
145
146
this._register(this.filterWidget.onDidChangeFilterText(text => {
147
this.filterState.setTextFilter(text);
148
}));
149
150
// React to shared filter state changes
151
this._register(this.filterState.onDidChange(() => {
152
syncContextKeys();
153
this.updateMoreFiltersChecked();
154
this.visibleLimit = PAGE_SIZE;
155
this.filterDirty = true;
156
this.refreshList();
157
}));
158
159
// Content wrapper (flex row: main column + detail panel)
160
const contentContainer = DOM.append(this.container, $('.chat-debug-logs-content'));
161
162
// Main column (table header + list/tree body)
163
const mainColumn = DOM.append(contentContainer, $('.chat-debug-logs-main'));
164
165
// Table header
166
this.tableHeader = DOM.append(mainColumn, $('.chat-debug-table-header'));
167
DOM.append(this.tableHeader, $('span.chat-debug-col-created', undefined, localize('chatDebug.col.created', "Created")));
168
DOM.append(this.tableHeader, $('span.chat-debug-col-name', undefined, localize('chatDebug.col.name', "Name")));
169
DOM.append(this.tableHeader, $('span.chat-debug-col-details', undefined, localize('chatDebug.col.details', "Details")));
170
171
// Progress bar (shown when session is in progress)
172
this.progressBar = this._register(new ProgressBar(mainColumn, {
173
...defaultProgressBarStyles,
174
ariaLabel: localize('chatDebug.progressAriaLabel', "Chat debug logs loading progress")
175
}));
176
177
// Body container
178
this.bodyContainer = DOM.append(mainColumn, $('.chat-debug-logs-body'));
179
180
// "Show More" container (below the body, shown when events exceed the visible limit)
181
this.showMoreContainer = DOM.append(mainColumn, $('.chat-debug-logs-show-more'));
182
DOM.hide(this.showMoreContainer);
183
184
// List container (initially hidden — tree view is default)
185
this.listContainer = DOM.append(this.bodyContainer, $('.chat-debug-list-container'));
186
DOM.hide(this.listContainer);
187
188
const accessibilityProvider = {
189
getAriaLabel: (e: IChatDebugEvent) => {
190
switch (e.kind) {
191
case 'toolCall': return localize('chatDebug.aria.toolCall', "Tool call: {0}{1}", e.toolName, e.result ? ` (${e.result})` : '');
192
case 'modelTurn': return localize('chatDebug.aria.modelTurn', "Model turn: {0}{1}{2}",
193
e.model ?? localize('chatDebug.aria.model', "model"),
194
e.totalTokens ? localize('chatDebug.aria.tokenCount', " {0} tokens", e.totalTokens) : '',
195
e.cachedTokens !== undefined ? localize('chatDebug.aria.cachedTokens', " {0} cached", e.cachedTokens) : '');
196
case 'generic': return `${e.category ? e.category + ': ' : ''}${e.name}: ${e.details ?? ''}`;
197
case 'subagentInvocation': return localize('chatDebug.aria.subagent', "Subagent: {0}{1}", e.agentName, e.description ? ` - ${e.description}` : '');
198
case 'userMessage': return localize('chatDebug.aria.userMessage', "User message: {0}", e.message);
199
case 'agentResponse': return localize('chatDebug.aria.agentResponse', "Agent response: {0}", e.message);
200
}
201
},
202
getWidgetAriaLabel: () => localize('chatDebug.ariaLabel', "Chat Debug Events"),
203
};
204
let nextFallbackId = 0;
205
const fallbackIds = new WeakMap<IChatDebugEvent, string>();
206
const identityProvider = {
207
getId: (e: IChatDebugEvent) => {
208
if (e.id) {
209
return e.id;
210
}
211
let fallback = fallbackIds.get(e);
212
if (!fallback) {
213
fallback = `_fallback_${nextFallbackId++}`;
214
fallbackIds.set(e, fallback);
215
}
216
return fallback;
217
}
218
};
219
220
this.list = this._register(this.instantiationService.createInstance(
221
WorkbenchList<IChatDebugEvent>,
222
'ChatDebugEvents',
223
this.listContainer,
224
new ChatDebugEventDelegate(),
225
[new ChatDebugEventRenderer()],
226
{ identityProvider, accessibilityProvider }
227
));
228
229
// Tree container (default view)
230
this.treeContainer = DOM.append(this.bodyContainer, $('.chat-debug-list-container'));
231
232
this.tree = this._register(this.instantiationService.createInstance(
233
WorkbenchObjectTree<IChatDebugEvent, void>,
234
'ChatDebugEventsTree',
235
this.treeContainer,
236
new ChatDebugEventDelegate(),
237
[new ChatDebugEventTreeRenderer()],
238
{ identityProvider, accessibilityProvider }
239
));
240
241
// Detail panel (sibling of main column so it aligns with table header)
242
this.detailPanel = this._register(this.instantiationService.createInstance(ChatDebugDetailPanel, contentContainer));
243
this._register(this.detailPanel.onDidChangeWidth(() => {
244
if (this.currentDimension) {
245
this.layout(this.currentDimension);
246
}
247
}));
248
this._register(this.detailPanel.onDidHide(() => {
249
if (this.list.getSelection().length > 0) {
250
this.list.setSelection([]);
251
}
252
if (this.tree.getSelection().length > 0) {
253
this.tree.setSelection([]);
254
}
255
if (this.currentDimension) {
256
this.layout(this.currentDimension);
257
}
258
}));
259
260
// Context menu
261
this._register(this.list.onContextMenu(e => {
262
if (e.element) {
263
this.showEventContextMenu(e.element, e.browserEvent);
264
}
265
}));
266
this._register(this.tree.onContextMenu(e => {
267
if (e.element) {
268
this.showEventContextMenu(e.element, e.browserEvent);
269
}
270
}));
271
272
// Resolve event details on selection
273
this._register(this.list.onDidChangeSelection(e => {
274
const selected = e.elements[0];
275
if (selected) {
276
this.detailPanel.show(selected);
277
} else {
278
this.detailPanel.hide();
279
}
280
}));
281
282
this._register(this.tree.onDidChangeSelection(e => {
283
const selected = e.elements[0];
284
if (selected) {
285
this.detailPanel.show(selected);
286
} else {
287
this.detailPanel.hide();
288
}
289
}));
290
}
291
292
setSession(sessionResource: URI): void {
293
if (!this.currentSessionResource || this.currentSessionResource.toString() !== sessionResource.toString()) {
294
this.visibleLimit = PAGE_SIZE;
295
}
296
this.currentSessionResource = sessionResource;
297
}
298
299
setFilterText(text: string): void {
300
this.filterWidget.setFilterText(text);
301
}
302
303
show(): void {
304
DOM.show(this.container);
305
this.loadEvents();
306
this.refreshList();
307
}
308
309
hide(): void {
310
DOM.hide(this.container);
311
}
312
313
focus(): void {
314
if (this.logsViewMode === LogsViewMode.Tree) {
315
this.tree.domFocus();
316
} else {
317
this.list.domFocus();
318
}
319
}
320
321
updateBreadcrumb(): void {
322
if (!this.currentSessionResource) {
323
return;
324
}
325
const sessionTitle = this.chatService.getSessionTitle(this.currentSessionResource) || LocalChatSessionUri.parseLocalSessionId(this.currentSessionResource) || this.currentSessionResource.toString();
326
this.breadcrumbWidget.setItems([
327
new TextBreadcrumbItem(localize('chatDebug.title', "Agent Debug Logs"), true),
328
new TextBreadcrumbItem(sessionTitle, true),
329
new TextBreadcrumbItem(localize('chatDebug.logs', "Logs")),
330
]);
331
}
332
333
layout(dimension: Dimension): void {
334
this.currentDimension = dimension;
335
const breadcrumbHeight = 22;
336
const headerHeight = this.headerContainer.offsetHeight;
337
const tableHeaderHeight = this.tableHeader.offsetHeight;
338
const showMoreHeight = this.showMoreContainer.offsetHeight;
339
const detailVisible = this.detailPanel.isVisible;
340
const detailWidth = detailVisible ? this.detailPanel.width : 0;
341
const listHeight = dimension.height - breadcrumbHeight - headerHeight - tableHeaderHeight - showMoreHeight;
342
const listWidth = dimension.width - detailWidth;
343
if (this.logsViewMode === LogsViewMode.Tree) {
344
this.tree.layout(listHeight, listWidth);
345
} else {
346
this.list.layout(listHeight, listWidth);
347
}
348
if (this.detailPanel.isVisible) {
349
this.detailPanel.layout(listHeight);
350
}
351
this.detailPanel.layoutSash();
352
}
353
354
refreshList(): void {
355
// Rebuild the filtered list from scratch only when filter criteria
356
// changed or events were bulk-reloaded. During streaming backfill
357
// the filtered list is kept up-to-date incrementally via addEvent(),
358
// making each refresh O(1) instead of O(n).
359
if (this.filterDirty) {
360
this.filteredEvents = this.events.filter(e => this.passesCurrentFilter(e));
361
this.filterDirty = false;
362
}
363
364
// Paginate: show only the first `visibleLimit` events to keep the UI
365
// responsive for large sessions. The "Show More" button loads the
366
// next page.
367
const totalFiltered = this.filteredEvents.length;
368
const display = totalFiltered > this.visibleLimit ? this.filteredEvents.slice(0, this.visibleLimit) : this.filteredEvents;
369
370
if (this.logsViewMode === LogsViewMode.List) {
371
this.list.splice(0, this.list.length, display);
372
} else {
373
this.refreshTree(display);
374
}
375
376
this.updateShowMore(totalFiltered);
377
378
// Re-layout when show-more visibility changed so the list/tree
379
// height accounts for the footer.
380
if (this.currentDimension) {
381
this.layout(this.currentDimension);
382
}
383
}
384
385
addEvent(event: IChatDebugEvent): void {
386
// Binary-insert into the unfiltered array to maintain chronological
387
// order. Events almost always arrive in order, so the insertion
388
// point is typically at the end (O(log n) comparison, O(1) splice).
389
this.binaryInsert(this.events, event);
390
391
// Incrementally update the filtered list so refreshList() does not
392
// need to re-scan the entire events array on every debounced tick.
393
if (!this.filterDirty && this.passesCurrentFilter(event)) {
394
this.binaryInsert(this.filteredEvents, event);
395
}
396
397
this.scheduleRefresh();
398
}
399
400
private binaryInsert(arr: IChatDebugEvent[], event: IChatDebugEvent): void {
401
const time = event.created.getTime();
402
let lo = 0;
403
let hi = arr.length;
404
while (lo < hi) {
405
const mid = (lo + hi) >>> 1;
406
if (arr[mid].created.getTime() <= time) {
407
lo = mid + 1;
408
} else {
409
hi = mid;
410
}
411
}
412
if (lo === arr.length) {
413
arr.push(event);
414
} else {
415
arr.splice(lo, 0, event);
416
}
417
}
418
419
/**
420
* Tests whether a single event passes the current kind + text + timestamp
421
* filters. Used for incremental filtering on each addEvent() call.
422
*/
423
private passesCurrentFilter(event: IChatDebugEvent): boolean {
424
// Kind filter
425
const category = event.kind === 'generic' ? event.category : undefined;
426
if (!this.filterState.isKindVisible(event.kind, category)) {
427
return false;
428
}
429
430
// Timestamp filter
431
if (!this.filterState.isTimestampVisible(event.created)) {
432
return false;
433
}
434
435
// Text filter — use cached parsed terms to avoid re-splitting on
436
// every addEvent() call during rapid backfill.
437
this.ensureCachedTerms();
438
if (this.cachedExcludeTerms.length > 0 && this.cachedExcludeTerms.some(term => debugEventMatchesText(event, term))) {
439
return false;
440
}
441
if (this.cachedIncludeTerms.length > 0 && !this.cachedIncludeTerms.some(term => debugEventMatchesText(event, term))) {
442
return false;
443
}
444
445
return true;
446
}
447
448
private ensureCachedTerms(): void {
449
const textOnly = this.filterState.textFilterWithoutTimestamps;
450
if (textOnly === this.cachedTextFilter) {
451
return;
452
}
453
this.cachedTextFilter = textOnly;
454
if (!textOnly) {
455
this.cachedIncludeTerms = [];
456
this.cachedExcludeTerms = [];
457
return;
458
}
459
const terms = textOnly.split(',').map(t => t.trim()).filter(t => t.length > 0);
460
this.cachedIncludeTerms = terms.filter(t => !t.startsWith('!'));
461
this.cachedExcludeTerms = terms.filter(t => t.startsWith('!')).map(t => t.slice(1).trim()).filter(t => t.length > 0);
462
}
463
464
private scheduleRefresh(): void {
465
if (!this.refreshScheduler.isScheduled()) {
466
this.refreshScheduler.schedule();
467
}
468
}
469
470
private loadEvents(): void {
471
this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)];
472
this.filterDirty = true;
473
474
const addEventDisposable = this.chatDebugService.onDidAddEvent(e => {
475
if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) {
476
this.addEvent(e);
477
}
478
});
479
480
// Reload events when provider events are cleared (before re-invoking providers)
481
const clearEventsDisposable = this.chatDebugService.onDidClearProviderEvents(sessionResource => {
482
if (!this.currentSessionResource || sessionResource.toString() === this.currentSessionResource.toString()) {
483
this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)];
484
this.filterDirty = true;
485
this.refreshList();
486
}
487
});
488
489
this.eventListener.value = combinedDisposable(addEventDisposable, clearEventsDisposable);
490
this.updateBreadcrumb();
491
this.trackSessionState();
492
}
493
494
private trackSessionState(): void {
495
if (!this.currentSessionResource) {
496
this.progressBar.stop();
497
this.sessionStateDisposable.clear();
498
return;
499
}
500
501
const model = this.chatService.getSession(this.currentSessionResource);
502
if (!model) {
503
this.progressBar.stop();
504
this.sessionStateDisposable.clear();
505
return;
506
}
507
508
this.sessionStateDisposable.value = autorun(reader => {
509
const inProgress = model.requestInProgress.read(reader);
510
if (inProgress) {
511
this.progressBar.infinite();
512
} else {
513
this.progressBar.stop();
514
}
515
});
516
}
517
518
private refreshTree(filtered: readonly IChatDebugEvent[]): void {
519
const treeElements = this.buildTreeHierarchy(filtered);
520
this.tree.setChildren(null, treeElements);
521
}
522
523
private buildTreeHierarchy(events: readonly IChatDebugEvent[]): IObjectTreeElement<IChatDebugEvent>[] {
524
const idToEvent = new Map<string, IChatDebugEvent>();
525
const idToChildren = new Map<string, IChatDebugEvent[]>();
526
const roots: IChatDebugEvent[] = [];
527
528
for (const event of events) {
529
if (event.id) {
530
idToEvent.set(event.id, event);
531
}
532
}
533
534
for (const event of events) {
535
if (event.parentEventId && idToEvent.has(event.parentEventId)) {
536
let children = idToChildren.get(event.parentEventId);
537
if (!children) {
538
children = [];
539
idToChildren.set(event.parentEventId, children);
540
}
541
children.push(event);
542
} else {
543
roots.push(event);
544
}
545
}
546
547
const toTreeElement = (event: IChatDebugEvent): IObjectTreeElement<IChatDebugEvent> => {
548
const children = event.id ? idToChildren.get(event.id) : undefined;
549
return {
550
element: event,
551
children: children?.map(toTreeElement),
552
collapsible: (children?.length ?? 0) > 0,
553
collapsed: false,
554
};
555
};
556
557
return roots.map(toTreeElement);
558
}
559
560
private updateShowMore(totalFiltered: number): void {
561
if (totalFiltered <= this.visibleLimit) {
562
if (this.showMoreVisible) {
563
DOM.hide(this.showMoreContainer);
564
this.showMoreVisible = false;
565
}
566
return;
567
}
568
569
// Create the status label and button once, then reuse.
570
if (!this.showMoreStatusLabel) {
571
this.showMoreStatusLabel = DOM.append(this.showMoreContainer, $('span.chat-debug-logs-show-more-status'));
572
}
573
if (!this.showMoreBtn) {
574
this.showMoreBtn = this.showMoreDisposables.add(new Button(this.showMoreContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.showMoreTitle', "Load more events") }));
575
this.showMoreDisposables.add(this.showMoreBtn.onDidClick(() => {
576
this.visibleLimit += PAGE_SIZE;
577
this.refreshList();
578
}));
579
}
580
581
const shown = Math.min(this.visibleLimit, totalFiltered);
582
const remaining = totalFiltered - shown;
583
584
this.showMoreStatusLabel.textContent = localize('chatDebug.showingCount', "Showing {0} of {1} events", shown, totalFiltered);
585
this.showMoreBtn.label = localize('chatDebug.showMore', "Show More ({0})", remaining);
586
587
if (!this.showMoreVisible) {
588
DOM.show(this.showMoreContainer);
589
this.showMoreVisible = true;
590
}
591
}
592
593
private toggleViewMode(): void {
594
if (this.logsViewMode === LogsViewMode.List) {
595
this.logsViewMode = LogsViewMode.Tree;
596
DOM.hide(this.listContainer);
597
DOM.show(this.treeContainer);
598
} else {
599
this.logsViewMode = LogsViewMode.List;
600
DOM.show(this.listContainer);
601
DOM.hide(this.treeContainer);
602
}
603
this.updateViewModeToggle();
604
this.refreshList();
605
if (this.currentDimension) {
606
this.layout(this.currentDimension);
607
}
608
}
609
610
private updateViewModeToggle(): void {
611
const el = this.viewModeToggle.element;
612
DOM.clearNode(el);
613
const isTree = this.logsViewMode === LogsViewMode.Tree;
614
DOM.append(el, $(`span${ThemeIcon.asCSSSelector(isTree ? Codicon.listTree : Codicon.listFlat)}`));
615
616
const labelContainer = DOM.append(el, $('span.chat-debug-view-mode-labels'));
617
const treeLabel = DOM.append(labelContainer, $('span.chat-debug-view-mode-label'));
618
treeLabel.textContent = localize('chatDebug.treeView', "Tree View");
619
const listLabel = DOM.append(labelContainer, $('span.chat-debug-view-mode-label'));
620
listLabel.textContent = localize('chatDebug.listView', "List View");
621
622
if (isTree) {
623
listLabel.classList.add('hidden');
624
} else {
625
treeLabel.classList.add('hidden');
626
}
627
628
const activeLabel = isTree
629
? localize('chatDebug.switchToListView', "Switch to List View")
630
: localize('chatDebug.switchToTreeView', "Switch to Tree View");
631
el.setAttribute('aria-label', activeLabel);
632
this.viewModeToggle.setTitle(activeLabel);
633
}
634
635
private updateMoreFiltersChecked(): void {
636
this.filterWidget.checkMoreFilters(!this.filterState.isAllFiltersDefault());
637
}
638
639
private showEventContextMenu(event: IChatDebugEvent, browserEvent: UIEvent): void {
640
const d = event.created;
641
const pad = (n: number) => String(n).padStart(2, '0');
642
const timestamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
643
const row = [getEventCreatedText(event), getEventNameText(event), getEventDetailsText(event)].filter(Boolean).join('\t');
644
const name = getEventNameText(event);
645
this.contextMenuService.showContextMenu({
646
getAnchor: () => DOM.isMouseEvent(browserEvent)
647
? new StandardMouseEvent(DOM.getWindow(this.container), browserEvent)
648
: this.container,
649
getActions: () => [
650
new Action('chatDebug.copyTimestamp', localize('chatDebug.copyTimestamp', "Copy Timestamp"), undefined, true, () => this.clipboardService.writeText(timestamp)),
651
new Action('chatDebug.copyRow', localize('chatDebug.copyRow', "Copy Row"), undefined, true, () => this.clipboardService.writeText(row)),
652
new Separator(),
653
new Action('chatDebug.filterBefore', localize('chatDebug.filterBefore', "Filter Before Timestamp"), undefined, true, () => this.applyFilterToken(`before:${timestamp}`)),
654
new Action('chatDebug.filterAfter', localize('chatDebug.filterAfter', "Filter After Timestamp"), undefined, true, () => this.applyFilterToken(`after:${timestamp}`)),
655
new Action('chatDebug.filterName', localize('chatDebug.filterName', "Filter Name"), undefined, !!name, () => this.applyFilterToken(name)),
656
],
657
});
658
}
659
660
private applyFilterToken(token: string): void {
661
this.filterWidget.setFilterText(token);
662
}
663
664
}
665
666