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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/vs/platform/actionWidget/browser/actionWidget.css
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@
display: block !important;
width: 100%;
margin-left: 0;
padding-left: 18px;
padding-left: 20px;
font-size: 11px;
line-height: 14px;
opacity: 0.8;
Expand Down
54 changes: 52 additions & 2 deletions src/vs/workbench/contrib/chat/browser/actions/chatContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.
import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';
import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../attachments/chatContextPickService.js';
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js';
import { isToolSet, ToolDataSource } from '../../common/tools/languageModelToolsService.js';
import { IChatWidget } from '../chat.js';
import { ILanguageModelToolsService, isToolSet, ToolDataSource } from '../../common/tools/languageModelToolsService.js';
import { IChatWidget, IChatWidgetService } from '../chat.js';
import { imageToHash, isImage } from '../widget/input/editor/chatPasteProviders.js';
import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js';
import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js';
import { createDebugEventsAttachment } from '../chatDebug/chatDebugAttachment.js';
import { IChatDebugService } from '../../common/chatDebugService.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
import { ITerminalService } from '../../../terminal/browser/terminal.js';
import { URI } from '../../../../../base/common/uri.js';
import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
Expand All @@ -38,9 +42,29 @@ export class ChatContextContributions extends Disposable implements IWorkbenchCo
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IChatContextPickService contextPickService: IChatContextPickService,
@IChatDebugService chatDebugService: IChatDebugService,
@IContextKeyService contextKeyService: IContextKeyService,
@ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService,
@IChatWidgetService chatWidgetService: IChatWidgetService,
) {
super();

// Bind at the global context key service level so the tools service can evaluate it.
// Widget-scoped keys are not reliably visible to singleton services during async request processing.
const hasAttachedDebugDataKey = ChatContextKeys.chatSessionHasAttachedDebugData.bindTo(contextKeyService);
this._store.add(chatWidgetService.onDidChangeFocusedSession(() => {
const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource;
hasAttachedDebugDataKey.set(!!sessionResource && chatDebugService.hasAttachedDebugData(sessionResource));
languageModelToolsService.flushToolUpdates();
}));
this._store.add(chatDebugService.onDidAttachDebugData(sessionResource => {
const focusedSession = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource;
if (focusedSession && focusedSession.toString() === sessionResource.toString()) {
hasAttachedDebugDataKey.set(true);
languageModelToolsService.flushToolUpdates();
}
}));

// ###############################################################################################
//
// Default context picks/values which are "native" to chat. This is NOT the complete list
Expand All @@ -54,6 +78,7 @@ export class ChatContextContributions extends Disposable implements IWorkbenchCo
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(OpenEditorContextValuePick)));
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ClipboardImageContextValuePick)));
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick)));
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(DebugEventsSnapshotContextValuePick)));
}
}

Expand Down Expand Up @@ -285,3 +310,28 @@ class ScreenshotContextValuePick implements IChatContextValueItem {
return blob && convertBufferToScreenshotVariable(blob);
}
}

class DebugEventsSnapshotContextValuePick implements IChatContextValueItem {

readonly type = 'valuePick';
readonly icon = Codicon.output;
readonly label = localize('chatContext.debugEventsSnapshot', 'Debug Events Snapshot');
readonly ordinal = -600;

constructor(
@IChatDebugService private readonly _chatDebugService: IChatDebugService,
) { }

isEnabled(widget: IChatWidget): boolean {
const sessionResource = widget.viewModel?.sessionResource;
return !!sessionResource && this._chatDebugService.getEvents(sessionResource).length > 0;
}

async asAttachment(widget: IChatWidget): Promise<IChatRequestVariableEntry | undefined> {
const sessionResource = widget.viewModel?.sessionResource;
if (!sessionResource) {
return undefined;
}
return createDebugEventsAttachment(sessionResource, this._chatDebugService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class ChatSteerWithMessageAction extends Action2 {
id: ChatSteerWithMessageAction.ID,
title: localize2('chat.steerWithMessage', "Steer with Message"),
tooltip: localize('chat.steerWithMessage.tooltip', "Send this message at the next opportunity, signaling the current request to yield"),
icon: Codicon.arrowRight,
icon: Codicon.arrowUp,
f1: false,
category: CHAT_CATEGORY,
precondition: ContextKeyExpr.and(
Expand Down Expand Up @@ -271,7 +271,7 @@ export function registerChatQueueActions(): void {
order: 1,
});
MenuRegistry.appendMenuItem(MenuId.ChatExecuteQueue, {
command: { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), icon: Codicon.arrowRight },
command: { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), icon: Codicon.arrowUp },
group: 'navigation',
order: 2,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Codicon } from '../../../../../base/common/codicons.js';
import { URI } from '../../../../../base/common/uri.js';
import * as nls from '../../../../../nls.js';
import { IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js';
import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js';

/**
* Descriptions of each debug event kind for the model. Adding a new event kind
* to {@link IChatDebugEvent} without adding an entry here will cause a compile error.
*/
const debugEventKindDescriptions: Record<IChatDebugEvent['kind'], string> = {
generic: '- 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'
+ '- generic (other): Miscellaneous logs. Resolving returns additional text details.',
toolCall: '- toolCall: A tool invocation. Resolving returns tool name, input, output, status, and duration.',
modelTurn: '- modelTurn: An LLM round-trip. Resolving returns model name, token usage, timing, errors, and prompt sections.',
subagentInvocation: '- subagentInvocation: A sub-agent spawn. Resolving returns agent name, status, duration, and counts.',
userMessage: '- 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.',
agentResponse: '- agentResponse: The model\'s response. Resolving returns the full response text and sections.',
};

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;
default: {
const _: never = event;
void _;
break;
}
}
}
return lines.join('\n');
}

/**
* Creates a debug events attachment for a chat session.
* This can be used to attach debug logs to a chat request.
*/
export async function createDebugEventsAttachment(
sessionResource: URI,
chatDebugService: IChatDebugService
): Promise<IChatRequestVariableEntry> {
chatDebugService.markDebugDataAttached(sessionResource);
if (!chatDebugService.hasInvokedProviders(sessionResource)) {
await chatDebugService.invokeProviders(sessionResource);
}
const events = chatDebugService.getEvents(sessionResource);
const summary = events.length > 0
? formatDebugEventsForContext(events)
: nls.localize('debugEventsSnapshot.noEvents', "No debug events found for this conversation.");

return {
id: 'chatDebugEvents',
name: nls.localize('debugEventsSnapshot.contextName', "Debug Events Snapshot"),
icon: Codicon.output,
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'
+ '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'
+ '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'
+ Object.values(debugEventKindDescriptions).join('\n'),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ export class ChatDebugEditor extends EditorPane {
}

this.chatDebugService.activeSessionResource = sessionResource;
this.chatDebugService.invokeProviders(sessionResource);
if (!this.chatDebugService.hasInvokedProviders(sessionResource)) {
this.chatDebugService.invokeProviders(sessionResource);
}
this.trackSessionModelChanges(sessionResource);

this.overviewView?.setSession(sessionResource);
Expand Down Expand Up @@ -327,7 +329,9 @@ export class ChatDebugEditor extends EditorPane {
this.savedSessionResource = undefined;
if (sessionResource) {
this.chatDebugService.activeSessionResource = sessionResource;
this.chatDebugService.invokeProviders(sessionResource);
if (!this.chatDebugService.hasInvokedProviders(sessionResource)) {
this.chatDebugService.invokeProviders(sessionResource);
}
} else {
this.showView(ViewState.Home);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button } from '../../../../../base/browser/ui/button/button.js';
import { IObjectTreeElement } from '../../../../../base/browser/ui/tree/tree.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Emitter } from '../../../../../base/common/event.js';
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { combinedDisposable, Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { autorun } from '../../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
Expand All @@ -29,6 +29,7 @@ import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem, LogsViewMode } f
import { ChatDebugFilterState, bindFilterContextKeys } from './chatDebugFilters.js';
import { ChatDebugDetailPanel } from './chatDebugDetailPanel.js';
import { IChatWidgetService } from '../chat.js';
import { createDebugEventsAttachment } from './chatDebugAttachment.js';

const $ = DOM.$;

Expand Down Expand Up @@ -131,9 +132,8 @@ export class ChatDebugLogsView extends Disposable {
}
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 });
const attachment = await createDebugEventsAttachment(this.currentSessionResource, this.chatDebugService);
widget.attachmentModel.addContext(attachment);
widget.focusInput();
}
}));
Expand Down Expand Up @@ -389,12 +389,23 @@ export class ChatDebugLogsView extends Disposable {

private loadEvents(): void {
this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)];
this.eventListener.value = this.chatDebugService.onDidAddEvent(e => {

const addEventDisposable = this.chatDebugService.onDidAddEvent(e => {
if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) {
this.events.push(e);
this.refreshList();
}
});

// Reload events when provider events are cleared (before re-invoking providers)
const clearEventsDisposable = this.chatDebugService.onDidClearProviderEvents(sessionResource => {
if (!this.currentSessionResource || sessionResource.toString() === this.currentSessionResource.toString()) {
this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)];
this.refreshList();
}
});

this.eventListener.value = combinedDisposable(addEventDisposable, clearEventsDisposable);
this.updateBreadcrumb();
this.trackSessionState();
}
Expand Down
Loading
Loading