Skip to content
Open
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
50 changes: 50 additions & 0 deletions electron/main/fileReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import path from 'path';
import * as unzipper from 'unzipper';
import { URL } from 'url';
import { parseStringPromise } from 'xml2js';
import { findDirectoriesByName } from './utils/log';

interface FileInfo {
path: string;
Expand Down Expand Up @@ -819,6 +820,55 @@ export class FileReader {
}
}

/**
* Resolves the `camel_logs` directories to export as `{ src, destName }`
* entries (destName is the path inside `~/.eigent`, so the zip stays readable).
*
* Prefers the specific task the user last ran (via {@link resolveTaskPaths},
* the same resolution `get-file-list` uses). Falls back to every `camel_logs`
* folder under the user's identity roots when no task is supplied or its
* folder is missing.
*/
public getCamelLogEntries(
email: string,
taskId?: string,
projectId?: string,
userId?: string | number | null
): { src: string; destName: string }[] {
const userHome = app.getPath('home');
const eigentRoot = path.join(userHome, '.eigent');
const toEntry = (dir: string) => ({
src: dir,
destName: path.relative(eigentRoot, dir) || path.basename(dir),
});

if (taskId) {
const { logPath } = this.resolveTaskPaths(
email,
taskId,
projectId,
userId
);
const camelLogPath = path.join(logPath, 'camel_logs');
if (fs.existsSync(camelLogPath)) {
return [toEntry(camelLogPath)];
}
}

const identities = this.getStorageIdentityCandidates(email, userId);
const seen = new Set<string>();
const entries: { src: string; destName: string }[] = [];
for (const identity of identities) {
const identityRoot = path.join(eigentRoot, identity);
for (const dir of findDirectoriesByName(identityRoot, 'camel_logs', 4)) {
if (seen.has(dir)) continue;
seen.add(dir);
entries.push(toEntry(dir));
}
}
return entries;
}

public deleteTaskFiles(
email: string,
taskId: string,
Expand Down
51 changes: 50 additions & 1 deletion electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
removeEnvKey,
updateEnvBlock,
} from './utils/envUtil';
import { createDiagnosticsZip, zipFolder } from './utils/log';
import { createDiagnosticsZip, zipDirectories, zipFolder } from './utils/log';
import { addMcp, readMcpConfig, removeMcp, updateMcp } from './utils/mcpConfig';
import {
checkVenvExistsForPreCheck,
Expand Down Expand Up @@ -1132,6 +1132,55 @@ function registerIpcHandlers() {
}
});

// Camel (backend) logs live per task at
// ~/.eigent/<identity>/[project_<id>/]task_<taskId>/camel_logs.
// Targets the task the user last ran when provided; otherwise exports all.
ipcMain.handle(
'export-camel-log',
async (
_event,
email: string,
taskId?: string,
projectId?: string,
userId?: string | number | null
) => {
try {
if (typeof email !== 'string' || !email) {
return { success: false, error: 'Missing email' };
}

const manager = checkManagerInstance(fileReader, 'FileReader');
const camelLogEntries = manager.getCamelLogEntries(
email,
taskId,
projectId,
userId
);
if (camelLogEntries.length === 0) {
return { success: false, error: 'no log file' };
}

const appVersion = app.getVersion();
const defaultFileName = `eigent-camel-logs-${appVersion}-${Date.now()}.zip`;
const { canceled, filePath } = await dialog.showSaveDialog({
title: 'Save Camel logs',
defaultPath: defaultFileName,
filters: [{ name: 'ZIP archive', extensions: ['zip'] }],
});

if (canceled || !filePath) {
return { success: false, error: '' };
}

await zipDirectories(filePath, camelLogEntries);
return { success: true, savedPath: filePath };
} catch (error: any) {
log.error('export-camel-log failed:', error);
return { success: false, error: error.message };
}
}
);

ipcMain.handle('get-diagnostics-info', async () => {
return {
version: app.getVersion(),
Expand Down
24 changes: 24 additions & 0 deletions electron/main/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,30 @@ export function registerUpdateIpcHandlers() {
ipcMain.handle('quit-and-install', () => {
autoUpdater.quitAndInstall(false, true);
});

// Dev-only: replay updater events from DevTools to exercise the top-bar
// update UI, e.g.
// window.ipcRenderer.invoke('debug-update-event', 'download-progress', { percent: 40 })
if (!app.isPackaged) {
const allowedChannels = [
'update-can-available',
'download-progress',
'update-downloaded',
'update-error',
];
ipcMain.handle(
'debug-update-event',
(
event: Electron.IpcMainInvokeEvent,
channel: string,
payload?: unknown
) => {
if (!allowedChannels.includes(channel)) return false;
event.sender.send(channel, payload);
return true;
}
);
}
}

function startDownload(
Expand Down
62 changes: 62 additions & 0 deletions electron/main/utils/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,68 @@ export function zipFolder(

export type DiagnosticsLogFile = { src: string; destName: string };

export type LogDirectoryEntry = { src: string; destName: string };

/**
* Zips multiple directories into one archive, each under its destName.
*/
export function zipDirectories(
outputZipPath: string,
dirs: LogDirectoryEntry[]
): Promise<string> {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(outputZipPath);
const archive = archiver('zip', { zlib: { level: 9 } });

output.on('close', () => resolve(outputZipPath));

archive.on('error', (err: any) => {
log.error('Archive error:', err);
reject(err);
});

archive.pipe(output);
for (const dir of dirs) {
archive.directory(dir.src, dir.destName);
}
archive.finalize();
});
}

/**
* Finds directories named `dirName` under `rootDir`. Camel logs live at
* `~/.eigent/<email>/[project_<id>/]task_<taskId>/camel_logs`, so a shallow
* bounded walk is enough.
*/
export function findDirectoriesByName(
rootDir: string,
dirName: string,
maxDepth = 3
): string[] {
const found: string[] = [];
const walk = (dir: string, depth: number) => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
const fullPath = path.join(dir, entry.name);
if (entry.name === dirName) {
found.push(fullPath);
} else if (depth < maxDepth) {
walk(fullPath, depth + 1);
}
}
};
if (fs.existsSync(rootDir)) {
walk(rootDir, 0);
}
return found;
}

/**
* Stages log files and bug_report.txt into a temp directory, zips to outputZipPath, then removes the staging dir.
*/
Expand Down
6 changes: 6 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
webviewDestroy: (webviewId: string) =>
ipcRenderer.invoke('webview-destroy', webviewId),
exportLog: () => ipcRenderer.invoke('export-log'),
exportCamelLog: (
email: string,
taskId?: string,
projectId?: string,
userId?: string | number | null
) => ipcRenderer.invoke('export-camel-log', email, taskId, projectId, userId),
getDiagnosticsInfo: () => ipcRenderer.invoke('get-diagnostics-info'),
exportDiagnosticsZip: (payload: { description: string; steps?: string }) =>
ipcRenderer.invoke('export-diagnostics-zip', payload),
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function App() {
return (
<StackProvider app={stackClientApp}>
<StackTheme>{content}</StackTheme>
<Toaster style={{ zIndex: '999999 !important', position: 'fixed' }} />
<Toaster style={{ zIndex: 999999, position: 'fixed' }} />
</StackProvider>
);
}
Expand Down
Loading
Loading