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
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { joinPath } from '../../../../base/common/resources.js';
import { localize, localize2 } from '../../../../nls.js';
import { Action2 } from '../../../../platform/actions/common/actions.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js';
import { IAgentHostService } from '../../../../platform/agentHost/common/agentService.js';
import { IRemoteAgentHostService } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { INativeHostService } from '../../../../platform/native/common/native.js';
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { IOutputService } from '../../../../workbench/services/output/common/output.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
import { IsAgentHostSession } from '../browser/agentHostSkillButtons.js';
import { IPathService } from '../../../../workbench/services/path/common/pathService.js';
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
import { resolveEventsUri, parseRemoteAuthorityFromScheme } from '../browser/openSessionEventsFileActions.js';

/** Output channel ID for the agent host process logger (forwarded via RemoteLoggerChannelClient). */
const AGENT_HOST_LOGGER_CHANNEL_ID = 'agenthost';
/** Output channel ID for the current window's renderer log. */
const WINDOW_LOG_CHANNEL_ID = 'rendererLog';
/** Output channel ID for the shared process compound log. */
const SHARED_PROCESS_LOG_CHANNEL_ID = 'shared';

const LOCAL_AH_SCHEME = 'agent-host-copilotcli';
const EH_CLI_SCHEME = 'copilotcli';

export class CollectAgentHostDebugLogsAction extends Action2 {

static readonly ID = 'agentHost.collectDebugLogs';

constructor() {
super({
id: CollectAgentHostDebugLogsAction.ID,
title: localize2('collectAgentHostDebugLogs', "Collect Agent Host Debug Logs"),
f1: true,
category: Categories.Developer,
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, IsAgentHostSession),
});
}

override async run(accessor: ServicesAccessor): Promise<void> {
const sessionsManagementService = accessor.get(ISessionsManagementService);
const pathService = accessor.get(IPathService);
const remoteAgentHostService = accessor.get(IRemoteAgentHostService);
const agentHostService = accessor.get(IAgentHostService);
const outputService = accessor.get(IOutputService);
const fileService = accessor.get(IFileService);
const fileDialogService = accessor.get(IFileDialogService);
const nativeHostService = accessor.get(INativeHostService);
const notificationService = accessor.get(INotificationService);
const textModelService = accessor.get(ITextModelService);

const sessionResource = sessionsManagementService.activeSession.get()?.resource;
const userHome = pathService.userHome({ preferLocal: true });

const eventsResult = resolveEventsUri(
sessionResource,
userHome,
authority => remoteAgentHostService.connections.find(c => agentHostAuthority(c.address) === authority),
);

switch (eventsResult.kind) {
case 'no-session':
notificationService.info(localize('collectDebugLogs.noSession', "No Copilot CLI session is active."));
return;
case 'unsupported-scheme':
notificationService.info(localize('collectDebugLogs.unsupported', "The active chat session is not a Copilot CLI session."));
return;
case 'remote-not-connected':
notificationService.warn(localize('collectDebugLogs.notConnected', "No active connection found for remote agent host '{0}'.", eventsResult.authority));
return;
case 'remote-no-home':
notificationService.warn(localize('collectDebugLogs.noHome', "Remote agent host '{0}' did not report a home directory.", eventsResult.authority));
return;
}

const isLocal = sessionResource?.scheme === LOCAL_AH_SCHEME || sessionResource?.scheme === EH_CLI_SCHEME;

// Collect all output channel IDs relevant for the current session's agent host.
const channelIds: string[] = [];

if (isLocal) {
// IPC traffic log for the local agent host connection
channelIds.push(`agenthost.${agentHostService.clientId}`);
// Agent host process logger (forwarded from the utility process)
channelIds.push(AGENT_HOST_LOGGER_CHANNEL_ID);
} else {
const remoteAuthority = parseRemoteAuthorityFromScheme(sessionResource!.scheme);
if (remoteAuthority) {
const connection = remoteAgentHostService.connections.find(c => agentHostAuthority(c.address) === remoteAuthority);
if (connection) {
channelIds.push(`agenthost.${connection.clientId}`);
}
}
}

// Always include the window and shared process logs
channelIds.push(WINDOW_LOG_CHANNEL_ID);
channelIds.push(SHARED_PROCESS_LOG_CHANNEL_ID);

const files: { path: string; contents: string }[] = [];

// 1. events.jsonl
try {
const content = await fileService.readFile(eventsResult.resource);
files.push({ path: 'events.jsonl', contents: content.value.toString() });
} catch {
// File may not exist yet if the session never wrote any events
}

// 2. Output channels
for (const channelId of channelIds) {
const channel = outputService.getChannel(channelId);
const descriptor = outputService.getChannelDescriptor(channelId);
if (!channel || !descriptor) {
continue;
}
const modelRef = await textModelService.createModelReference(channel.uri);
try {
const filename = `${descriptor.label.replace(/[/\\:*?"<>|]/g, '-')}.log`;
files.push({ path: filename, contents: modelRef.object.textEditorModel.getValue() });
} finally {
modelRef.dispose();
}
}

if (files.length === 0) {
notificationService.notify({
severity: Severity.Warning,
message: localize('collectDebugLogs.noFiles', "No log files were found for the active session."),
});
return;
}

const sessionTitle = sessionsManagementService.activeSession.get()?.title.get();
const titleSlug = sessionTitle
? `-${sessionTitle.replace(/[/\\:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40)}`
: '';
const defaultUri = joinPath(await fileDialogService.defaultFilePath(), `ah-logs${titleSlug}.zip`);
const saveUri = await fileDialogService.showSaveDialog({
title: localize('collectDebugLogs.saveDialogTitle', "Save Agent Host Debug Logs"),
defaultUri,
filters: [{ name: localize('collectDebugLogs.zipFilter', "Zip Archive"), extensions: ['zip'] }],
});

if (!saveUri) {
return;
}

try {
await nativeHostService.createZipFile(saveUri, files);
} catch (error) {
notificationService.notify({
severity: Severity.Error,
message: localize('collectDebugLogs.saveError', "Failed to save debug logs: {0}", error instanceof Error ? error.message : String(error)),
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import { IViewsService } from '../../../../workbench/services/views/common/views
import { ILifecycleService, LifecyclePhase } from '../../../../workbench/services/lifecycle/common/lifecycle.js';
import { NewChatViewPane, SessionsViewId } from '../browser/newChatViewPane.js';
import { DebugAgentHostInDevToolsAction } from '../../../../workbench/contrib/chat/electron-browser/actions/debugAgentHostAction.js';
import { CollectAgentHostDebugLogsAction } from '../../agentHost/electron-browser/collectDebugLogsAction.js';

registerAction2(DebugAgentHostInDevToolsAction);
registerAction2(CollectAgentHostDebugLogsAction);

class SelectAgentsFolderContribution extends Disposable implements IWorkbenchContribution {

Expand Down
Loading