perf: convert synchronous I/O to async operations in worktree handlers (#337)

This commit optimizes the merge handler to prevent UI blocking by converting synchronous file operations to asynchronous I/O:

- Make handleProcessExit async to support async operations
- Replace readFileSync with fsPromises.readFile for commit message reading
- Refactor plan persistence to use async I/O with parallel updates via Promise.all()
- Implement fire-and-forget pattern for plan updates to prevent blocking the response
- Improve error handling with separate tracking for main vs worktree plans

These changes eliminate blocking I/O operations that could freeze the UI during merge operations, particularly when updating implementation plans across both the main project and worktree.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
JoshuaRileyDev
2025-12-27 13:13:53 +00:00
committed by GitHub
parent 50e3111ae2
commit d3a63b09fd
@@ -443,7 +443,7 @@ export function registerWorktreeHandlers(
});
// Handler for when process exits
const handleProcessExit = (code: number | null, signal: string | null = null) => {
const handleProcessExit = async (code: number | null, signal: string | null = null) => {
if (resolved) return; // Prevent double-resolution
resolved = true;
if (timeoutId) clearTimeout(timeoutId);
@@ -539,13 +539,14 @@ export function registerWorktreeHandlers(
debug('Merge result. isStageOnly:', isStageOnly, 'newStatus:', newStatus, 'staged:', staged);
// Read suggested commit message if staging succeeded
// OPTIMIZATION: Use async I/O to prevent blocking
let suggestedCommitMessage: string | undefined;
if (staged) {
const commitMsgPath = path.join(specDir, 'suggested_commit_message.txt');
try {
if (existsSync(commitMsgPath)) {
const { readFileSync } = require('fs');
suggestedCommitMessage = readFileSync(commitMsgPath, 'utf-8').trim();
const { promises: fsPromises } = require('fs');
suggestedCommitMessage = (await fsPromises.readFile(commitMsgPath, 'utf-8')).trim();
debug('Read suggested commit message:', suggestedCommitMessage?.substring(0, 100));
}
} catch (e) {
@@ -556,17 +557,21 @@ export function registerWorktreeHandlers(
// Persist the status change to implementation_plan.json
// Issue #243: We must update BOTH the main project's plan AND the worktree's plan (if it exists)
// because ProjectStore prefers the worktree version when deduplicating tasks.
// OPTIMIZATION: Use async I/O and parallel updates to prevent UI blocking
const planPaths = [
path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN), // Main project
path.join(worktreePath, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN) // Worktree
{ path: path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN), isMain: true },
{ path: path.join(worktreePath, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN), isMain: false }
];
const { readFileSync, writeFileSync } = require('fs');
const { promises: fsPromises } = require('fs');
for (const planPath of planPaths) {
try {
if (existsSync(planPath)) {
const planContent = readFileSync(planPath, 'utf-8');
// Fire and forget - don't block the response on file writes
const updatePlans = async () => {
const updates = planPaths.map(async ({ path: planPath, isMain }) => {
try {
if (!existsSync(planPath)) return;
const planContent = await fsPromises.readFile(planPath, 'utf-8');
const plan = JSON.parse(planContent);
plan.status = newStatus;
plan.planStatus = planStatus;
@@ -575,17 +580,22 @@ export function registerWorktreeHandlers(
plan.stagedAt = new Date().toISOString();
plan.stagedInMainProject = true;
}
writeFileSync(planPath, JSON.stringify(plan, null, 2));
await fsPromises.writeFile(planPath, JSON.stringify(plan, null, 2));
} catch (persistError) {
// Only log error if main plan fails; worktree plan might legitimately be missing or read-only
if (isMain) {
console.error('Failed to persist task status to main plan:', persistError);
} else {
debug('Failed to persist task status to worktree plan (non-critical):', persistError);
}
}
} catch (persistError) {
// Only log error if main plan fails; worktree plan might legitimately be missing or read-only
if (planPath.includes(specDir)) {
console.error('Failed to persist task status to main plan:', persistError);
} else {
debug('Failed to persist task status to worktree plan (non-critical):', persistError);
}
}
}
});
await Promise.all(updates);
};
// Run async updates without blocking the response
updatePlans().catch(err => debug('Background plan update failed:', err));
const mainWindow = getMainWindow();
if (mainWindow) {