diff --git a/docs/troubleshooting/bug.md b/docs/troubleshooting/bug.md index 17cac16e..8a190eac 100644 --- a/docs/troubleshooting/bug.md +++ b/docs/troubleshooting/bug.md @@ -10,23 +10,23 @@ width="100%" height="auto" /> -### Step 1: Access the Bug Report Feature +### Step 1: Open Report a bug -1. Locate the **Bug Report** button in the top section of the desktop interface, positioned to the right of your project name -1. Click the **Bug Report** button to initiate the reporting process +1. In the top bar, click the **Support** (help) icon +1. Choose **Report a bug** -### Step 2: Download Log Files +### Step 2: Describe the issue and save diagnostics -- Upon clicking the Bug Report button, log files will be automatically downloaded to your device -- These log files contain technical information that helps our development team diagnose issues more effectively +1. Enter what went wrong. Optionally add **steps to reproduce** +1. Click **Save diagnostics and open email** +1. Choose where to save the **diagnostics ZIP** (it includes app logs and a `bug_report.txt` file) -### Step 3: Complete the Bug Report Form +Your default email app opens addressed to **info@eigent.ai** with a pre-filled message. -- A bug report web form will automatically open in your default browser -- Please provide the following information: - - **Bug Description**: Write a clear description of the issue you encountered - - **Screenshot Upload**: Attach relevant screenshots that demonstrate the problem - - **Log File Upload**: Upload the log files that were downloaded in Step 2 +### Step 3: Send the email + +1. **Attach the ZIP** you just saved to the message (the mail app cannot add this automatically) +1. Add screenshots or other files if they help, then send ### Step 4: Join Our Community for Real-time Support @@ -47,9 +47,11 @@ Developers and technical users are welcome to report issues directly through our - **Repository URL**: https://github.com/eigent-ai/eigent - Submit detailed issues with reproduction steps +**Optional:** In the same **Report a bug** dialog, use **Download logs** to save a single log file (without the full diagnostics ZIP). + ## Important Notes -- Always include log files when reporting bugs for faster resolution +- Always include the diagnostics ZIP (or log export) when reporting bugs for faster resolution - Provide as much detail as possible in your bug description - Screenshots help our team understand visual issues more quickly - Our community channels provide the fastest response times for urgent issues diff --git a/electron/main/index.ts b/electron/main/index.ts index efddcfea..a1c4dc33 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -59,7 +59,7 @@ import { removeEnvKey, updateEnvBlock, } from './utils/envUtil'; -import { zipFolder } from './utils/log'; +import { createDiagnosticsZip, zipFolder } from './utils/log'; import { addMcp, readMcpConfig, removeMcp, updateMcp } from './utils/mcpConfig'; import { checkVenvExistsForPreCheck, @@ -1111,6 +1111,100 @@ function registerIpcHandlers() { } }); + ipcMain.handle('get-diagnostics-info', async () => { + return { + version: app.getVersion(), + platform: process.platform, + arch: process.arch, + }; + }); + + ipcMain.handle( + 'export-diagnostics-zip', + async ( + _event, + payload: { description: string; steps?: string } | undefined + ) => { + try { + const description = + typeof payload?.description === 'string' + ? payload.description.trim() + : ''; + if (!description) { + return { success: false, error: 'Description is required' }; + } + const steps = + typeof payload?.steps === 'string' ? payload.steps.trim() : ''; + + const logFiles: { src: string; destName: string }[] = []; + if (fs.existsSync(logPath)) { + logFiles.push({ src: logPath, destName: 'electron-main.log' }); + } + const backupResolved = getBackupLogPath(); + if ( + fs.existsSync(backupResolved) && + path.resolve(backupResolved) !== path.resolve(logPath) + ) { + logFiles.push({ + src: backupResolved, + destName: 'electron-userdata-logs.log', + }); + } + if (logFiles.length === 0) { + return { success: false, error: 'no log file' }; + } + + const appVersion = app.getVersion(); + const platform = process.platform; + const arch = process.arch; + const bugReportText = [ + 'Eigent bug report', + '=================', + '', + `App version: ${appVersion}`, + `OS: ${platform} (${arch})`, + '', + 'Description', + '-----------', + description, + '', + ...(steps + ? ['Steps to reproduce', '-------------------', steps, ''] + : []), + ].join('\n'); + + const defaultFileName = `eigent-diagnostics-${appVersion}-${Date.now()}.zip`; + const { canceled, filePath } = await dialog.showSaveDialog({ + title: 'Save diagnostics', + defaultPath: defaultFileName, + filters: [{ name: 'ZIP archive', extensions: ['zip'] }], + }); + + if (canceled || !filePath) { + return { success: false, error: '' }; + } + + await createDiagnosticsZip(filePath, bugReportText, logFiles); + return { success: true, savedPath: filePath }; + } catch (error: any) { + log.error('export-diagnostics-zip failed:', error); + return { success: false, error: error.message }; + } + } + ); + + ipcMain.handle('open-mailto', async (_event, url: string) => { + try { + if (typeof url !== 'string' || !url.startsWith('mailto:')) { + return { success: false, error: 'Invalid mailto URL' }; + } + await shell.openExternal(url); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + ipcMain.handle( 'upload-log', async ( diff --git a/electron/main/utils/log.ts b/electron/main/utils/log.ts index bf700787..495576cb 100644 --- a/electron/main/utils/log.ts +++ b/electron/main/utils/log.ts @@ -12,7 +12,11 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import { randomBytes } from 'node:crypto'; import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; // @ts-ignore import archiver from 'archiver'; import log from 'electron-log'; @@ -37,3 +41,42 @@ export function zipFolder( archive.finalize(); }); } + +export type DiagnosticsLogFile = { src: string; destName: string }; + +/** + * Stages log files and bug_report.txt into a temp directory, zips to outputZipPath, then removes the staging dir. + */ +export async function createDiagnosticsZip( + outputZipPath: string, + bugReportText: string, + logFiles: DiagnosticsLogFile[] +): Promise { + if (logFiles.length === 0) { + throw new Error('no log files to include'); + } + const id = randomBytes(8).toString('hex'); + const staging = path.join(os.tmpdir(), `eigent-diagnostics-${id}`); + await fsp.mkdir(staging, { recursive: true }); + try { + for (const f of logFiles) { + if (!fs.existsSync(f.src)) { + log.warn(`[diagnostics] skip missing log: ${f.src}`); + continue; + } + await fsp.copyFile(f.src, path.join(staging, f.destName)); + } + await fsp.writeFile( + path.join(staging, 'bug_report.txt'), + bugReportText, + 'utf-8' + ); + const entries = await fsp.readdir(staging); + if (entries.length === 0) { + throw new Error('no log files to include'); + } + await zipFolder(staging, outputZipPath); + } finally { + await fsp.rm(staging, { recursive: true, force: true }); + } +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 2071116c..2547c038 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -76,6 +76,10 @@ contextBridge.exposeInMainWorld('electronAPI', { webviewDestroy: (webviewId: string) => ipcRenderer.invoke('webview-destroy', webviewId), exportLog: () => ipcRenderer.invoke('export-log'), + getDiagnosticsInfo: () => ipcRenderer.invoke('get-diagnostics-info'), + exportDiagnosticsZip: (payload: { description: string; steps?: string }) => + ipcRenderer.invoke('export-diagnostics-zip', payload), + openMailto: (url: string) => ipcRenderer.invoke('open-mailto', url), uploadLog: (email: string, taskId: string, baseUrl: string, token: string) => ipcRenderer.invoke('upload-log', email, taskId, baseUrl, token), // mcp diff --git a/scripts/design-token-usage.allowlist b/scripts/design-token-usage.allowlist index 02b34262..fa84573b 100644 --- a/scripts/design-token-usage.allowlist +++ b/scripts/design-token-usage.allowlist @@ -9,3 +9,7 @@ src/components/ui/WordCarousel/WordCarousel.tsx src/components/Terminal/index.tsx # # Install progress bar (src/components/ui/progress-install.tsx) and similar: use CSS vars only — no entry needed. +# +# Electron shell surfaces use fixed native colors in preload HTML and BrowserWindow options. +electron/main/index.ts +electron/preload/index.ts diff --git a/src/components/Dialog/ReportBugDialog.tsx b/src/components/Dialog/ReportBugDialog.tsx new file mode 100644 index 00000000..bef3c841 --- /dev/null +++ b/src/components/Dialog/ReportBugDialog.tsx @@ -0,0 +1,293 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from '@/components/ui/dialog'; +import { Textarea } from '@/components/ui/textarea'; +import { useHost } from '@/host'; +import { Download } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +const INFO_EMAIL = 'info@eigent.ai'; + +function buildMailtoUrl( + subject: string, + body: string +): { url: string; truncated: boolean } { + const maxLen = 1800; + const tail = '\n\n[…]'; + let b = body; + let truncated = false; + if (b.length > maxLen) { + b = b.slice(0, maxLen - tail.length) + tail; + truncated = true; + } + const url = `mailto:${INFO_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(b)}`; + return { url, truncated }; +} + +/** Matches `getDiagnosticsInfo` in preload / `ElectronAPI` */ +type DiagnosticsInfo = { + version: string; + platform: string; + arch: string; +}; + +interface ReportBugDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function ReportBugDialog({ + open, + onOpenChange, +}: ReportBugDialogProps) { + const host = useHost(); + const { t } = useTranslation(); + const [description, setDescription] = useState(''); + const [steps, setSteps] = useState(''); + const [meta, setMeta] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [downloadingLog, setDownloadingLog] = useState(false); + + const handleDownloadLog = useCallback(async () => { + const exportLog = host?.electronAPI?.exportLog; + if (!exportLog) { + toast.error(t('layout.general-error')); + return; + } + setDownloadingLog(true); + try { + const response = await exportLog(); + if (!response?.success) { + if (response?.error) { + toast.error(response.error); + } + return; + } + if (response.savedPath) { + toast.success(`${t('layout.log-saved')} ${response.savedPath}`); + return; + } + if (response.data === 'log file is empty') { + toast.info(t('layout.log-file-empty')); + } + } catch (e: unknown) { + toast.error(e instanceof Error ? e.message : t('layout.general-error')); + } finally { + setDownloadingLog(false); + } + }, [host?.electronAPI, t]); + + useEffect(() => { + if (!open) { + return; + } + const api = host?.electronAPI; + if (!api?.getDiagnosticsInfo) { + return; + } + void api + .getDiagnosticsInfo() + .then((info: DiagnosticsInfo) => { + if (info?.version) { + setMeta({ + version: info.version, + platform: info.platform, + arch: info.arch, + }); + } + }) + .catch(() => { + setMeta(null); + }); + }, [open, host?.electronAPI]); + + useEffect(() => { + if (!open) { + setDescription(''); + setSteps(''); + } + }, [open]); + + const onSubmit = useCallback(async () => { + const api = host?.electronAPI; + if (!api?.exportDiagnosticsZip || !api?.openMailto) { + toast.error(t('layout.general-error')); + return; + } + const trimmed = description.trim(); + if (!trimmed) { + toast.error(t('layout.report-bug-description-required')); + return; + } + setSubmitting(true); + try { + const response = await api.exportDiagnosticsZip({ + description: trimmed, + steps: steps.trim() || undefined, + }); + if (!response?.success) { + if (response?.error === '') { + return; + } + if (response?.error) { + toast.error(response.error); + } else { + toast.error(t('layout.general-error')); + } + return; + } + if (!response.savedPath) { + return; + } + + const subject = t('layout.report-bug-mail-subject'); + const v = meta?.version ?? '—'; + const p = meta?.platform ?? '—'; + const a = meta?.arch ?? '—'; + const body = [ + t('layout.report-bug-mail-body-intro'), + '', + t('layout.report-bug-mail-body-path', { path: response.savedPath }), + '', + '—', + t('layout.report-bug-mail-body-meta', { version: v, os: p, arch: a }), + '', + t('layout.report-bug-mail-body-desc'), + trimmed, + '', + ...(steps.trim() + ? [t('layout.report-bug-mail-body-steps'), steps.trim(), ''] + : []), + ].join('\n'); + + const { url, truncated } = buildMailtoUrl(subject, body); + if (truncated) { + toast.info(t('layout.report-bug-mail-body-truncated')); + } + + const mail = await api.openMailto(url); + if (!mail?.success) { + if (mail?.error) { + toast.error(mail.error); + } + return; + } + onOpenChange(false); + toast.success(t('layout.report-bug-diagnostics-saved')); + } catch (e: unknown) { + toast.error(e instanceof Error ? e.message : t('layout.general-error')); + } finally { + setSubmitting(false); + } + }, [host?.electronAPI, description, steps, meta, onOpenChange, t]); + + return ( + + +
+ + {t('layout.report-bug-dialog-title')} + +
+
+ {meta && ( +

+ {t('layout.report-bug-meta', { + version: meta.version, + os: meta.platform, + arch: meta.arch, + })} +

+ )} +

+ {t('layout.report-bug-footer-hint')} +

+ +