File size: 7,312 Bytes
52efc7b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import open from 'open';
import process from 'node:process';
import {
type CommandContext,
type SlashCommand,
CommandKind,
} from './types.js';
import { MessageType } from '../types.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatBytes } from '../utils/formatters.js';
import {
IdeClient,
getVersion,
INITIAL_HISTORY_LENGTH,
debugLogger,
} from '@google/gemini-cli-core';
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
import { exportHistoryToFile } from '../utils/historyExportUtils.js';
import {
captureHeapSnapshot,
MEMORY_SNAPSHOT_AUTO_THRESHOLD_BYTES,
} from '../utils/memorySnapshot.js';
import { stat } from 'node:fs/promises';
import path from 'node:path';
export const bugCommand: SlashCommand = {
name: 'bug',
description: 'Submit a bug report',
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim();
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const osVersion = `${process.platform} ${process.version}`;
let sandboxEnv = 'no sandbox';
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
sandboxEnv = process.env['SANDBOX'].replace(/^gemini-(?:code-)?/, '');
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
sandboxEnv = `sandbox-exec (${
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion = config?.getModel() || 'Unknown';
const cliVersion = await getVersion();
const memoryUsage = formatBytes(process.memoryUsage().rss);
const ideClient = await getIdeClientName(context);
const terminalName =
terminalCapabilityManager.getTerminalName() || 'Unknown';
const terminalBgColor =
terminalCapabilityManager.getTerminalBackgroundColor() || 'Unknown';
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
? 'Supported'
: 'Unsupported';
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
let info = `
* **CLI Version:** ${cliVersion}
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** ${config?.getSessionId() || 'Unknown'}
* **Operating System:** ${osVersion}
* **Sandbox Environment:** ${sandboxEnv}
* **Model Version:** ${modelVersion}
* **Auth Type:** ${authType}
* **Memory Usage:** ${memoryUsage}
* **Terminal Name:** ${terminalName}
* **Terminal Background:** ${terminalBgColor}
* **Kitty Keyboard Protocol:** ${kittyProtocol}
`;
if (ideClient) {
info += `* **IDE Client:** ${ideClient}\n`;
}
const chat = agentContext?.geminiClient?.getChat();
const history = chat?.getHistory() || [];
let historyFileMessage = '';
let problemValue = bugDescription;
if (history.length > INITIAL_HISTORY_LENGTH) {
const tempDir = config?.storage?.getProjectTempDir();
if (tempDir) {
const historyFileName = `bug-report-history-${Date.now()}.json`;
const historyFilePath = path.join(tempDir, historyFileName);
try {
await exportHistoryToFile({ history, filePath: historyFilePath });
historyFileMessage = `\n\n--------------------------------------------------------------------------------\n\n📄 **Chat History Exported**\nTo help us debug, we've exported your current chat history to:\n${historyFilePath}\n\nPlease consider attaching this file to your GitHub issue if you feel comfortable doing so.\n\n**Privacy Disclaimer:** Please do not upload any logs containing sensitive or private information that you are not comfortable sharing publicly.`;
problemValue += `\n\n[ACTION REQUIRED] 📎 PLEASE ATTACH THE EXPORTED CHAT HISTORY JSON FILE TO THIS ISSUE IF YOU FEEL COMFORTABLE SHARING IT.`;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
debugLogger.error(
`Failed to export chat history for bug report: ${errorMessage}`,
);
}
}
}
let bugReportUrl =
'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}&problem={problem}';
const bugCommandSettings = config?.getBugCommand();
if (bugCommandSettings?.urlTemplate) {
bugReportUrl = bugCommandSettings.urlTemplate;
}
bugReportUrl = bugReportUrl
.replace('{title}', encodeURIComponent(bugDescription))
.replace('{info}', encodeURIComponent(info))
.replace('{problem}', encodeURIComponent(problemValue));
context.ui.addItem(
{
type: MessageType.INFO,
text: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}${historyFileMessage}`,
},
Date.now(),
);
try {
await open(bugReportUrl);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Could not open URL in browser: ${errorMessage}`,
},
Date.now(),
);
}
const rss = process.memoryUsage().rss;
const tempDir = config?.storage?.getProjectTempDir();
if (rss >= MEMORY_SNAPSHOT_AUTO_THRESHOLD_BYTES && tempDir) {
const snapshotPath = path.join(
tempDir,
`bug-memory-${Date.now()}.heapsnapshot`,
);
context.ui.addItem(
{
type: MessageType.INFO,
text: `High memory usage detected (${formatBytes(rss)}). Capturing V8 heap snapshot to ${snapshotPath}.\nThis can take 20+ seconds and the CLI may be temporarily unresponsive; please do not exit.`,
},
Date.now(),
);
try {
const startedAt = Date.now();
await captureHeapSnapshot(snapshotPath);
const durationMs = Date.now() - startedAt;
let sizeText = '';
try {
const { size } = await stat(snapshotPath);
sizeText = ` (${formatBytes(size)})`;
} catch {
// Size reporting is best-effort; the snapshot itself was captured successfully.
}
context.ui.addItem(
{
type: MessageType.INFO,
text: `Heap snapshot saved${sizeText} in ${durationMs}ms:\n${snapshotPath}\n\nConsider attaching it to your bug report only if it does not contain sensitive information.`,
},
Date.now(),
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
debugLogger.error(
`Failed to capture heap snapshot for bug report: ${errorMessage}`,
);
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to capture heap snapshot: ${errorMessage}`,
},
Date.now(),
);
}
}
},
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.agentContext?.config.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
return ideClient.getDetectedIdeDisplayName() ?? '';
}
|