Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.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 { Dimension } from '../../../../../base/browser/dom.js';7import { BreadcrumbsWidget } from '../../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';8import { Button } from '../../../../../base/browser/ui/button/button.js';9import { ProgressBar } from '../../../../../base/browser/ui/progressbar/progressbar.js';10import { IObjectTreeElement } from '../../../../../base/browser/ui/tree/tree.js';11import { Codicon } from '../../../../../base/common/codicons.js';12import { Emitter } from '../../../../../base/common/event.js';13import { combinedDisposable, Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';14import { autorun } from '../../../../../base/common/observable.js';15import { RunOnceScheduler } from '../../../../../base/common/async.js';16import { ThemeIcon } from '../../../../../base/common/themables.js';17import { URI } from '../../../../../base/common/uri.js';18import { localize } from '../../../../../nls.js';19import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';20import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';21import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';22import { WorkbenchList, WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js';23import { defaultBreadcrumbsWidgetStyles, defaultButtonStyles, defaultProgressBarStyles } from '../../../../../platform/theme/browser/defaultStyles.js';24import { FilterWidget } from '../../../../browser/parts/views/viewFilter.js';25import { IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js';26import { debugEventMatchesText } from '../../common/chatDebugEvents.js';27import { IChatService } from '../../common/chatService/chatService.js';28import { LocalChatSessionUri } from '../../common/model/chatUri.js';29import { ChatDebugEventRenderer, ChatDebugEventDelegate, ChatDebugEventTreeRenderer, getEventCreatedText, getEventNameText, getEventDetailsText } from './chatDebugEventList.js';30import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem, LogsViewMode } from './chatDebugTypes.js';31import { ChatDebugFilterState, bindFilterContextKeys } from './chatDebugFilters.js';32import { ChatDebugDetailPanel } from './chatDebugDetailPanel.js';33import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';34import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';35import { Action, Separator } from '../../../../../base/common/actions.js';36import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';3738const $ = DOM.$;3940const PAGE_SIZE = 1000;4142export const enum LogsNavigation {43Home = 'home',44Overview = 'overview',45}4647export class ChatDebugLogsView extends Disposable {4849private readonly _onNavigate = this._register(new Emitter<LogsNavigation>());50readonly onNavigate = this._onNavigate.event;5152readonly container: HTMLElement;53private readonly breadcrumbWidget: BreadcrumbsWidget;54private readonly headerContainer: HTMLElement;55private readonly tableHeader: HTMLElement;56private readonly bodyContainer: HTMLElement;57private readonly listContainer: HTMLElement;58private readonly treeContainer: HTMLElement;59private readonly detailPanel: ChatDebugDetailPanel;60private readonly filterWidget: FilterWidget;61private readonly viewModeToggle: Button;6263private list: WorkbenchList<IChatDebugEvent>;64private tree: WorkbenchObjectTree<IChatDebugEvent, void>;6566private currentSessionResource: URI | undefined;67private logsViewMode: LogsViewMode = LogsViewMode.Tree;68private events: IChatDebugEvent[] = [];69private filteredEvents: IChatDebugEvent[] = [];70private filterDirty = true;71private cachedIncludeTerms: string[] = [];72private cachedExcludeTerms: string[] = [];73private cachedTextFilter: string | undefined;74private currentDimension: Dimension | undefined;75private readonly eventListener = this._register(new MutableDisposable());76private readonly sessionStateDisposable = this._register(new MutableDisposable());77private readonly refreshScheduler: RunOnceScheduler;78private readonly progressBar: ProgressBar;79private readonly showMoreContainer: HTMLElement;80private readonly showMoreDisposables = this._register(new DisposableStore());81private showMoreStatusLabel: HTMLElement | undefined;82private showMoreBtn: Button | undefined;83private showMoreVisible = false;84private visibleLimit = PAGE_SIZE;8586constructor(87parent: HTMLElement,88private readonly filterState: ChatDebugFilterState,89@IChatService private readonly chatService: IChatService,90@IChatDebugService private readonly chatDebugService: IChatDebugService,91@IInstantiationService private readonly instantiationService: IInstantiationService,92@IContextKeyService private readonly contextKeyService: IContextKeyService,93@IClipboardService private readonly clipboardService: IClipboardService,94@IContextMenuService private readonly contextMenuService: IContextMenuService,95) {96super();97this.refreshScheduler = this._register(new RunOnceScheduler(() => this.refreshList(), 50));98this.container = DOM.append(parent, $('.chat-debug-logs'));99DOM.hide(this.container);100101// Breadcrumb102const breadcrumbContainer = DOM.append(this.container, $('.chat-debug-breadcrumb'));103this.breadcrumbWidget = this._register(new BreadcrumbsWidget(breadcrumbContainer, 3, undefined, Codicon.chevronRight, defaultBreadcrumbsWidgetStyles));104this._register(setupBreadcrumbKeyboardNavigation(breadcrumbContainer, this.breadcrumbWidget));105this._register(this.breadcrumbWidget.onDidSelectItem(e => {106if (e.type === 'select' && e.item instanceof TextBreadcrumbItem) {107this.breadcrumbWidget.setSelection(undefined);108const items = this.breadcrumbWidget.getItems();109const idx = items.indexOf(e.item);110if (idx === 0) {111this._onNavigate.fire(LogsNavigation.Home);112} else if (idx === 1) {113this._onNavigate.fire(LogsNavigation.Overview);114}115}116}));117118// Header (filter)119this.headerContainer = DOM.append(this.container, $('.chat-debug-editor-header'));120121// Scoped context key service for filter menu items122const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.headerContainer));123const syncContextKeys = bindFilterContextKeys(this.filterState, scopedContextKeyService);124syncContextKeys();125126const childInstantiationService = this._register(this.instantiationService.createChild(127new ServiceCollection([IContextKeyService, scopedContextKeyService])128));129this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, {130placeholder: localize('chatDebug.search', "Filter (e.g. text, !exclude, before:YYYY-MM-DDTHH:MM:SS)"),131ariaLabel: localize('chatDebug.filterAriaLabel', "Filter debug events"),132}));133134// View mode toggle135this.viewModeToggle = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.toggleViewMode', "Toggle between list and tree view") }));136this.viewModeToggle.element.classList.add('chat-debug-view-mode-toggle', 'monaco-text-button');137this.updateViewModeToggle();138this._register(this.viewModeToggle.onDidClick(() => {139this.toggleViewMode();140}));141142const filterContainer = DOM.append(this.headerContainer, $('.viewpane-filter-container'));143filterContainer.appendChild(this.filterWidget.element);144145this._register(this.filterWidget.onDidChangeFilterText(text => {146this.filterState.setTextFilter(text);147}));148149// React to shared filter state changes150this._register(this.filterState.onDidChange(() => {151syncContextKeys();152this.updateMoreFiltersChecked();153this.visibleLimit = PAGE_SIZE;154this.filterDirty = true;155this.refreshList();156}));157158// Content wrapper (flex row: main column + detail panel)159const contentContainer = DOM.append(this.container, $('.chat-debug-logs-content'));160161// Main column (table header + list/tree body)162const mainColumn = DOM.append(contentContainer, $('.chat-debug-logs-main'));163164// Table header165this.tableHeader = DOM.append(mainColumn, $('.chat-debug-table-header'));166DOM.append(this.tableHeader, $('span.chat-debug-col-created', undefined, localize('chatDebug.col.created', "Created")));167DOM.append(this.tableHeader, $('span.chat-debug-col-name', undefined, localize('chatDebug.col.name', "Name")));168DOM.append(this.tableHeader, $('span.chat-debug-col-details', undefined, localize('chatDebug.col.details', "Details")));169170// Progress bar (shown when session is in progress)171this.progressBar = this._register(new ProgressBar(mainColumn, {172...defaultProgressBarStyles,173ariaLabel: localize('chatDebug.progressAriaLabel', "Chat debug logs loading progress")174}));175176// Body container177this.bodyContainer = DOM.append(mainColumn, $('.chat-debug-logs-body'));178179// "Show More" container (below the body, shown when events exceed the visible limit)180this.showMoreContainer = DOM.append(mainColumn, $('.chat-debug-logs-show-more'));181DOM.hide(this.showMoreContainer);182183// List container (initially hidden — tree view is default)184this.listContainer = DOM.append(this.bodyContainer, $('.chat-debug-list-container'));185DOM.hide(this.listContainer);186187const accessibilityProvider = {188getAriaLabel: (e: IChatDebugEvent) => {189switch (e.kind) {190case 'toolCall': return localize('chatDebug.aria.toolCall', "Tool call: {0}{1}", e.toolName, e.result ? ` (${e.result})` : '');191case 'modelTurn': return localize('chatDebug.aria.modelTurn', "Model turn: {0}{1}{2}",192e.model ?? localize('chatDebug.aria.model', "model"),193e.totalTokens ? localize('chatDebug.aria.tokenCount', " {0} tokens", e.totalTokens) : '',194e.cachedTokens !== undefined ? localize('chatDebug.aria.cachedTokens', " {0} cached", e.cachedTokens) : '');195case 'generic': return `${e.category ? e.category + ': ' : ''}${e.name}: ${e.details ?? ''}`;196case 'subagentInvocation': return localize('chatDebug.aria.subagent', "Subagent: {0}{1}", e.agentName, e.description ? ` - ${e.description}` : '');197case 'userMessage': return localize('chatDebug.aria.userMessage', "User message: {0}", e.message);198case 'agentResponse': return localize('chatDebug.aria.agentResponse', "Agent response: {0}", e.message);199}200},201getWidgetAriaLabel: () => localize('chatDebug.ariaLabel', "Chat Debug Events"),202};203let nextFallbackId = 0;204const fallbackIds = new WeakMap<IChatDebugEvent, string>();205const identityProvider = {206getId: (e: IChatDebugEvent) => {207if (e.id) {208return e.id;209}210let fallback = fallbackIds.get(e);211if (!fallback) {212fallback = `_fallback_${nextFallbackId++}`;213fallbackIds.set(e, fallback);214}215return fallback;216}217};218219this.list = this._register(this.instantiationService.createInstance(220WorkbenchList<IChatDebugEvent>,221'ChatDebugEvents',222this.listContainer,223new ChatDebugEventDelegate(),224[new ChatDebugEventRenderer()],225{ identityProvider, accessibilityProvider }226));227228// Tree container (default view)229this.treeContainer = DOM.append(this.bodyContainer, $('.chat-debug-list-container'));230231this.tree = this._register(this.instantiationService.createInstance(232WorkbenchObjectTree<IChatDebugEvent, void>,233'ChatDebugEventsTree',234this.treeContainer,235new ChatDebugEventDelegate(),236[new ChatDebugEventTreeRenderer()],237{ identityProvider, accessibilityProvider }238));239240// Detail panel (sibling of main column so it aligns with table header)241this.detailPanel = this._register(this.instantiationService.createInstance(ChatDebugDetailPanel, contentContainer));242this._register(this.detailPanel.onDidChangeWidth(() => {243if (this.currentDimension) {244this.layout(this.currentDimension);245}246}));247this._register(this.detailPanel.onDidHide(() => {248if (this.list.getSelection().length > 0) {249this.list.setSelection([]);250}251if (this.tree.getSelection().length > 0) {252this.tree.setSelection([]);253}254if (this.currentDimension) {255this.layout(this.currentDimension);256}257}));258259// Context menu260this._register(this.list.onContextMenu(e => {261if (e.element) {262this.showEventContextMenu(e.element, e.browserEvent);263}264}));265this._register(this.tree.onContextMenu(e => {266if (e.element) {267this.showEventContextMenu(e.element, e.browserEvent);268}269}));270271// Resolve event details on selection272this._register(this.list.onDidChangeSelection(e => {273const selected = e.elements[0];274if (selected) {275this.detailPanel.show(selected);276} else {277this.detailPanel.hide();278}279}));280281this._register(this.tree.onDidChangeSelection(e => {282const selected = e.elements[0];283if (selected) {284this.detailPanel.show(selected);285} else {286this.detailPanel.hide();287}288}));289}290291setSession(sessionResource: URI): void {292if (!this.currentSessionResource || this.currentSessionResource.toString() !== sessionResource.toString()) {293this.visibleLimit = PAGE_SIZE;294}295this.currentSessionResource = sessionResource;296}297298setFilterText(text: string): void {299this.filterWidget.setFilterText(text);300}301302show(): void {303DOM.show(this.container);304this.loadEvents();305this.refreshList();306}307308hide(): void {309DOM.hide(this.container);310}311312focus(): void {313if (this.logsViewMode === LogsViewMode.Tree) {314this.tree.domFocus();315} else {316this.list.domFocus();317}318}319320updateBreadcrumb(): void {321if (!this.currentSessionResource) {322return;323}324const sessionTitle = this.chatService.getSessionTitle(this.currentSessionResource) || LocalChatSessionUri.parseLocalSessionId(this.currentSessionResource) || this.currentSessionResource.toString();325this.breadcrumbWidget.setItems([326new TextBreadcrumbItem(localize('chatDebug.title', "Agent Debug Logs"), true),327new TextBreadcrumbItem(sessionTitle, true),328new TextBreadcrumbItem(localize('chatDebug.logs', "Logs")),329]);330}331332layout(dimension: Dimension): void {333this.currentDimension = dimension;334const breadcrumbHeight = 22;335const headerHeight = this.headerContainer.offsetHeight;336const tableHeaderHeight = this.tableHeader.offsetHeight;337const showMoreHeight = this.showMoreContainer.offsetHeight;338const detailVisible = this.detailPanel.isVisible;339const detailWidth = detailVisible ? this.detailPanel.width : 0;340const listHeight = dimension.height - breadcrumbHeight - headerHeight - tableHeaderHeight - showMoreHeight;341const listWidth = dimension.width - detailWidth;342if (this.logsViewMode === LogsViewMode.Tree) {343this.tree.layout(listHeight, listWidth);344} else {345this.list.layout(listHeight, listWidth);346}347if (this.detailPanel.isVisible) {348this.detailPanel.layout(listHeight);349}350this.detailPanel.layoutSash();351}352353refreshList(): void {354// Rebuild the filtered list from scratch only when filter criteria355// changed or events were bulk-reloaded. During streaming backfill356// the filtered list is kept up-to-date incrementally via addEvent(),357// making each refresh O(1) instead of O(n).358if (this.filterDirty) {359this.filteredEvents = this.events.filter(e => this.passesCurrentFilter(e));360this.filterDirty = false;361}362363// Paginate: show only the first `visibleLimit` events to keep the UI364// responsive for large sessions. The "Show More" button loads the365// next page.366const totalFiltered = this.filteredEvents.length;367const display = totalFiltered > this.visibleLimit ? this.filteredEvents.slice(0, this.visibleLimit) : this.filteredEvents;368369if (this.logsViewMode === LogsViewMode.List) {370this.list.splice(0, this.list.length, display);371} else {372this.refreshTree(display);373}374375this.updateShowMore(totalFiltered);376377// Re-layout when show-more visibility changed so the list/tree378// height accounts for the footer.379if (this.currentDimension) {380this.layout(this.currentDimension);381}382}383384addEvent(event: IChatDebugEvent): void {385// Binary-insert into the unfiltered array to maintain chronological386// order. Events almost always arrive in order, so the insertion387// point is typically at the end (O(log n) comparison, O(1) splice).388this.binaryInsert(this.events, event);389390// Incrementally update the filtered list so refreshList() does not391// need to re-scan the entire events array on every debounced tick.392if (!this.filterDirty && this.passesCurrentFilter(event)) {393this.binaryInsert(this.filteredEvents, event);394}395396this.scheduleRefresh();397}398399private binaryInsert(arr: IChatDebugEvent[], event: IChatDebugEvent): void {400const time = event.created.getTime();401let lo = 0;402let hi = arr.length;403while (lo < hi) {404const mid = (lo + hi) >>> 1;405if (arr[mid].created.getTime() <= time) {406lo = mid + 1;407} else {408hi = mid;409}410}411if (lo === arr.length) {412arr.push(event);413} else {414arr.splice(lo, 0, event);415}416}417418/**419* Tests whether a single event passes the current kind + text + timestamp420* filters. Used for incremental filtering on each addEvent() call.421*/422private passesCurrentFilter(event: IChatDebugEvent): boolean {423// Kind filter424const category = event.kind === 'generic' ? event.category : undefined;425if (!this.filterState.isKindVisible(event.kind, category)) {426return false;427}428429// Timestamp filter430if (!this.filterState.isTimestampVisible(event.created)) {431return false;432}433434// Text filter — use cached parsed terms to avoid re-splitting on435// every addEvent() call during rapid backfill.436this.ensureCachedTerms();437if (this.cachedExcludeTerms.length > 0 && this.cachedExcludeTerms.some(term => debugEventMatchesText(event, term))) {438return false;439}440if (this.cachedIncludeTerms.length > 0 && !this.cachedIncludeTerms.some(term => debugEventMatchesText(event, term))) {441return false;442}443444return true;445}446447private ensureCachedTerms(): void {448const textOnly = this.filterState.textFilterWithoutTimestamps;449if (textOnly === this.cachedTextFilter) {450return;451}452this.cachedTextFilter = textOnly;453if (!textOnly) {454this.cachedIncludeTerms = [];455this.cachedExcludeTerms = [];456return;457}458const terms = textOnly.split(',').map(t => t.trim()).filter(t => t.length > 0);459this.cachedIncludeTerms = terms.filter(t => !t.startsWith('!'));460this.cachedExcludeTerms = terms.filter(t => t.startsWith('!')).map(t => t.slice(1).trim()).filter(t => t.length > 0);461}462463private scheduleRefresh(): void {464if (!this.refreshScheduler.isScheduled()) {465this.refreshScheduler.schedule();466}467}468469private loadEvents(): void {470this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)];471this.filterDirty = true;472473const addEventDisposable = this.chatDebugService.onDidAddEvent(e => {474if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) {475this.addEvent(e);476}477});478479// Reload events when provider events are cleared (before re-invoking providers)480const clearEventsDisposable = this.chatDebugService.onDidClearProviderEvents(sessionResource => {481if (!this.currentSessionResource || sessionResource.toString() === this.currentSessionResource.toString()) {482this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)];483this.filterDirty = true;484this.refreshList();485}486});487488this.eventListener.value = combinedDisposable(addEventDisposable, clearEventsDisposable);489this.updateBreadcrumb();490this.trackSessionState();491}492493private trackSessionState(): void {494if (!this.currentSessionResource) {495this.progressBar.stop();496this.sessionStateDisposable.clear();497return;498}499500const model = this.chatService.getSession(this.currentSessionResource);501if (!model) {502this.progressBar.stop();503this.sessionStateDisposable.clear();504return;505}506507this.sessionStateDisposable.value = autorun(reader => {508const inProgress = model.requestInProgress.read(reader);509if (inProgress) {510this.progressBar.infinite();511} else {512this.progressBar.stop();513}514});515}516517private refreshTree(filtered: readonly IChatDebugEvent[]): void {518const treeElements = this.buildTreeHierarchy(filtered);519this.tree.setChildren(null, treeElements);520}521522private buildTreeHierarchy(events: readonly IChatDebugEvent[]): IObjectTreeElement<IChatDebugEvent>[] {523const idToEvent = new Map<string, IChatDebugEvent>();524const idToChildren = new Map<string, IChatDebugEvent[]>();525const roots: IChatDebugEvent[] = [];526527for (const event of events) {528if (event.id) {529idToEvent.set(event.id, event);530}531}532533for (const event of events) {534if (event.parentEventId && idToEvent.has(event.parentEventId)) {535let children = idToChildren.get(event.parentEventId);536if (!children) {537children = [];538idToChildren.set(event.parentEventId, children);539}540children.push(event);541} else {542roots.push(event);543}544}545546const toTreeElement = (event: IChatDebugEvent): IObjectTreeElement<IChatDebugEvent> => {547const children = event.id ? idToChildren.get(event.id) : undefined;548return {549element: event,550children: children?.map(toTreeElement),551collapsible: (children?.length ?? 0) > 0,552collapsed: false,553};554};555556return roots.map(toTreeElement);557}558559private updateShowMore(totalFiltered: number): void {560if (totalFiltered <= this.visibleLimit) {561if (this.showMoreVisible) {562DOM.hide(this.showMoreContainer);563this.showMoreVisible = false;564}565return;566}567568// Create the status label and button once, then reuse.569if (!this.showMoreStatusLabel) {570this.showMoreStatusLabel = DOM.append(this.showMoreContainer, $('span.chat-debug-logs-show-more-status'));571}572if (!this.showMoreBtn) {573this.showMoreBtn = this.showMoreDisposables.add(new Button(this.showMoreContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.showMoreTitle', "Load more events") }));574this.showMoreDisposables.add(this.showMoreBtn.onDidClick(() => {575this.visibleLimit += PAGE_SIZE;576this.refreshList();577}));578}579580const shown = Math.min(this.visibleLimit, totalFiltered);581const remaining = totalFiltered - shown;582583this.showMoreStatusLabel.textContent = localize('chatDebug.showingCount', "Showing {0} of {1} events", shown, totalFiltered);584this.showMoreBtn.label = localize('chatDebug.showMore', "Show More ({0})", remaining);585586if (!this.showMoreVisible) {587DOM.show(this.showMoreContainer);588this.showMoreVisible = true;589}590}591592private toggleViewMode(): void {593if (this.logsViewMode === LogsViewMode.List) {594this.logsViewMode = LogsViewMode.Tree;595DOM.hide(this.listContainer);596DOM.show(this.treeContainer);597} else {598this.logsViewMode = LogsViewMode.List;599DOM.show(this.listContainer);600DOM.hide(this.treeContainer);601}602this.updateViewModeToggle();603this.refreshList();604if (this.currentDimension) {605this.layout(this.currentDimension);606}607}608609private updateViewModeToggle(): void {610const el = this.viewModeToggle.element;611DOM.clearNode(el);612const isTree = this.logsViewMode === LogsViewMode.Tree;613DOM.append(el, $(`span${ThemeIcon.asCSSSelector(isTree ? Codicon.listTree : Codicon.listFlat)}`));614615const labelContainer = DOM.append(el, $('span.chat-debug-view-mode-labels'));616const treeLabel = DOM.append(labelContainer, $('span.chat-debug-view-mode-label'));617treeLabel.textContent = localize('chatDebug.treeView', "Tree View");618const listLabel = DOM.append(labelContainer, $('span.chat-debug-view-mode-label'));619listLabel.textContent = localize('chatDebug.listView', "List View");620621if (isTree) {622listLabel.classList.add('hidden');623} else {624treeLabel.classList.add('hidden');625}626627const activeLabel = isTree628? localize('chatDebug.switchToListView', "Switch to List View")629: localize('chatDebug.switchToTreeView', "Switch to Tree View");630el.setAttribute('aria-label', activeLabel);631this.viewModeToggle.setTitle(activeLabel);632}633634private updateMoreFiltersChecked(): void {635this.filterWidget.checkMoreFilters(!this.filterState.isAllFiltersDefault());636}637638private showEventContextMenu(event: IChatDebugEvent, browserEvent: UIEvent): void {639const d = event.created;640const pad = (n: number) => String(n).padStart(2, '0');641const timestamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;642const row = [getEventCreatedText(event), getEventNameText(event), getEventDetailsText(event)].filter(Boolean).join('\t');643const name = getEventNameText(event);644this.contextMenuService.showContextMenu({645getAnchor: () => DOM.isMouseEvent(browserEvent)646? new StandardMouseEvent(DOM.getWindow(this.container), browserEvent)647: this.container,648getActions: () => [649new Action('chatDebug.copyTimestamp', localize('chatDebug.copyTimestamp', "Copy Timestamp"), undefined, true, () => this.clipboardService.writeText(timestamp)),650new Action('chatDebug.copyRow', localize('chatDebug.copyRow', "Copy Row"), undefined, true, () => this.clipboardService.writeText(row)),651new Separator(),652new Action('chatDebug.filterBefore', localize('chatDebug.filterBefore', "Filter Before Timestamp"), undefined, true, () => this.applyFilterToken(`before:${timestamp}`)),653new Action('chatDebug.filterAfter', localize('chatDebug.filterAfter', "Filter After Timestamp"), undefined, true, () => this.applyFilterToken(`after:${timestamp}`)),654new Action('chatDebug.filterName', localize('chatDebug.filterName', "Filter Name"), undefined, !!name, () => this.applyFilterToken(name)),655],656});657}658659private applyFilterToken(token: string): void {660this.filterWidget.setFilterText(token);661}662663}664665666