From d3a63b09fdd7b1d200dec66afec049884b6a9e54 Mon Sep 17 00:00:00 2001 From: JoshuaRileyDev <59296334+JoshuaRileyDev@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:13:53 +0000 Subject: [PATCH] perf: convert synchronous I/O to async operations in worktree handlers (#337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ipc-handlers/task/worktree-handlers.ts | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts index f6f1d289..354c861c 100644 --- a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts @@ -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) {