Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function registerChatOpenAgentDebugPanelAction() {
});
}

async run(accessor: ServicesAccessor, context?: URI | unknown): Promise<void> {
async run(accessor: ServicesAccessor, context?: URI | unknown, filter?: string): Promise<void> {
const editorService = accessor.get(IEditorService);
const chatWidgetService = accessor.get(IChatWidgetService);
const chatDebugService = accessor.get(IChatDebugService);
Expand All @@ -88,7 +88,7 @@ export function registerChatOpenAgentDebugPanelAction() {
}
chatDebugService.activeSessionResource = sessionResource;

const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs' };
const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs', filter };
await editorService.openEditor(ChatDebugEditorInput.instance, options);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,16 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {
}));
}

// Handle click for debug events attachments
if (attachment.kind === 'debugEvents') {
this.element.style.cursor = 'pointer';
this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => {
const d = new Date(attachment.snapshotTime);
const filter = `before:${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
this.commandService.executeCommand('workbench.action.chat.openAgentDebugPanelForSession', attachment.sessionResource, filter);
}));
}

// Setup tooltip hover for string context attachments
if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) {
this._setupTooltipHover(attachment.tooltip);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export class ChatDebugEditor extends EditorPane {
}

private _applyNavigationOptions(options: IChatDebugEditorOptions): void {
const { sessionResource, viewHint } = options;
const { sessionResource, viewHint, filter } = options;
if (viewHint === 'logs' && sessionResource) {
this.navigateToSession(sessionResource, 'logs');
} else if (viewHint === 'flowchart' && sessionResource) {
Expand All @@ -356,6 +356,12 @@ export class ChatDebugEditor extends EditorPane {
} else if (this.viewState === ViewState.Home) {
this.showView(ViewState.Home);
}

// Apply filter text if provided (e.g. from debug events snapshot)
if (filter !== undefined && this.filterState) {
this.filterState.setTextFilter(filter);
this.logsView?.setFilterText(filter);
}
}

override layout(dimension: Dimension): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class ChatDebugFilterState extends Disposable {
// Text filter
textFilter: string = '';

// Parsed timestamp filters (epoch ms)
beforeTimestamp: number | undefined;
afterTimestamp: number | undefined;

isKindVisible(kind: string, category?: string): boolean {
switch (kind) {
case 'toolCall': return this.filterKindToolCall;
Expand Down Expand Up @@ -70,10 +74,90 @@ export class ChatDebugFilterState extends Disposable {
const normalized = text.toLowerCase();
if (this.textFilter !== normalized) {
this.textFilter = normalized;
this._parseTimestampFilters(normalized);
this._onDidChange.fire();
}
}

setBeforeTimestamp(timestamp: number | undefined): void {
if (this.beforeTimestamp !== timestamp) {
this.beforeTimestamp = timestamp;
this._onDidChange.fire();
}
}

/**
* Parse `before:YYYY[-MM[-DD[THH[:MM[:SS]]]]]` from the filter text.
* Each component after the year is optional.
*/
private _parseTimestampFilters(text: string): void {
this.beforeTimestamp = ChatDebugFilterState.parseTimeToken(text, 'before');
this.afterTimestamp = ChatDebugFilterState.parseTimeToken(text, 'after');
}

static parseTimeToken(text: string, prefix: string): number | undefined {
const regex = new RegExp(`${prefix}:(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:t(\\d{1,2})(?::(\\d{2})(?::(\\d{2}))?)?)?)?)?(?!\\w)`);
const m = regex.exec(text);
if (!m) {
return undefined;
}

const year = parseInt(m[1], 10);
const month = m[2] !== undefined ? parseInt(m[2], 10) - 1 : undefined;
const day = m[3] !== undefined ? parseInt(m[3], 10) : undefined;
const hour = m[4] !== undefined ? parseInt(m[4], 10) : undefined;
const minute = m[5] !== undefined ? parseInt(m[5], 10) : undefined;
const second = m[6] !== undefined ? parseInt(m[6], 10) : undefined;

// For 'before:', round up to the end of the most specific unit given.
// For 'after:', use the start of the most specific unit.
if (prefix === 'before') {
if (second !== undefined) {
return new Date(year, month!, day!, hour!, minute!, second, 999).getTime();
} else if (minute !== undefined) {
return new Date(year, month!, day!, hour!, minute, 59, 999).getTime();
} else if (hour !== undefined) {
return new Date(year, month!, day!, hour, 59, 59, 999).getTime();
} else if (day !== undefined) {
return new Date(year, month!, day, 23, 59, 59, 999).getTime();
} else if (month !== undefined) {
// End of the given month
return new Date(year, month + 1, 0, 23, 59, 59, 999).getTime();
} else {
// End of the given year
return new Date(year, 11, 31, 23, 59, 59, 999).getTime();
}
} else {
return new Date(
year,
month ?? 0,
day ?? 1,
hour ?? 0,
minute ?? 0,
second ?? 0,
0,
).getTime();
}
}

/** Returns the text filter with before:/after: tokens removed. */
get textFilterWithoutTimestamps(): string {
return this.textFilter
.replace(/\b(?:before|after):\d{4}(?:-\d{2}(?:-\d{2}(?:t\d{1,2}(?::\d{2}(?::\d{2})?)?)?)?)?\b/g, '')
.trim();
}

isTimestampVisible(created: Date): boolean {
const time = created.getTime();
if (this.beforeTimestamp !== undefined && time > this.beforeTimestamp) {
return false;
}
if (this.afterTimestamp !== undefined && time < this.afterTimestamp) {
return false;
}
return true;
}

fire(): void {
this._onDidChange.fire();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ChatDebugEventRenderer, ChatDebugEventDelegate, ChatDebugEventTreeRende
import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem, LogsViewMode } from './chatDebugTypes.js';
import { ChatDebugFilterState, bindFilterContextKeys } from './chatDebugFilters.js';
import { ChatDebugDetailPanel } from './chatDebugDetailPanel.js';
import { IChatWidgetService } from '../chat.js';

const $ = DOM.$;

Expand Down Expand Up @@ -70,6 +71,7 @@ export class ChatDebugLogsView extends Disposable {
@IChatDebugService private readonly chatDebugService: IChatDebugService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
) {
super();
this.container = DOM.append(parent, $('.chat-debug-logs'));
Expand Down Expand Up @@ -104,7 +106,7 @@ export class ChatDebugLogsView extends Disposable {
new ServiceCollection([IContextKeyService, scopedContextKeyService])
));
this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, {
placeholder: localize('chatDebug.search', "Filter (e.g. text, !exclude)"),
placeholder: localize('chatDebug.search', "Filter (e.g. text, !exclude, before:YYYY-MM-DDTHH:MM:SS)"),
ariaLabel: localize('chatDebug.filterAriaLabel', "Filter debug events"),
}));

Expand All @@ -119,6 +121,23 @@ export class ChatDebugLogsView extends Disposable {
const filterContainer = DOM.append(this.headerContainer, $('.viewpane-filter-container'));
filterContainer.appendChild(this.filterWidget.element);

// Troubleshoot button
const troubleshootButton = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.troubleshoot', "Add snapshot to Chat") }));
troubleshootButton.element.classList.add('chat-debug-troubleshoot-button', 'monaco-text-button');
DOM.append(troubleshootButton.element, $(`span${ThemeIcon.asCSSSelector(Codicon.chatSparkle)}`));
this._register(troubleshootButton.onDidClick(async () => {
if (!this.currentSessionResource) {
return;
}
const widget = await this.chatWidgetService.openSession(this.currentSessionResource);
if (widget) {
const value = '/troubleshoot ';
widget.inputEditor.setValue(value);
widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
widget.focusInput();
}
}));

this._register(this.filterWidget.onDidChangeFilterText(text => {
this.filterState.setTextFilter(text);
}));
Expand Down Expand Up @@ -241,6 +260,10 @@ export class ChatDebugLogsView extends Disposable {
this.currentSessionResource = sessionResource;
}

setFilterText(text: string): void {
this.filterWidget.setFilterText(text);
}

show(): void {
DOM.show(this.container);
this.loadEvents();
Expand Down Expand Up @@ -297,8 +320,11 @@ export class ChatDebugLogsView extends Disposable {
return this.filterState.isKindVisible(e.kind, category);
});

// Filter by text search
const filterText = this.filterState.textFilter;
// Filter by timestamp (before:/after: syntax)
filtered = filtered.filter(e => this.filterState.isTimestampVisible(e.created));

// Filter by text search (excluding before:/after: tokens)
const filterText = this.filterState.textFilterWithoutTimestamps;
if (filterText) {
const terms = filterText.split(/\s*,\s*/).filter(t => t.length > 0);
const includeTerms = terms.filter(t => !t.startsWith('!')).map(t => t.trim());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const $ = DOM.$;
export interface IChatDebugEditorOptions extends IEditorOptions {
readonly sessionResource?: URI;
readonly viewHint?: 'home' | 'overview' | 'logs' | 'flowchart';
/** When set, automatically applies this text as the log filter. */
readonly filter?: string;
}

export const enum ViewState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@
.chat-debug-editor-header .viewpane-filter-container {
flex: 1;
max-width: 500px;
margin-right: auto;
}
.chat-debug-editor-header .viewpane-filter-container .monaco-inputbox {
border-color: var(--vscode-panelInput-border, transparent) !important;
Expand All @@ -293,6 +294,12 @@
align-items: center;
gap: 6px;
}
.chat-debug-troubleshoot-button.monaco-button {
width: auto;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.chat-debug-view-mode-labels {
display: grid;
}
Expand Down
Loading
Loading