eigent/electron/main/reviewChanges.ts
Douglas Lai 6d16eb51fa
Some checks are pending
Pre-commit / pre-commit (push) Waiting to run
Test / Run Web + Local Brain Smoke (push) Waiting to run
Test / Run Frontend Guardrails (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
Feat/in session dif review (#1817)
Co-authored-by: Tong Chen <web_chentong@163.com>
2026-08-21 13:43:21 +08:00

105 lines
3.5 KiB
TypeScript

// ========= 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 { ipcMain } from 'electron';
import log from 'electron-log';
import fs from 'node:fs';
import path from 'node:path';
/**
* The agent file toolkit backs a file up as `<name>.<YYYYMMDD_HHMMSS>.bak`
* next to it before overwriting. Those backups are the "before" side of the
* review tab's diffs.
*/
const BACKUP_TIMESTAMP_PATTERN = /^\d{8}_\d{6}$/;
/** One backup file, with the size the renderer needs to skip huge diffs. */
export interface ReviewBackup {
path: string;
size: number;
}
export interface ReviewBackupEntry {
/** The absolute path that was asked about. */
path: string;
/** Whether the file currently exists on disk. */
exists: boolean;
/** Size of the current on-disk file; null when it does not exist. */
size: number | null;
/**
* This file's backups, sorted oldest-first (the first entry is the content
* from before the earliest recorded write).
*/
backups: ReviewBackup[];
}
async function backupsFor(
filePath: string,
directoryEntries: Map<string, Promise<string[]>>
): Promise<ReviewBackup[]> {
const dir = path.dirname(filePath);
const base = path.basename(filePath);
let namesPromise = directoryEntries.get(dir);
if (!namesPromise) {
namesPromise = fs.promises.readdir(dir).catch(() => []);
directoryEntries.set(dir, namesPromise);
}
const names = await namesPromise;
const prefix = `${base}.`;
const backups = names.filter((name) => {
if (!name.startsWith(prefix) || !name.endsWith('.bak')) return false;
const stamp = name.slice(prefix.length, -'.bak'.length);
return BACKUP_TIMESTAMP_PATTERN.test(stamp);
});
backups.sort();
return Promise.all(
backups.map(async (name) => {
const backupPath = path.join(dir, name);
const size = await fs.promises
.stat(backupPath)
.then((stats) => stats.size)
.catch(() => 0);
return { path: backupPath, size };
})
);
}
export function registerReviewChangesIpcHandlers(): void {
ipcMain.handle(
'review-list-backups',
async (_event, filePaths: string[]): Promise<ReviewBackupEntry[]> => {
try {
const directoryEntries = new Map<string, Promise<string[]>>();
return await Promise.all(
(filePaths ?? []).map(async (filePath) => {
const stats = await fs.promises
.stat(filePath)
.then((result) => (result.isFile() ? result : null))
.catch(() => null);
const backups = await backupsFor(filePath, directoryEntries);
return {
path: filePath,
exists: Boolean(stats),
size: stats ? stats.size : null,
backups,
};
})
);
} catch (error) {
log.error('review-list-backups failed', error);
return [];
}
}
);
}