From f133ad0576379f017dcb652c57359beff7463f2b Mon Sep 17 00:00:00 2001 From: Nikolas Syring Date: Thu, 18 Dec 2025 22:41:02 +0100 Subject: [PATCH] feat: Add graceful interrupt with silent restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ESC now sends SIGINT to interrupt generation gracefully - Automatic silent restart with --continue after interrupt - Added isRestarting flag to prevent session_ended during restart - No UI messages during interrupt/restart cycle - Removed SIGKILL fallback - SIGINT is sufficient 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/server.js | 59 +++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/backend/server.js b/backend/server.js index edec57b..5a83c84 100644 --- a/backend/server.js +++ b/backend/server.js @@ -597,6 +597,7 @@ wss.on('connection', async (ws, req) => { let currentProject = null; let currentHostId = null; // Track current host for restart let isInitialized = false; + let isRestarting = false; // Flag to prevent session_ended during interrupt restart let currentPermissionMode = 'default'; let savedPermissionMode = 'default'; // Store mode before plan mode switch let inPlanMode = false; // Track if we're in plan mode (to require approval for ExitPlanMode) @@ -646,7 +647,7 @@ wss.on('connection', async (ws, req) => { } }; - const startClaudeSession = (projectPath, resume = true, hostId = null) => { + const startClaudeSession = (projectPath, resume = true, hostId = null, silent = false) => { if (claudeProcess) { console.log(`[${sessionId}] Killing existing Claude process`); claudeProcess.kill(); @@ -710,10 +711,13 @@ wss.on('connection', async (ws, req) => { sessions.set(sessionId, { process: claudeProcess, project: projectPath, host: host, hostId: hostId, user: wsUser }); - sendToClient('session_started', { - sessionId, - project: projectPath - }); + // Only send session_started if not a silent restart (e.g., after interrupt) + if (!silent) { + sendToClient('session_started', { + sessionId, + project: projectPath + }); + } // Send control protocol initialization after process starts const initializeControlProtocol = () => { @@ -937,8 +941,11 @@ wss.on('connection', async (ws, req) => { claudeProcess.on('close', (code) => { console.log(`[${sessionId}] Claude process exited with code ${code}`); - sendToClient('session_ended', { code }); - sessions.delete(sessionId); + // Don't send session_ended if we're restarting after interrupt + if (!isRestarting) { + sendToClient('session_ended', { code }); + sessions.delete(sessionId); + } claudeProcess = null; }); @@ -984,34 +991,40 @@ wss.on('connection', async (ws, req) => { break; case 'stop_generation': - // Kill the process and restart with --continue to resume session + // Interrupt Claude and restart with --continue + // In JSON mode, SIGINT causes Claude to exit (unlike TUI mode where it just stops output) + // So we need to restart the session automatically if (claudeProcess) { - console.log(`[${sessionId}] Stop generation: killing process and restarting`); + console.log(`[${sessionId}] Stop generation: sending SIGINT and will restart`); + + // Set flag to prevent session_ended from being sent + isRestarting = true; // Save current state for restart const restartProject = currentProject; const restartHost = currentHostId; const restartPermissionMode = currentPermissionMode; - // Kill the process - claudeProcess.kill('SIGKILL'); - claudeProcess = null; - isInitialized = false; - - // Notify frontend + // Notify frontend (no message - silent interrupt) sendToClient('generation_stopped', { - message: 'Generation stopped, reconnecting...', timestamp: Date.now() }); - // Restart after a short delay - setTimeout(() => { - console.log(`[${sessionId}] Restarting session with --continue`); - startClaudeSession(restartProject, true, restartHost); + // Listen for exit and restart + claudeProcess.once('exit', (code) => { + console.log(`[${sessionId}] Claude exited with code ${code}, restarting with --continue`); + isInitialized = false; - // Restore permission mode after initialization - savedPermissionMode = restartPermissionMode; - }, 500); + // Restart with --continue to resume conversation (silent = no session_started message) + setTimeout(() => { + startClaudeSession(restartProject, true, restartHost, true); // silent=true + savedPermissionMode = restartPermissionMode; + isRestarting = false; // Clear flag after restart + }, 200); + }); + + // Send SIGINT (graceful interrupt) + claudeProcess.kill('SIGINT'); } else { sendToClient('generation_stopped', { message: 'No active process',