| |
| |
| |
| |
| |
|
|
| 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 { |
| |
| } |
| 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() ?? ''; |
| } |
|
|