From 9e5953011f243179d84810eee18e4fa579e1de47 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 10:24:19 -0800 Subject: [PATCH 01/13] slash command --- .../contrib/chat/browser/chatSlashCommands.ts | 56 ++++++++ .../chat/common/actions/chatContextKeys.ts | 1 + .../resolveDebugEventDetailsTool.ts | 126 ++++++++++++++++++ .../chat/common/tools/builtinTools/tools.ts | 4 + 4 files changed, 187 insertions(+) create mode 100644 src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 9b3796035309f0..0b0db735cb2d64 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -15,6 +15,9 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IChatAgentService } from '../common/participants/chatAgents.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; +import { IChatDebugEvent, IChatDebugService } from '../common/chatDebugService.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; import { IChatService } from '../common/chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js'; @@ -46,6 +49,8 @@ export class ChatSlashCommandsContribution extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, + @IChatDebugService chatDebugService: IChatDebugService, + @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @@ -116,6 +121,28 @@ export class ChatSlashCommandsContribution extends Disposable { await commandService.executeCommand('github.copilot.debug.showChatLogView'); })); } + this._store.add(slashCommandService.registerSlashCommand({ + command: 'troubleshoot', + detail: nls.localize('troubleshoot', "Troubleshoot the current conversation with debug events"), + sortText: 'z3_troubleshoot', + executeImmediately: false, + locations: [ChatAgentLocation.Chat], + }, async (_prompt, progress, _history, _location, sessionResource) => { + ChatContextKeys.troubleshootActive.bindTo(contextKeyService).set(true); + const events = chatDebugService.getEvents(sessionResource); + if (events.length === 0) { + progress.report({ content: new MarkdownString(nls.localize('troubleshoot.noEvents', "No debug events found for this conversation.")), kind: 'markdownContent' }); + await timeout(200); + return; + } + + progress.report({ content: new MarkdownString(nls.localize('troubleshoot.header', "## Debug Events for This Conversation\n\nFound {0} debug event(s). Use the `resolveDebugEventDetails` tool in follow-up messages to inspect specific events by ID.\n", events.length)), kind: 'markdownContent' }); + + const summary = formatDebugEventsForContext(events); + progress.report({ content: new MarkdownString('```\n' + summary + '\n```'), kind: 'markdownContent' }); + + await timeout(200); + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'agents', detail: nls.localize('agents', "Configure custom agents"), @@ -340,3 +367,32 @@ export class ChatSlashCommandsContribution extends Disposable { })); } } + +function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string { + const lines: string[] = []; + for (const event of events) { + const ts = event.created.toISOString(); + const id = event.id ? ` [id=${event.id}]` : ''; + switch (event.kind) { + case 'generic': + lines.push(`[${ts}]${id} ${event.level >= 3 ? 'ERROR' : event.level >= 2 ? 'WARN' : 'INFO'}: ${event.name}${event.details ? ' - ' + event.details : ''}${event.category ? ' (category: ' + event.category + ')' : ''}`); + break; + case 'toolCall': + lines.push(`[${ts}]${id} TOOL_CALL: ${event.toolName}${event.result ? ' result=' + event.result : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'modelTurn': + lines.push(`[${ts}]${id} MODEL_TURN: ${event.requestName ?? 'unknown'}${event.model ? ' model=' + event.model : ''}${event.inputTokens !== undefined ? ' tokens(in=' + event.inputTokens + ',out=' + (event.outputTokens ?? '?') + ')' : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'subagentInvocation': + lines.push(`[${ts}]${id} SUBAGENT: ${event.agentName}${event.status ? ' status=' + event.status : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'userMessage': + lines.push(`[${ts}]${id} USER_MESSAGE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); + break; + case 'agentResponse': + lines.push(`[${ts}]${id} AGENT_RESPONSE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); + break; + } + } + return lines.join('\n'); +} diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index a85c46b7bfba0f..fcb3db2aec1f80 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -89,6 +89,7 @@ export namespace ChatContextKeys { export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); export const hasPendingRequests = new RawContextKey('chatHasPendingRequests', false, { type: 'boolean', description: localize('chatHasPendingRequests', "True when there are pending requests in the queue.") }); export const chatSessionHasDebugData = new RawContextKey('chatSessionHasDebugData', false, { type: 'boolean', description: localize('chatSessionHasDebugData', "True when the current chat session has debug log data.") }); + export const troubleshootActive = new RawContextKey('chatTroubleshootActive', false, { type: 'boolean', description: localize('chatTroubleshootActive', "True when the /troubleshoot slash command has been used in the current session.") }); export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts new file mode 100644 index 00000000000000..a0c56802b5f12b --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../../actions/chatContextKeys.js'; +import { IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; + +export const ResolveDebugEventDetailsToolId = 'vscode_resolveDebugEventDetails_internal'; + +export const ResolveDebugEventDetailsToolData: IToolData = { + id: ResolveDebugEventDetailsToolId, + displayName: 'Resolve Debug Event Details', + canBeReferencedInPrompt: false, + modelDescription: 'Resolves the full details for a specific chat debug event by its event ID. Use this tool to get detailed information about a debug event such as tool call input/output, model turn details, user message sections, or file lists. The event ID can be found in the debug event log summary provided in the conversation context.', + source: ToolDataSource.Internal, + when: ContextKeyExpr.equals(ChatContextKeys.troubleshootActive.key, true), + inputSchema: { + type: 'object', + properties: { + eventId: { + type: 'string', + description: 'The ID of the debug event to resolve details for.', + }, + }, + required: ['eventId'], + }, +}; + +function formatResolvedContent(content: IChatDebugResolvedEventContent): string { + switch (content.kind) { + case 'text': + return content.value; + case 'fileList': { + const lines: string[] = [`File list (${content.discoveryType}):`]; + if (content.sourceFolders) { + for (const folder of content.sourceFolders) { + lines.push(` Source folder: ${folder.uri.toString()} (${folder.storage}, ${folder.fileCount} files${folder.exists ? '' : ', missing'})`); + } + } + for (const file of content.files) { + const status = file.status === 'loaded' ? 'loaded' : `skipped${file.skipReason ? `: ${file.skipReason}` : ''}`; + lines.push(` ${file.uri.toString()} [${status}]`); + } + return lines.join('\n'); + } + case 'message': { + const lines: string[] = [`${content.type === 'user' ? 'User' : 'Agent'} message: ${content.message}`]; + for (const section of content.sections) { + lines.push(`--- ${section.name} ---`); + lines.push(section.content); + } + return lines.join('\n'); + } + case 'toolCall': { + const lines: string[] = [`Tool call: ${content.toolName}`]; + if (content.result) { + lines.push(`Result: ${content.result}`); + } + if (content.durationInMillis !== undefined) { + lines.push(`Duration: ${content.durationInMillis}ms`); + } + if (content.input) { + lines.push(`Input:\n${content.input}`); + } + if (content.output) { + lines.push(`Output:\n${content.output}`); + } + return lines.join('\n'); + } + case 'modelTurn': { + const lines: string[] = [`Model turn: ${content.requestName}`]; + if (content.model) { + lines.push(`Model: ${content.model}`); + } + if (content.status) { + lines.push(`Status: ${content.status}`); + } + if (content.durationInMillis !== undefined) { + lines.push(`Duration: ${content.durationInMillis}ms`); + } + if (content.inputTokens !== undefined || content.outputTokens !== undefined) { + lines.push(`Tokens: input=${content.inputTokens ?? '?'}, output=${content.outputTokens ?? '?'}, cached=${content.cachedTokens ?? '?'}, total=${content.totalTokens ?? '?'}`); + } + if (content.errorMessage) { + lines.push(`Error: ${content.errorMessage}`); + } + if (content.sections) { + for (const section of content.sections) { + lines.push(`--- ${section.name} ---`); + lines.push(section.content); + } + } + return lines.join('\n'); + } + } +} + +export class ResolveDebugEventDetailsTool implements IToolImpl { + constructor( + @IChatDebugService private readonly chatDebugService: IChatDebugService, + ) { } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const eventId = invocation.parameters['eventId'] as string; + if (!eventId) { + return { + content: [{ kind: 'text', value: 'Error: eventId parameter is required.' }], + }; + } + + const resolved = await this.chatDebugService.resolveEvent(eventId); + if (!resolved) { + return { + content: [{ kind: 'text', value: `No details found for event ID: ${eventId}` }], + }; + } + + return { + content: [{ kind: 'text', value: formatResolvedContent(resolved) }], + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 0258444f00b343..d5ad07903aab7d 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -11,6 +11,7 @@ import { AskQuestionsTool, AskQuestionsToolData } from './askQuestionsTool.js'; import { ConfirmationTool, ConfirmationToolData, ConfirmationToolWithOptionsData } from './confirmationTool.js'; import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; +import { ResolveDebugEventDetailsTool, ResolveDebugEventDetailsToolData } from './resolveDebugEventDetailsTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -39,6 +40,9 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); + const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); + this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); + const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); let runSubagentRegistration: IDisposable | undefined; From b6deae66a947c110cac782250e0dfab0dc9b0b18 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 11:37:14 -0800 Subject: [PATCH 02/13] fixes --- .../contrib/chat/browser/chatSlashCommands.ts | 34 ++++++++++--------- .../chat/common/actions/chatContextKeys.ts | 1 - .../common/chatService/chatServiceImpl.ts | 3 ++ .../resolveDebugEventDetailsTool.ts | 3 -- .../chat/common/tools/builtinTools/tools.ts | 1 + 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 0b0db735cb2d64..0ae2d21d89b3c9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -15,12 +15,11 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IChatAgentService } from '../common/participants/chatAgents.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IChatDebugEvent, IChatDebugService } from '../common/chatDebugService.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; -import { IChatService } from '../common/chatService/chatService.js'; +import { ChatRequestQueueKind, IChatService } from '../common/chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js'; +import { IChatRequestVariableEntry } from '../common/attachments/chatVariableEntries.js'; import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; import { ChatSubmitAction, OpenModePickerAction, OpenModelPickerAction } from './actions/chatExecuteActions.js'; import { ManagePluginsAction } from './actions/chatPluginActions.js'; @@ -50,7 +49,6 @@ export class ChatSlashCommandsContribution extends Disposable { @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, @IChatDebugService chatDebugService: IChatDebugService, - @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @@ -126,22 +124,26 @@ export class ChatSlashCommandsContribution extends Disposable { detail: nls.localize('troubleshoot', "Troubleshoot the current conversation with debug events"), sortText: 'z3_troubleshoot', executeImmediately: false, + silent: true, locations: [ChatAgentLocation.Chat], - }, async (_prompt, progress, _history, _location, sessionResource) => { - ChatContextKeys.troubleshootActive.bindTo(contextKeyService).set(true); + }, async (prompt, _progress, _history, _location, sessionResource) => { const events = chatDebugService.getEvents(sessionResource); - if (events.length === 0) { - progress.report({ content: new MarkdownString(nls.localize('troubleshoot.noEvents', "No debug events found for this conversation.")), kind: 'markdownContent' }); - await timeout(200); - return; - } - - progress.report({ content: new MarkdownString(nls.localize('troubleshoot.header', "## Debug Events for This Conversation\n\nFound {0} debug event(s). Use the `resolveDebugEventDetails` tool in follow-up messages to inspect specific events by ID.\n", events.length)), kind: 'markdownContent' }); + const summary = events.length > 0 + ? formatDebugEventsForContext(events) + : nls.localize('troubleshoot.noEvents', "No debug events found for this conversation."); - const summary = formatDebugEventsForContext(events); - progress.report({ content: new MarkdownString('```\n' + summary + '\n```'), kind: 'markdownContent' }); + const attachedContext: IChatRequestVariableEntry[] = [{ + id: 'chatDebugEvents', + name: nls.localize('troubleshoot.contextName', "Debug Events"), + kind: 'generic', + value: summary, + modelDescription: 'These are the debug event logs from the current chat conversation. Use them to help answer the user\'s troubleshooting question. You can invoke the resolveDebugEventDetails tool with an event ID to get full details for a specific event.', + }]; - await timeout(200); + chatService.sendRequest(sessionResource, prompt, { + attachedContext, + queue: ChatRequestQueueKind.Queued, + }); })); this._store.add(slashCommandService.registerSlashCommand({ command: 'agents', diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index fcb3db2aec1f80..a85c46b7bfba0f 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -89,7 +89,6 @@ export namespace ChatContextKeys { export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); export const hasPendingRequests = new RawContextKey('chatHasPendingRequests', false, { type: 'boolean', description: localize('chatHasPendingRequests', "True when there are pending requests in the queue.") }); export const chatSessionHasDebugData = new RawContextKey('chatSessionHasDebugData', false, { type: 'boolean', description: localize('chatSessionHasDebugData', "True when the current chat session has debug log data.") }); - export const troubleshootActive = new RawContextKey('chatTroubleshootActive', false, { type: 'boolean', description: localize('chatTroubleshootActive', "True when the /troubleshoot slash command has been used in the current session.") }); export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index c2393a4b9cd98f..44624239021fd1 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1146,6 +1146,9 @@ export class ChatService extends Disposable implements IChatService { if ((token.isCancellationRequested && !rawResult)) { return; } else if (!request) { + // Silent slash command completed successfully — allow queued + // requests to proceed. + shouldProcessPending = !token.isCancellationRequested; return; } else { if (!rawResult) { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts index a0c56802b5f12b..e56466399040b9 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { ChatContextKeys } from '../../actions/chatContextKeys.js'; import { IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; @@ -17,7 +15,6 @@ export const ResolveDebugEventDetailsToolData: IToolData = { canBeReferencedInPrompt: false, modelDescription: 'Resolves the full details for a specific chat debug event by its event ID. Use this tool to get detailed information about a debug event such as tool call input/output, model turn details, user message sections, or file lists. The event ID can be found in the debug event log summary provided in the conversation context.', source: ToolDataSource.Internal, - when: ContextKeyExpr.equals(ChatContextKeys.troubleshootActive.key, true), inputSchema: { type: 'object', properties: { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index d5ad07903aab7d..619e63406dd363 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -42,6 +42,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); + this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData)); const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); From bbb5b32129beab9e884c7479750c9364d3700925 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 12:14:38 -0800 Subject: [PATCH 03/13] update --- .../contrib/chat/browser/chatSlashCommands.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 0ae2d21d89b3c9..47b79a9611cc53 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -7,6 +7,7 @@ import { timeout } from '../../../../base/common/async.js'; import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { constObservable } from '../../../../base/common/observable.js'; import * as nls from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -14,7 +15,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IChatAgentService } from '../common/participants/chatAgents.js'; +import { IChatAgentService, UserSelectedTools } from '../common/participants/chatAgents.js'; import { IChatDebugEvent, IChatDebugService } from '../common/chatDebugService.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; import { ChatRequestQueueKind, IChatService } from '../common/chatService/chatService.js'; @@ -31,8 +32,9 @@ import { CONFIGURE_PROMPTS_ACTION_ID } from './promptSyntax/runPromptAction.js'; import { CONFIGURE_SKILLS_ACTION_ID } from './promptSyntax/skillActions.js'; import { AutoApproveStorageKeys, - globalAutoApproveDescription + globalAutoApproveDescription, } from './tools/languageModelToolsService.js'; +import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; import { Target } from '../common/promptSyntax/service/promptsService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; @@ -49,6 +51,7 @@ export class ChatSlashCommandsContribution extends Disposable { @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, @IChatDebugService chatDebugService: IChatDebugService, + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, @IConfigurationService configurationService: IConfigurationService, @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @@ -137,12 +140,26 @@ export class ChatSlashCommandsContribution extends Disposable { name: nls.localize('troubleshoot.contextName', "Debug Events"), kind: 'generic', value: summary, - modelDescription: 'These are the debug event logs from the current chat conversation. Use them to help answer the user\'s troubleshooting question. You can invoke the resolveDebugEventDetails tool with an event ID to get full details for a specific event.', + modelDescription: 'These are the debug event logs from the current chat conversation. Analyze them to help answer the user\'s troubleshooting question.\n' + + '\n' + + 'CRITICAL INSTRUCTION: You MUST call the resolveDebugEventDetails tool on relevant events BEFORE answering. The log lines below are only summaries — they do NOT contain the actual data (file paths, prompt content, tool I/O, etc.). The real information is only available by resolving events. Never answer based solely on the summary lines. Always resolve first, then answer.\n' + + '\n' + + 'Call resolveDebugEventDetails in parallel on all events that could be relevant to the user\'s question. When in doubt, resolve more events rather than fewer.\n' + + '\n' + + 'Event types and what resolving them returns:\n' + + '- generic (category: "discovery"): File discovery for instructions, skills, agents, hooks. Resolving returns a fileList with full file paths, load status, skip reasons, and source folders. Always resolve these for questions about customization files.\n' + + '- userMessage: The full prompt sent to the model. Resolving returns the complete message and all prompt sections (system prompt, instructions, context). Essential for understanding what the model received.\n' + + '- agentResponse: The model\'s response. Resolving returns the full response text and sections.\n' + + '- modelTurn: An LLM round-trip. Resolving returns model name, token usage, timing, errors, and prompt sections.\n' + + '- toolCall: A tool invocation. Resolving returns tool name, input, output, status, and duration.\n' + + '- subagentInvocation: A sub-agent spawn. Resolving returns agent name, status, duration, and counts.\n' + + '- generic (other): Miscellaneous logs. Resolving returns additional text details.', }]; chatService.sendRequest(sessionResource, prompt, { attachedContext, queue: ChatRequestQueueKind.Queued, + userSelectedTools: constObservable(snapshotUserSelectedTools(toolsService)), }); })); this._store.add(slashCommandService.registerSlashCommand({ @@ -398,3 +415,11 @@ function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string } return lines.join('\n'); } + +function snapshotUserSelectedTools(toolsService: ILanguageModelToolsService): UserSelectedTools { + const result: UserSelectedTools = {}; + for (const tool of toolsService.getTools(undefined)) { + result[tool.id] = true; + } + return result; +} From 06233cd20a51ebaedc4fba228ddcc881b0a5306e Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 12:22:47 -0800 Subject: [PATCH 04/13] PR --- .../resolveDebugEventDetailsTool.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts index e56466399040b9..3faa5a8cb7614a 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { localize } from '../../../../../../nls.js'; import { IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; @@ -11,7 +12,7 @@ export const ResolveDebugEventDetailsToolId = 'vscode_resolveDebugEventDetails_i export const ResolveDebugEventDetailsToolData: IToolData = { id: ResolveDebugEventDetailsToolId, - displayName: 'Resolve Debug Event Details', + displayName: localize('resolveDebugEventDetails.displayName', "Resolve Debug Event Details"), canBeReferencedInPrompt: false, modelDescription: 'Resolves the full details for a specific chat debug event by its event ID. Use this tool to get detailed information about a debug event such as tool call input/output, model turn details, user message sections, or file lists. The event ID can be found in the debug event log summary provided in the conversation context.', source: ToolDataSource.Internal, @@ -109,6 +110,20 @@ export class ResolveDebugEventDetailsTool implements IToolImpl { }; } + const sessionResource = invocation.context?.sessionResource; + if (!sessionResource) { + return { + content: [{ kind: 'text', value: 'Error: no chat session context available.' }], + }; + } + + const sessionEvents = this.chatDebugService.getEvents(sessionResource); + if (!sessionEvents.some(e => e.id === eventId)) { + return { + content: [{ kind: 'text', value: `No event with ID "${eventId}" found in the current session.` }], + }; + } + const resolved = await this.chatDebugService.resolveEvent(eventId); if (!resolved) { return { From 4b3d90d924d4bb5ec2002c00e6b3d74dbc49e892 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 12:50:03 -0800 Subject: [PATCH 05/13] fix --- src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 47b79a9611cc53..3e4b19c77206a0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -130,6 +130,7 @@ export class ChatSlashCommandsContribution extends Disposable { silent: true, locations: [ChatAgentLocation.Chat], }, async (prompt, _progress, _history, _location, sessionResource) => { + await chatDebugService.invokeProviders(sessionResource); const events = chatDebugService.getEvents(sessionResource); const summary = events.length > 0 ? formatDebugEventsForContext(events) From 31b85e4c1bcaa6b91f165f43d9ce5641ae6f4db7 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 16:39:50 -0800 Subject: [PATCH 06/13] PR --- .../attachments/chatAttachmentWidgets.ts | 8 +++ .../contrib/chat/browser/chatSlashCommands.ts | 13 ++++- .../input/editor/chatInputCompletions.ts | 4 +- .../common/attachments/chatVariableEntries.ts | 11 +++- .../resolveDebugEventDetailsTool.ts | 55 +++++++++++++++++-- .../chat/common/tools/builtinTools/tools.ts | 2 +- 6 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index c1763a79436c13..310d7c1b6258e6 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -640,6 +640,14 @@ 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, () => { + this.commandService.executeCommand('workbench.action.chat.openAgentDebugPanelForSession', attachment.sessionResource); + })); + } + // Setup tooltip hover for string context attachments if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) { this._setupTooltipHover(attachment.tooltip); diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 3e4b19c77206a0..0a9b21e2ce96a5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -124,7 +124,7 @@ export class ChatSlashCommandsContribution extends Disposable { } this._store.add(slashCommandService.registerSlashCommand({ command: 'troubleshoot', - detail: nls.localize('troubleshoot', "Troubleshoot the current conversation with debug events"), + detail: nls.localize('troubleshoot', "Troubleshoot with a snapshot of debug events from the conversation so far (run again to refresh)"), sortText: 'z3_troubleshoot', executeImmediately: false, silent: true, @@ -138,8 +138,10 @@ export class ChatSlashCommandsContribution extends Disposable { const attachedContext: IChatRequestVariableEntry[] = [{ id: 'chatDebugEvents', - name: nls.localize('troubleshoot.contextName', "Debug Events"), - kind: 'generic', + name: nls.localize('troubleshoot.contextName', "Debug Events Snapshot"), + kind: 'debugEvents', + snapshotTime: Date.now(), + sessionResource, value: summary, modelDescription: 'These are the debug event logs from the current chat conversation. Analyze them to help answer the user\'s troubleshooting question.\n' + '\n' @@ -412,6 +414,11 @@ function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string case 'agentResponse': lines.push(`[${ts}]${id} AGENT_RESPONSE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); break; + default: { + const _: never = event; + void _; + break; + } } } return lines.join('\n'); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index a03b216363ceb4..b8e354773438bf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -143,7 +143,7 @@ class SlashCommandCompletions extends Disposable { .map((c, i): CompletionItem => { const withSlash = `/${c.command}`; return { - label: withSlash, + label: { label: withSlash, description: c.detail }, insertText: c.executeImmediately ? '' : `${withSlash} `, documentation: c.detail, range, @@ -187,7 +187,7 @@ class SlashCommandCompletions extends Disposable { suggestions: slashCommands.map((c, i): CompletionItem => { const withSlash = `${chatSubcommandLeader}${c.command}`; return { - label: withSlash, + label: { label: withSlash, description: c.detail }, insertText: c.executeImmediately ? '' : `${withSlash} `, documentation: c.detail, range, diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 5655cc695fd95d..fbf760fe84d670 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -308,13 +308,22 @@ export interface IAgentFeedbackVariableEntry extends IBaseChatRequestVariableEnt }>; } +export interface IChatRequestDebugEventsVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'debugEvents'; + /** Timestamp when the debug events were snapshotted. */ + readonly snapshotTime: number; + /** The session resource these debug events belong to. */ + readonly sessionResource: URI; +} + export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry | IChatRequestToolSetEntry | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry | IPromptFileVariableEntry | IPromptTextVariableEntry | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry | ITerminalVariableEntry - | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry | IDebugVariableEntry | IAgentFeedbackVariableEntry; + | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry | IDebugVariableEntry | IAgentFeedbackVariableEntry + | IChatRequestDebugEventsVariableEntry; export namespace IChatRequestVariableEntry { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts index 3faa5a8cb7614a..bb50742dc1d0c7 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -5,8 +5,8 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { localize } from '../../../../../../nls.js'; -import { IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; -import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; +import { IChatDebugEvent, IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; export const ResolveDebugEventDetailsToolId = 'vscode_resolveDebugEventDetails_internal'; @@ -94,6 +94,30 @@ function formatResolvedContent(content: IChatDebugResolvedEventContent): string } return lines.join('\n'); } + default: { + const _: never = content; + return JSON.stringify(_); + } + } +} + +function truncate(text: string, maxLength = 30): string { + if (text.length <= maxLength) { + return text; + } + const lastSpace = text.lastIndexOf(' ', maxLength); + const cutoff = lastSpace > maxLength / 2 ? lastSpace : maxLength; + return text.substring(0, cutoff) + '\u2026'; +} + +function getEventLabel(event: IChatDebugEvent): string { + switch (event.kind) { + case 'generic': return event.name; + case 'toolCall': return event.toolName; + case 'modelTurn': return event.requestName ?? localize('debugEvent.modelTurn', "Model Turn"); + case 'userMessage': return localize('debugEvent.userMessage', "User Message: {0}", truncate(event.message)); + case 'agentResponse': return localize('debugEvent.agentResponse', "Agent Response: {0}", truncate(event.message)); + case 'subagentInvocation': return event.agentName; } } @@ -102,9 +126,32 @@ export class ResolveDebugEventDetailsTool implements IToolImpl { @IChatDebugService private readonly chatDebugService: IChatDebugService, ) { } + async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + const eventId = context.parameters?.eventId; + let eventLabel: string | undefined; + if (typeof eventId === 'string' && context.chatSessionResource) { + const events = this.chatDebugService.getEvents(context.chatSessionResource); + const event = events.find(e => e.id === eventId); + if (event) { + eventLabel = getEventLabel(event); + } + } + + if (eventLabel) { + return { + invocationMessage: localize('resolveDebugEventDetails.invocationMessageNamed', 'Resolving details for "{0}"', eventLabel), + pastTenseMessage: localize('resolveDebugEventDetails.pastTenseMessageNamed', 'Resolved details for "{0}"', eventLabel), + }; + } + return { + invocationMessage: localize('resolveDebugEventDetails.invocationMessage', 'Resolving debug event details'), + pastTenseMessage: localize('resolveDebugEventDetails.pastTenseMessage', 'Resolved debug event details'), + }; + } + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { - const eventId = invocation.parameters['eventId'] as string; - if (!eventId) { + const eventId = invocation.parameters['eventId']; + if (typeof eventId !== 'string' || !eventId) { return { content: [{ kind: 'text', value: 'Error: eventId parameter is required.' }], }; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 619e63406dd363..a04e6304ab7159 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -40,7 +40,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); - const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); + const resolveDebugEventDetailsTool = this._register(instantiationService.createInstance(ResolveDebugEventDetailsTool)); this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData)); From 92ddee3c7c02a526935e9416feb3d78b257a70a4 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 17:10:56 -0800 Subject: [PATCH 07/13] filters etc. --- .../actions/chatOpenAgentDebugPanelAction.ts | 4 +- .../attachments/chatAttachmentWidgets.ts | 2 +- .../chat/browser/chatDebug/chatDebugEditor.ts | 10 ++- .../browser/chatDebug/chatDebugFilters.ts | 83 +++++++++++++++++++ .../browser/chatDebug/chatDebugLogsView.ts | 32 ++++++- .../chat/browser/chatDebug/chatDebugTypes.ts | 2 + .../browser/chatDebug/media/chatDebug.css | 7 ++ .../contrib/chat/browser/chatSlashCommands.ts | 2 + 8 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts index 9fefe82c3acf26..9878790697cec8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts @@ -67,7 +67,7 @@ export function registerChatOpenAgentDebugPanelAction() { }); } - async run(accessor: ServicesAccessor, context?: URI | unknown): Promise { + async run(accessor: ServicesAccessor, context?: URI | unknown, filterBeforeTimestamp?: number): Promise { const editorService = accessor.get(IEditorService); const chatWidgetService = accessor.get(IChatWidgetService); const chatDebugService = accessor.get(IChatDebugService); @@ -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', filterBeforeTimestamp }; await editorService.openEditor(ChatDebugEditorInput.instance, options); } }); diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 310d7c1b6258e6..9bc17d3294fc06 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -644,7 +644,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { if (attachment.kind === 'debugEvents') { this.element.style.cursor = 'pointer'; this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => { - this.commandService.executeCommand('workbench.action.chat.openAgentDebugPanelForSession', attachment.sessionResource); + this.commandService.executeCommand('workbench.action.chat.openAgentDebugPanelForSession', attachment.sessionResource, attachment.snapshotTime); })); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index e9b7160ea45b92..4993e3bce03dca 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -341,7 +341,7 @@ export class ChatDebugEditor extends EditorPane { } private _applyNavigationOptions(options: IChatDebugEditorOptions): void { - const { sessionResource, viewHint } = options; + const { sessionResource, viewHint, filterBeforeTimestamp } = options; if (viewHint === 'logs' && sessionResource) { this.navigateToSession(sessionResource, 'logs'); } else if (viewHint === 'flowchart' && sessionResource) { @@ -356,6 +356,14 @@ export class ChatDebugEditor extends EditorPane { } else if (this.viewState === ViewState.Home) { this.showView(ViewState.Home); } + + // Apply before-timestamp filter if provided (e.g. from debug events snapshot) + if (filterBeforeTimestamp !== undefined && this.filterState) { + const d = new Date(filterBeforeTimestamp); + const filterText = `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.filterState.setTextFilter(filterText); + this.logsView?.setFilterText(filterText); + } } override layout(dimension: Dimension): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts index 8f25da718a1c15..9b8453ea334b89 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts @@ -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; @@ -70,10 +74,89 @@ 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:HH:MM:SS`, `before:YYYY-MM-DD`, or `before:YYYY-MM-DDTHH:MM:SS` + * (ISO 8601) from the filter text. + */ + 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 { + // For 'before:', round up to include the entire second (ms=999). + // For 'after:', use the start of the second (ms=0). + const ms = prefix === 'before' ? 999 : 0; + + // Full ISO 8601: before:YYYY-MM-DDTHH:MM:SS or before:YYYY-MM-DDTHH:MM + const fullRegex = new RegExp(`${prefix}:(\\d{4})-(\\d{2})-(\\d{2})t(\\d{1,2}):(\\d{2})(?::(\\d{2}))?`); + const fullMatch = fullRegex.exec(text); + if (fullMatch) { + const d = new Date( + parseInt(fullMatch[1], 10), parseInt(fullMatch[2], 10) - 1, parseInt(fullMatch[3], 10), + parseInt(fullMatch[4], 10), parseInt(fullMatch[5], 10), fullMatch[6] ? parseInt(fullMatch[6], 10) : 0, ms + ); + return d.getTime(); + } + + // Date-only ISO 8601: before:YYYY-MM-DD (end of that day for before, start for after) + const dateRegex = new RegExp(`${prefix}:(\\d{4})-(\\d{2})-(\\d{2})(?!\\d|t)`); + const dateMatch = dateRegex.exec(text); + if (dateMatch) { + const year = parseInt(dateMatch[1], 10); + const month = parseInt(dateMatch[2], 10) - 1; + const day = parseInt(dateMatch[3], 10); + if (prefix === 'before') { + return new Date(year, month, day, 23, 59, 59, 999).getTime(); + } + return new Date(year, month, day, 0, 0, 0, 0).getTime(); + } + + // Time-only: before:HH:MM:SS or before:HH:MM (relative to today) + const timeRegex = new RegExp(`${prefix}:(\\d{1,2}):(\\d{2})(?::(\\d{2}))?`); + const timeMatch = timeRegex.exec(text); + if (timeMatch) { + const now = new Date(); + const d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), + parseInt(timeMatch[1], 10), parseInt(timeMatch[2], 10), timeMatch[3] ? parseInt(timeMatch[3], 10) : 0, ms); + return d.getTime(); + } + + return undefined; + } + + /** 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, '') + .replace(/\b(?:before|after):\d{4}-\d{2}-\d{2}\b/g, '') + .replace(/\b(?:before|after):\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(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index 5fa4cde6b6f52d..d22d809a64d3b0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -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.$; @@ -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')); @@ -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"), })); @@ -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', "Troubleshoot with 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); })); @@ -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(); @@ -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()); diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts index c34697347125ec..75f9ac68f6db1c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts @@ -19,6 +19,8 @@ const $ = DOM.$; export interface IChatDebugEditorOptions extends IEditorOptions { readonly sessionResource?: URI; readonly viewHint?: 'home' | 'overview' | 'logs' | 'flowchart'; + /** When set, automatically filters logs to events before this timestamp (epoch ms). */ + readonly filterBeforeTimestamp?: number; } export const enum ViewState { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css b/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css index 93068162a11ebf..7efa2352ee7cc9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css @@ -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; @@ -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; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 0a9b21e2ce96a5..e4b3cbffd57a97 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -149,6 +149,8 @@ export class ChatSlashCommandsContribution extends Disposable { + '\n' + 'Call resolveDebugEventDetails in parallel on all events that could be relevant to the user\'s question. When in doubt, resolve more events rather than fewer.\n' + '\n' + + 'IMPORTANT: Do NOT mention event IDs, tool resolution steps, or internal debug mechanics in your response. The user does not know about debug events or event IDs. Present your findings directly and naturally, as if you simply know the answer. Never say things like "I need to resolve events" or show event IDs.\n' + + '\n' + 'Event types and what resolving them returns:\n' + '- generic (category: "discovery"): File discovery for instructions, skills, agents, hooks. Resolving returns a fileList with full file paths, load status, skip reasons, and source folders. Always resolve these for questions about customization files.\n' + '- userMessage: The full prompt sent to the model. Resolving returns the complete message and all prompt sections (system prompt, instructions, context). Essential for understanding what the model received.\n' From fa9b2fd422c442234fe9fda962e7f6da5c55e1a8 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 17:24:39 -0800 Subject: [PATCH 08/13] PR --- .../actions/chatOpenAgentDebugPanelAction.ts | 4 +- .../attachments/chatAttachmentWidgets.ts | 4 +- .../chat/browser/chatDebug/chatDebugEditor.ts | 12 +- .../browser/chatDebug/chatDebugFilters.ts | 81 +++---- .../browser/chatDebug/chatDebugLogsView.ts | 2 +- .../chat/browser/chatDebug/chatDebugTypes.ts | 4 +- .../test/browser/chatDebugFilters.test.ts | 220 ++++++++++++++++++ 7 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts index 9878790697cec8..860559d64e3d23 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts @@ -67,7 +67,7 @@ export function registerChatOpenAgentDebugPanelAction() { }); } - async run(accessor: ServicesAccessor, context?: URI | unknown, filterBeforeTimestamp?: number): Promise { + async run(accessor: ServicesAccessor, context?: URI | unknown, filter?: string): Promise { const editorService = accessor.get(IEditorService); const chatWidgetService = accessor.get(IChatWidgetService); const chatDebugService = accessor.get(IChatDebugService); @@ -88,7 +88,7 @@ export function registerChatOpenAgentDebugPanelAction() { } chatDebugService.activeSessionResource = sessionResource; - const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs', filterBeforeTimestamp }; + const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs', filter }; await editorService.openEditor(ChatDebugEditorInput.instance, options); } }); diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 9bc17d3294fc06..8a20bd70982829 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -644,7 +644,9 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { if (attachment.kind === 'debugEvents') { this.element.style.cursor = 'pointer'; this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => { - this.commandService.executeCommand('workbench.action.chat.openAgentDebugPanelForSession', attachment.sessionResource, attachment.snapshotTime); + 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); })); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index 4993e3bce03dca..ec403b30f956d6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -341,7 +341,7 @@ export class ChatDebugEditor extends EditorPane { } private _applyNavigationOptions(options: IChatDebugEditorOptions): void { - const { sessionResource, viewHint, filterBeforeTimestamp } = options; + const { sessionResource, viewHint, filter } = options; if (viewHint === 'logs' && sessionResource) { this.navigateToSession(sessionResource, 'logs'); } else if (viewHint === 'flowchart' && sessionResource) { @@ -357,12 +357,10 @@ export class ChatDebugEditor extends EditorPane { this.showView(ViewState.Home); } - // Apply before-timestamp filter if provided (e.g. from debug events snapshot) - if (filterBeforeTimestamp !== undefined && this.filterState) { - const d = new Date(filterBeforeTimestamp); - const filterText = `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.filterState.setTextFilter(filterText); - this.logsView?.setFilterText(filterText); + // Apply filter text if provided (e.g. from debug events snapshot) + if (filter !== undefined && this.filterState) { + this.filterState.setTextFilter(filter); + this.logsView?.setFilterText(filter); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts index 9b8453ea334b89..fefa283cceb641 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts @@ -87,8 +87,8 @@ export class ChatDebugFilterState extends Disposable { } /** - * Parse `before:HH:MM:SS`, `before:YYYY-MM-DD`, or `before:YYYY-MM-DDTHH:MM:SS` - * (ISO 8601) from the filter text. + * 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'); @@ -96,53 +96,54 @@ export class ChatDebugFilterState extends Disposable { } static parseTimeToken(text: string, prefix: string): number | undefined { - // For 'before:', round up to include the entire second (ms=999). - // For 'after:', use the start of the second (ms=0). - const ms = prefix === 'before' ? 999 : 0; - - // Full ISO 8601: before:YYYY-MM-DDTHH:MM:SS or before:YYYY-MM-DDTHH:MM - const fullRegex = new RegExp(`${prefix}:(\\d{4})-(\\d{2})-(\\d{2})t(\\d{1,2}):(\\d{2})(?::(\\d{2}))?`); - const fullMatch = fullRegex.exec(text); - if (fullMatch) { - const d = new Date( - parseInt(fullMatch[1], 10), parseInt(fullMatch[2], 10) - 1, parseInt(fullMatch[3], 10), - parseInt(fullMatch[4], 10), parseInt(fullMatch[5], 10), fullMatch[6] ? parseInt(fullMatch[6], 10) : 0, ms - ); - return d.getTime(); + 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; } - // Date-only ISO 8601: before:YYYY-MM-DD (end of that day for before, start for after) - const dateRegex = new RegExp(`${prefix}:(\\d{4})-(\\d{2})-(\\d{2})(?!\\d|t)`); - const dateMatch = dateRegex.exec(text); - if (dateMatch) { - const year = parseInt(dateMatch[1], 10); - const month = parseInt(dateMatch[2], 10) - 1; - const day = parseInt(dateMatch[3], 10); - if (prefix === 'before') { - return new Date(year, month, day, 23, 59, 59, 999).getTime(); + 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(); } - return new Date(year, month, day, 0, 0, 0, 0).getTime(); + } else { + return new Date( + year, + month ?? 0, + day ?? 1, + hour ?? 0, + minute ?? 0, + second ?? 0, + 0, + ).getTime(); } - - // Time-only: before:HH:MM:SS or before:HH:MM (relative to today) - const timeRegex = new RegExp(`${prefix}:(\\d{1,2}):(\\d{2})(?::(\\d{2}))?`); - const timeMatch = timeRegex.exec(text); - if (timeMatch) { - const now = new Date(); - const d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), - parseInt(timeMatch[1], 10), parseInt(timeMatch[2], 10), timeMatch[3] ? parseInt(timeMatch[3], 10) : 0, ms); - return d.getTime(); - } - - return undefined; } /** 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, '') - .replace(/\b(?:before|after):\d{4}-\d{2}-\d{2}\b/g, '') - .replace(/\b(?:before|after):\d{1,2}:\d{2}(?::\d{2})?\b/g, '') + .replace(/\b(?:before|after):\d{4}(?:-\d{2}(?:-\d{2}(?:t\d{1,2}(?::\d{2}(?::\d{2})?)?)?)?)?\b/g, '') .trim(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index d22d809a64d3b0..97e037992d3c09 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -122,7 +122,7 @@ export class ChatDebugLogsView extends Disposable { filterContainer.appendChild(this.filterWidget.element); // Troubleshoot button - const troubleshootButton = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.troubleshoot', "Troubleshoot with Chat") })); + const troubleshootButton = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.troubleshoot', "Add event logs 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 () => { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts index 75f9ac68f6db1c..a6ac1bc9799724 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts @@ -19,8 +19,8 @@ const $ = DOM.$; export interface IChatDebugEditorOptions extends IEditorOptions { readonly sessionResource?: URI; readonly viewHint?: 'home' | 'overview' | 'logs' | 'flowchart'; - /** When set, automatically filters logs to events before this timestamp (epoch ms). */ - readonly filterBeforeTimestamp?: number; + /** When set, automatically applies this text as the log filter. */ + readonly filter?: string; } export const enum ViewState { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts new file mode 100644 index 00000000000000..9bb37a5f829638 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ChatDebugFilterState } from '../../browser/chatDebug/chatDebugFilters.js'; + +suite('ChatDebugFilterState', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('parseTimeToken', () => { + + suite('before: prefix', () => { + + test('year only — rounds to end of year', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026', 'before'); + assert.strictEqual(result, new Date(2026, 11, 31, 23, 59, 59, 999).getTime()); + }); + + test('year-month — rounds to end of month', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03', 'before'); + // new Date(2026, 3, 0) gives last day of March + assert.strictEqual(result, new Date(2026, 3, 0, 23, 59, 59, 999).getTime()); + }); + + test('year-month (February, non-leap) — rounds to end of Feb', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2025-02', 'before'); + assert.strictEqual(result, new Date(2025, 2, 0, 23, 59, 59, 999).getTime()); + }); + + test('year-month-day — rounds to end of day', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 23, 59, 59, 999).getTime()); + }); + + test('date with hour only — rounds to end of hour', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03t14', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 59, 59, 999).getTime()); + }); + + test('date with hour:minute — rounds to end of minute', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03t14:30', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 59, 999).getTime()); + }); + + test('full date-time with seconds', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03t14:30:45', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 45, 999).getTime()); + }); + }); + + suite('after: prefix', () => { + + test('year only — start of year', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026', 'after'); + assert.strictEqual(result, new Date(2026, 0, 1, 0, 0, 0, 0).getTime()); + }); + + test('year-month — start of month', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03', 'after'); + assert.strictEqual(result, new Date(2026, 2, 1, 0, 0, 0, 0).getTime()); + }); + + test('year-month-day — start of day', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 0, 0, 0, 0).getTime()); + }); + + test('date with hour only — start of hour', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03t14', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 0, 0, 0).getTime()); + }); + + test('date with hour:minute — start of minute', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03t14:30', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 0, 0).getTime()); + }); + + test('full date-time with seconds', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03t14:30:45', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 45, 0).getTime()); + }); + }); + + suite('no match', () => { + + test('returns undefined for empty string', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('', 'before'), undefined); + }); + + test('returns undefined for unrelated text', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('hello world', 'before'), undefined); + }); + + test('returns undefined for wrong prefix', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('after:2026', 'before'), undefined); + }); + + test('returns undefined for bare time without date', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('before:14:30', 'before'), undefined); + }); + }); + + suite('embedded in text', () => { + + test('extracts token from surrounding text', () => { + const result = ChatDebugFilterState.parseTimeToken('some text before:2026-03-03 more text', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 23, 59, 59, 999).getTime()); + }); + + test('handles both before and after in same string', () => { + const text = 'after:2026-01 before:2026-03'; + const after = ChatDebugFilterState.parseTimeToken(text, 'after'); + const before = ChatDebugFilterState.parseTimeToken(text, 'before'); + assert.strictEqual(after, new Date(2026, 0, 1, 0, 0, 0, 0).getTime()); + assert.strictEqual(before, new Date(2026, 3, 0, 23, 59, 59, 999).getTime()); + }); + }); + }); + + suite('setTextFilter and timestamp parsing', () => { + let state: ChatDebugFilterState; + + setup(() => { + state = disposables.add(new ChatDebugFilterState()); + }); + + test('sets beforeTimestamp and afterTimestamp from text', () => { + state.setTextFilter('after:2026-01-01 before:2026-12-31'); + assert.strictEqual(state.afterTimestamp, new Date(2026, 0, 1, 0, 0, 0, 0).getTime()); + assert.strictEqual(state.beforeTimestamp, new Date(2026, 11, 31, 23, 59, 59, 999).getTime()); + }); + + test('clears timestamps when tokens removed', () => { + state.setTextFilter('before:2026'); + assert.ok(state.beforeTimestamp !== undefined); + state.setTextFilter('hello'); + assert.strictEqual(state.beforeTimestamp, undefined); + }); + }); + + suite('textFilterWithoutTimestamps', () => { + let state: ChatDebugFilterState; + + setup(() => { + state = disposables.add(new ChatDebugFilterState()); + }); + + test('strips year-only token', () => { + state.setTextFilter('before:2026 hello'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('strips year-month token', () => { + state.setTextFilter('after:2026-03 hello'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('strips full date-time token', () => { + state.setTextFilter('before:2026-03-03t14:30:45 hello'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('strips multiple tokens', () => { + state.setTextFilter('after:2026-01 hello before:2026-12'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('returns empty when only tokens', () => { + state.setTextFilter('before:2026'); + assert.strictEqual(state.textFilterWithoutTimestamps, ''); + }); + }); + + suite('isTimestampVisible', () => { + let state: ChatDebugFilterState; + + setup(() => { + state = disposables.add(new ChatDebugFilterState()); + }); + + test('visible when no timestamp filters set', () => { + assert.strictEqual(state.isTimestampVisible(new Date(2026, 5, 15)), true); + }); + + test('hidden when after beforeTimestamp', () => { + state.setTextFilter('before:2026-03'); + // April 1st is after end of March + assert.strictEqual(state.isTimestampVisible(new Date(2026, 3, 1)), false); + }); + + test('visible when before beforeTimestamp', () => { + state.setTextFilter('before:2026-03'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 1, 15)), true); + }); + + test('hidden when before afterTimestamp', () => { + state.setTextFilter('after:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 4, 31)), false); + }); + + test('visible when after afterTimestamp', () => { + state.setTextFilter('after:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 6, 1)), true); + }); + + test('visible when within before/after range', () => { + state.setTextFilter('after:2026-03 before:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 3, 15)), true); + }); + + test('hidden when outside before/after range', () => { + state.setTextFilter('after:2026-03 before:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 0, 1)), false); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 8, 1)), false); + }); + }); +}); From b46532466b6bb84b805ae9b3f7e40c64cb3e8d73 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 17:37:54 -0800 Subject: [PATCH 09/13] update --- .../contrib/chat/browser/chatDebug/chatDebugLogsView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index 97e037992d3c09..a9bbec0b6d53f9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -122,7 +122,7 @@ export class ChatDebugLogsView extends Disposable { filterContainer.appendChild(this.filterWidget.element); // Troubleshoot button - const troubleshootButton = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.troubleshoot', "Add event logs snapshot to Chat") })); + 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 () => { From 597120d0a7b383290d46bcf94b4246c7aff7080c Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 18:41:05 -0800 Subject: [PATCH 10/13] fixes --- .../contrib/chat/browser/chatSlashCommands.ts | 23 ++++++++----------- .../chatReferencesContentPart.ts | 3 +-- .../chat/common/actions/chatContextKeys.ts | 1 + .../common/chatService/chatServiceImpl.ts | 2 +- .../common/participants/chatSlashCommands.ts | 10 ++++---- .../resolveDebugEventDetailsTool.ts | 3 +++ 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index e4b3cbffd57a97..f9cb03fa6b143c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -7,7 +7,6 @@ import { timeout } from '../../../../base/common/async.js'; import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { constObservable } from '../../../../base/common/observable.js'; import * as nls from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -15,7 +14,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IChatAgentService, UserSelectedTools } from '../common/participants/chatAgents.js'; +import { IChatAgentService } from '../common/participants/chatAgents.js'; import { IChatDebugEvent, IChatDebugService } from '../common/chatDebugService.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; import { ChatRequestQueueKind, IChatService } from '../common/chatService/chatService.js'; @@ -35,6 +34,8 @@ import { globalAutoApproveDescription, } from './tools/languageModelToolsService.js'; import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; import { Target } from '../common/promptSyntax/service/promptsService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; @@ -51,12 +52,13 @@ export class ChatSlashCommandsContribution extends Disposable { @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, @IChatDebugService chatDebugService: IChatDebugService, - @ILanguageModelToolsService toolsService: ILanguageModelToolsService, @IConfigurationService configurationService: IConfigurationService, @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ @@ -129,7 +131,9 @@ export class ChatSlashCommandsContribution extends Disposable { executeImmediately: false, silent: true, locations: [ChatAgentLocation.Chat], - }, async (prompt, _progress, _history, _location, sessionResource) => { + }, async (prompt, _progress, _history, _location, sessionResource, _token, options) => { + ChatContextKeys.chatSessionHasTroubleshootData.bindTo(this.contextKeyService).set(true); + languageModelToolsService.flushToolUpdates(); await chatDebugService.invokeProviders(sessionResource); const events = chatDebugService.getEvents(sessionResource); const summary = events.length > 0 @@ -162,9 +166,9 @@ export class ChatSlashCommandsContribution extends Disposable { }]; chatService.sendRequest(sessionResource, prompt, { - attachedContext, + ...options, queue: ChatRequestQueueKind.Queued, - userSelectedTools: constObservable(snapshotUserSelectedTools(toolsService)), + attachedContext, }); })); this._store.add(slashCommandService.registerSlashCommand({ @@ -426,10 +430,3 @@ function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string return lines.join('\n'); } -function snapshotUserSelectedTools(toolsService: ILanguageModelToolsService): UserSelectedTools { - const result: UserSelectedTools = {}; - for (const tool of toolsService.getTools(undefined)) { - result[tool.id] = true; - } - return result; -} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts index 6ff863773dbbb5..b0edcc5b8603f6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -389,8 +389,7 @@ class CollapsibleListRenderer implements IListRenderer('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); export const hasPendingRequests = new RawContextKey('chatHasPendingRequests', false, { type: 'boolean', description: localize('chatHasPendingRequests', "True when there are pending requests in the queue.") }); export const chatSessionHasDebugData = new RawContextKey('chatSessionHasDebugData', false, { type: 'boolean', description: localize('chatSessionHasDebugData', "True when the current chat session has debug log data.") }); + export const chatSessionHasTroubleshootData = new RawContextKey('chatSessionHasTroubleshootData', false, { type: 'boolean', description: localize('chatSessionHasTroubleshootData', "True when the /troubleshoot slash command has been run in the current chat session.") }); export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 44624239021fd1..bca208c4605ac9 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1135,7 +1135,7 @@ export class ChatService extends Disposable implements IChatService { const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { progressCallback([p]); - }), history, location, model.sessionResource, token); + }), history, location, model.sessionResource, token, options); agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); rawResult = {}; diff --git a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts index 50f145ccf10041..9e9d145cfc29ea 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts @@ -9,7 +9,7 @@ import { Disposable, IDisposable, toDisposable } from '../../../../../base/commo import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IProgress } from '../../../../../platform/progress/common/progress.js'; import { IChatMessage } from '../languageModels.js'; -import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from '../chatService/chatService.js'; +import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData, IChatSendRequestOptions } from '../chatService/chatService.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -44,7 +44,7 @@ export interface IChatSlashData { export interface IChatSlashFragment { content: string | { treeData: IChatResponseProgressFileTreeData }; } -export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; +export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken, options?: IChatSendRequestOptions): Promise<{ followUp: IChatFollowup[] } | void> }; export const IChatSlashCommandService = createDecorator('chatSlashCommandService'); @@ -55,7 +55,7 @@ export interface IChatSlashCommandService { _serviceBrand: undefined; readonly onDidChangeCommands: Event; registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable; - executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; + executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken, options?: IChatSendRequestOptions): Promise<{ followUp: IChatFollowup[] } | void>; getCommands(location: ChatAgentLocation, mode: ChatModeKind): Array; hasCommand(id: string): boolean; } @@ -105,7 +105,7 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom return this._commands.has(id); } - async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { + async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken, options?: IChatSendRequestOptions): Promise<{ followUp: IChatFollowup[] } | void> { const data = this._commands.get(id); if (!data) { throw new Error('No command with id ${id} NOT registered'); @@ -117,6 +117,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom throw new Error(`No command with id ${id} NOT resolved`); } - return await data.command(prompt, progress, history, location, sessionResource, token); + return await data.command(prompt, progress, history, location, sessionResource, token, options); } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts index bb50742dc1d0c7..b3ff4593a43e78 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { localize } from '../../../../../../nls.js'; +import { ChatContextKeys } from '../../actions/chatContextKeys.js'; import { IChatDebugEvent, IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; @@ -12,7 +13,9 @@ export const ResolveDebugEventDetailsToolId = 'vscode_resolveDebugEventDetails_i export const ResolveDebugEventDetailsToolData: IToolData = { id: ResolveDebugEventDetailsToolId, + toolReferenceName: 'resolveDebugEventDetails', displayName: localize('resolveDebugEventDetails.displayName', "Resolve Debug Event Details"), + when: ChatContextKeys.chatSessionHasTroubleshootData, canBeReferencedInPrompt: false, modelDescription: 'Resolves the full details for a specific chat debug event by its event ID. Use this tool to get detailed information about a debug event such as tool call input/output, model turn details, user message sections, or file lists. The event ID can be found in the debug event log summary provided in the conversation context.', source: ToolDataSource.Internal, From 6b0361a661a86079a0a7a93f90519447bd3cd0aa Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 18:43:39 -0800 Subject: [PATCH 11/13] fix --- .../workbench/contrib/chat/common/tools/builtinTools/tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index a04e6304ab7159..619e63406dd363 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -40,7 +40,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); - const resolveDebugEventDetailsTool = this._register(instantiationService.createInstance(ResolveDebugEventDetailsTool)); + const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData)); From a9d009b0fa8b93226980176b4e9d83abc61479b0 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 18:53:24 -0800 Subject: [PATCH 12/13] fix --- .../contrib/chat/browser/chatSlashCommands.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index f9cb03fa6b143c..85dbb8b4890b47 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -39,6 +39,7 @@ import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; import { Target } from '../common/promptSyntax/service/promptsService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IChatWidgetService } from './chat.js'; export class ChatSlashCommandsContribution extends Disposable { @@ -59,8 +60,22 @@ export class ChatSlashCommandsContribution extends Disposable { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @IChatWidgetService chatWidgetService: IChatWidgetService, ) { super(); + + const troubleshootSessions = new Set(); + const hasTroubleshootDataKey = ChatContextKeys.chatSessionHasTroubleshootData.bindTo(this.contextKeyService); + const updateTroubleshootDataKey = () => { + const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource; + hasTroubleshootDataKey.set(!!sessionResource && troubleshootSessions.has(sessionResource.toString())); + }; + this._store.add(chatWidgetService.onDidChangeFocusedWidget(widget => { + updateTroubleshootDataKey(); + if (widget) { + this._store.add(widget.onDidChangeViewModel(() => updateTroubleshootDataKey())); + } + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'clear', detail: nls.localize('clear', "Start a new chat and archive the current one"), @@ -132,7 +147,8 @@ export class ChatSlashCommandsContribution extends Disposable { silent: true, locations: [ChatAgentLocation.Chat], }, async (prompt, _progress, _history, _location, sessionResource, _token, options) => { - ChatContextKeys.chatSessionHasTroubleshootData.bindTo(this.contextKeyService).set(true); + troubleshootSessions.add(sessionResource.toString()); + hasTroubleshootDataKey.set(true); languageModelToolsService.flushToolUpdates(); await chatDebugService.invokeProviders(sessionResource); const events = chatDebugService.getEvents(sessionResource); From 511bba30004e6ee4e4dae7d430bc609c7500f5f4 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Tue, 3 Mar 2026 19:11:37 -0800 Subject: [PATCH 13/13] clean --- .../contrib/chat/browser/chatSlashCommands.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 85dbb8b4890b47..adedacab312307 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -33,10 +33,10 @@ import { AutoApproveStorageKeys, globalAutoApproveDescription, } from './tools/languageModelToolsService.js'; +import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; -import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; import { Target } from '../common/promptSyntax/service/promptsService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IChatWidgetService } from './chat.js'; @@ -66,15 +66,10 @@ export class ChatSlashCommandsContribution extends Disposable { const troubleshootSessions = new Set(); const hasTroubleshootDataKey = ChatContextKeys.chatSessionHasTroubleshootData.bindTo(this.contextKeyService); - const updateTroubleshootDataKey = () => { + this._store.add(chatWidgetService.onDidChangeFocusedSession(() => { const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource; hasTroubleshootDataKey.set(!!sessionResource && troubleshootSessions.has(sessionResource.toString())); - }; - this._store.add(chatWidgetService.onDidChangeFocusedWidget(widget => { - updateTroubleshootDataKey(); - if (widget) { - this._store.add(widget.onDidChangeViewModel(() => updateTroubleshootDataKey())); - } + languageModelToolsService.flushToolUpdates(); })); this._store.add(slashCommandService.registerSlashCommand({ command: 'clear',