Path: blob/main/src/vs/workbench/contrib/chat/common/chatDebugEvents.ts
13401 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 { IChatDebugEvent } from './chatDebugService.js';67/**8* Checks whether a debug event matches a single text search term.9* Used by both the debug panel filter and the listDebugEvents tool.10*/11export function debugEventMatchesText(event: IChatDebugEvent, term: string): boolean {12if (event.kind.toLowerCase().includes(term)) {13return true;14}15switch (event.kind) {16case 'toolCall':17return event.toolName.toLowerCase().includes(term)18|| (event.input?.toLowerCase().includes(term) ?? false)19|| (event.output?.toLowerCase().includes(term) ?? false);20case 'modelTurn':21return (event.model?.toLowerCase().includes(term) ?? false)22|| (event.requestName?.toLowerCase().includes(term) ?? false);23case 'generic':24return event.name.toLowerCase().includes(term)25|| (event.details?.toLowerCase().includes(term) ?? false)26|| (event.category?.toLowerCase().includes(term) ?? false);27case 'subagentInvocation':28return event.agentName.toLowerCase().includes(term)29|| (event.description?.toLowerCase().includes(term) ?? false);30case 'userMessage':31case 'agentResponse':32return event.message.toLowerCase().includes(term)33|| event.sections.some(s => s.name.toLowerCase().includes(term) || s.content.toLowerCase().includes(term));34}35}3637/**38* Regex used to match `before:` and `after:` timestamp tokens inside filter text.39*/40const timestampTokenPattern = /\b(?:before|after):\d{4}(?:-\d{2}(?:-\d{2}(?:t\d{1,2}(?::\d{2}(?::\d{2})?)?)?)?)?(\b|$)/g;4142/**43* Parse a `before:YYYY[-MM[-DD[THH[:MM[:SS]]]]]` or `after:…` token from44* free-form filter text. Each component after the year is optional.45*46* For `before:`, the timestamp is rounded **up** to the end of the most47* specific unit given (e.g. `before:2026-03` → end-of-March).48* For `after:`, the timestamp is the **start** of the most specific unit.49*/50export function parseTimeToken(text: string, prefix: string): number | undefined {51const regex = new RegExp(`${prefix}:(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:t(\\d{1,2})(?::(\\d{2})(?::(\\d{2}))?)?)?)?)?(?!\\w)`);52const m = regex.exec(text);53if (!m) {54return undefined;55}5657const year = parseInt(m[1], 10);58const month = m[2] !== undefined ? parseInt(m[2], 10) - 1 : undefined;59const day = m[3] !== undefined ? parseInt(m[3], 10) : undefined;60const hour = m[4] !== undefined ? parseInt(m[4], 10) : undefined;61const minute = m[5] !== undefined ? parseInt(m[5], 10) : undefined;62const second = m[6] !== undefined ? parseInt(m[6], 10) : undefined;6364if (prefix === 'before') {65if (second !== undefined) {66return new Date(year, month!, day!, hour!, minute!, second, 999).getTime();67} else if (minute !== undefined) {68return new Date(year, month!, day!, hour!, minute, 59, 999).getTime();69} else if (hour !== undefined) {70return new Date(year, month!, day!, hour, 59, 59, 999).getTime();71} else if (day !== undefined) {72return new Date(year, month!, day, 23, 59, 59, 999).getTime();73} else if (month !== undefined) {74return new Date(year, month + 1, 0, 23, 59, 59, 999).getTime();75} else {76return new Date(year, 11, 31, 23, 59, 59, 999).getTime();77}78} else {79return new Date(80year,81month ?? 0,82day ?? 1,83hour ?? 0,84minute ?? 0,85second ?? 0,860,87).getTime();88}89}9091/**92* Strips `before:…` and `after:…` timestamp tokens from filter text,93* returning only the plain text search portion.94*/95export function stripTimestampTokens(text: string): string {96return text.replace(timestampTokenPattern, '').trim();97}9899/**100* Filters debug events by comma-separated text terms and optional101* `before:`/`after:` timestamp tokens.102*103* Terms prefixed with `!` are exclusions; all others are inclusions.104* At least one inclusion term must match (if any are present).105* Timestamp tokens are parsed and applied as date-range bounds, then106* stripped before text matching.107*/108export function filterDebugEventsByText(events: readonly IChatDebugEvent[], filterText: string): readonly IChatDebugEvent[] {109const beforeTimestamp = parseTimeToken(filterText, 'before');110const afterTimestamp = parseTimeToken(filterText, 'after');111112// Strip timestamp tokens before splitting into text search terms113const textOnly = stripTimestampTokens(filterText);114const terms = textOnly.split(/\s*,\s*/).filter(t => t.length > 0);115const includeTerms = terms.filter(t => !t.startsWith('!')).map(t => t.trim());116const excludeTerms = terms.filter(t => t.startsWith('!')).map(t => t.slice(1).trim()).filter(t => t.length > 0);117118return events.filter(e => {119// Timestamp bounds120const time = e.created.getTime();121if (beforeTimestamp !== undefined && time > beforeTimestamp) {122return false;123}124if (afterTimestamp !== undefined && time < afterTimestamp) {125return false;126}127// Text matching128if (excludeTerms.some(term => debugEventMatchesText(e, term))) {129return false;130}131if (includeTerms.length > 0) {132return includeTerms.some(term => debugEventMatchesText(e, term));133}134return true;135});136}137138export interface DebugEventFilterOptions {139readonly kind?: string;140readonly filter?: string;141readonly limit?: number;142}143144/**145* Applies kind, text, and limit filters to debug events.146* Used by the listDebugEvents tool to consolidate all filtering in one place.147*/148export function filterDebugEvents(events: readonly IChatDebugEvent[], options: DebugEventFilterOptions): readonly IChatDebugEvent[] {149let result = events;150151if (options.kind) {152result = result.filter(e => e.kind === options.kind);153}154155if (options.filter) {156result = filterDebugEventsByText(result, options.filter);157}158159if (options.limit !== undefined && options.limit > 0 && result.length > options.limit) {160result = result.slice(result.length - options.limit);161}162163return result;164}165166167