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