Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { VSBuffer } from '../../../../../base/common/buffer.js';6import { joinPath } from '../../../../../base/common/resources.js';7import { URI } from '../../../../../base/common/uri.js';8import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';9import { localize, localize2 } from '../../../../../nls.js';10import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';11import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';12import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';13import { IDialogService, IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';14import { IFileService } from '../../../../../platform/files/common/files.js';15import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js';16import { IOpenerService } from '../../../../../platform/opener/common/opener.js';17import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';18import { ActiveEditorContext } from '../../../../common/contextkeys.js';19import { IEditorService } from '../../../../services/editor/common/editorService.js';20import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js';21import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';22import { IChatDebugService } from '../../common/chatDebugService.js';23import { ChatViewId, IChatWidgetService } from '../chat.js';24import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js';25import { ChatDebugEditorInput } from '../chatDebug/chatDebugEditorInput.js';26import { Codicon } from '../../../../../base/common/codicons.js';27import { IChatDebugEditorOptions } from '../chatDebug/chatDebugTypes.js';28import { LocalChatSessionUri } from '../../common/model/chatUri.js';2930/**31* Registers the Open Agent Debug Logs and Show Agent Debug Logs actions.32*/33export function registerChatOpenAgentDebugPanelAction() {34registerAction2(class OpenAgentDebugPanelAction extends Action2 {35constructor() {36super({37id: 'workbench.action.chat.openAgentDebugPanel',38title: localize2('chat.openAgentDebugPanel.label', "Open Agent Debug Logs"),39f1: true,40category: Categories.Developer,41precondition: ChatContextKeys.enabled,42});43}4445async run(accessor: ServicesAccessor): Promise<void> {46const editorService = accessor.get(IEditorService);47const chatDebugService = accessor.get(IChatDebugService);4849// Clear active session so the editor shows the home view50chatDebugService.activeSessionResource = undefined;5152const options: IChatDebugEditorOptions = { pinned: true, viewHint: 'home' };53await editorService.openEditor(ChatDebugEditorInput.instance, options);54}55});5657registerAction2(class OpenAgentDebugPanelForSessionAction extends Action2 {58constructor() {59super({60id: 'workbench.action.chat.openAgentDebugPanelForSession',61title: localize2('chat.openAgentDebugPanelForSession.label', "Show Agent Debug Logs"),62f1: false,63category: CHAT_CATEGORY,64precondition: ChatContextKeys.enabled,65menu: [{66id: CHAT_CONFIG_MENU_ID,67when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),68order: 0,69group: '4_logs'70}, {71id: MenuId.ChatWelcomeContext,72group: '2_settings',73order: 0,74when: ChatContextKeys.inChatEditor.negate()75}, {76id: MenuId.ViewTitle,77when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),78order: 0,79group: '4_logs'80}]81});82}8384async run(accessor: ServicesAccessor, context?: URI | unknown, filter?: string): Promise<void> {85const editorService = accessor.get(IEditorService);86const chatWidgetService = accessor.get(IChatWidgetService);87const chatDebugService = accessor.get(IChatDebugService);8889// Extract session resource from context — may be a URI directly90// or an IChatViewTitleActionContext from the chat config menu91let sessionResource: URI | undefined;92if (URI.isUri(context)) {93sessionResource = context;94} else if (isChatViewTitleActionContext(context)) {95sessionResource = context.sessionResource;96}9798// Fall back to the last focused widget99if (!sessionResource) {100const widget = chatWidgetService.lastFocusedWidget;101sessionResource = widget?.viewModel?.sessionResource;102}103chatDebugService.activeSessionResource = sessionResource;104105const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs', filter };106await editorService.openEditor(ChatDebugEditorInput.instance, options);107}108});109110const defaultDebugLogFileName = 'agent-debug-log.json';111const debugLogFilters = [{ name: localize('chatDebugLog.file.label', "Agent Debug Log"), extensions: ['json'] }];112113registerAction2(class ExportAgentDebugLogAction extends Action2 {114constructor() {115super({116id: 'workbench.action.chat.exportAgentDebugLog',117title: localize2('chat.exportAgentDebugLog.label', "Export Agent Debug Log..."),118icon: Codicon.chatExport,119f1: true,120category: Categories.Developer,121precondition: ChatContextKeys.enabled,122menu: [{123id: MenuId.EditorTitle,124group: 'navigation',125when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID),126order: 10127}],128});129}130131async run(accessor: ServicesAccessor): Promise<void> {132const chatDebugService = accessor.get(IChatDebugService);133const fileDialogService = accessor.get(IFileDialogService);134const fileService = accessor.get(IFileService);135const notificationService = accessor.get(INotificationService);136const openerService = accessor.get(IOpenerService);137const telemetryService = accessor.get(ITelemetryService);138139const sessionResource = chatDebugService.activeSessionResource;140if (!sessionResource) {141notificationService.notify({ severity: Severity.Info, message: localize('chatDebugLog.noSession', "No active debug session to export. Navigate to a session first.") });142return;143}144145const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);146const rawIdentifier = localSessionId ?? (sessionResource.path.replace(/^\//, '') || sessionResource.authority);147const sessionIdentifier = rawIdentifier?.replace(/[/\\:*?"<>|.]+/g, '_').replace(/^_+|_+$/g, '');148const exportFileName = sessionIdentifier ? `agent-debug-log-${sessionIdentifier}.json` : defaultDebugLogFileName;149const defaultUri = joinPath(await fileDialogService.defaultFilePath(), exportFileName);150const outputPath = await fileDialogService.showSaveDialog({ defaultUri, filters: debugLogFilters });151if (!outputPath) {152return;153}154155const data = await chatDebugService.exportLog(sessionResource);156if (!data) {157notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.exportFailed', "Export is not supported by the current provider.") });158return;159}160161await fileService.writeFile(outputPath, VSBuffer.wrap(data));162163telemetryService.publicLog2<ChatDebugExportEvent, ChatDebugExportClassification>('chatDebugLogExported', {164fileSizeBytes: data.byteLength,165});166167notificationService.prompt(168Severity.Info,169localize('chatDebugLog.exportSuccess', "Agent debug log exported successfully."),170[{171label: localize('chatDebugLog.openExportedFile', "Open File"),172run: () => openerService.open(outputPath)173}]174);175}176});177178registerAction2(class ImportAgentDebugLogAction extends Action2 {179constructor() {180super({181id: 'workbench.action.chat.importAgentDebugLog',182title: localize2('chat.importAgentDebugLog.label', "Import Agent Debug Log..."),183icon: Codicon.chatImport,184f1: true,185category: Categories.Developer,186precondition: ChatContextKeys.enabled,187menu: [{188id: MenuId.EditorTitle,189group: 'navigation',190when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID),191order: 11192}],193});194}195196async run(accessor: ServicesAccessor): Promise<void> {197const chatDebugService = accessor.get(IChatDebugService);198const dialogService = accessor.get(IDialogService);199const editorService = accessor.get(IEditorService);200const fileDialogService = accessor.get(IFileDialogService);201const fileService = accessor.get(IFileService);202const notificationService = accessor.get(INotificationService);203const telemetryService = accessor.get(ITelemetryService);204205const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName);206const result = await fileDialogService.showOpenDialog({207defaultUri,208canSelectFiles: true,209filters: debugLogFilters210});211if (!result) {212return;213}214215const maxImportSize = 50 * 1024 * 1024; // 50 MB216const stat = await fileService.stat(result[0]);217if (stat.size !== undefined && stat.size > maxImportSize) {218telemetryService.publicLog2<ChatDebugImportEvent, ChatDebugImportClassification>('chatDebugLogImported', {219fileSizeBytes: stat.size,220result: 'fileTooLarge',221});222await dialogService.warn(223localize('chatDebugLog.fileTooLargeTitle', "File Too Large"),224localize('chatDebugLog.fileTooLargeDetail', "The selected file ({0} MB) exceeds the 50 MB size limit for debug log imports.", (stat.size / (1024 * 1024)).toFixed(1))225);226return;227}228229const content = await fileService.readFile(result[0]);230const sessionUri = await chatDebugService.importLog(content.value.buffer);231if (!sessionUri) {232telemetryService.publicLog2<ChatDebugImportEvent, ChatDebugImportClassification>('chatDebugLogImported', {233fileSizeBytes: content.value.byteLength,234result: 'providerFailed',235});236notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.importFailed', "Import is not supported by the current provider.") });237return;238}239240telemetryService.publicLog2<ChatDebugImportEvent, ChatDebugImportClassification>('chatDebugLogImported', {241fileSizeBytes: content.value.byteLength,242result: 'success',243});244245const options: IChatDebugEditorOptions = { pinned: true, sessionResource: sessionUri, viewHint: 'overview' };246await editorService.openEditor(ChatDebugEditorInput.instance, options);247}248});249}250251// Telemetry types252253type ChatDebugExportEvent = {254fileSizeBytes: number;255};256257type ChatDebugExportClassification = {258fileSizeBytes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Size of the exported chat debug log file in bytes.' };259owner: 'vijayu';260comment: 'Tracks usage of the Agent Debug Logs export feature.';261};262263type ChatDebugImportEvent = {264fileSizeBytes: number;265result: 'success' | 'fileTooLarge' | 'providerFailed';266};267268type ChatDebugImportClassification = {269fileSizeBytes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Size of the imported chat debug log file in bytes.' };270result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Outcome of the chat debug file import: success, fileTooLarge, or providerFailed.' };271owner: 'vijayu';272comment: 'Tracks usage of the Agent Debug Logs import feature and failure modes.';273};274275276