feat: Add graceful interrupt with silent restart

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 22:41:02 +01:00
parent 905e295f5d
commit f133ad0576

View File

@@ -597,6 +597,7 @@ wss.on('connection', async (ws, req) => {
let currentProject = null; let currentProject = null;
let currentHostId = null; // Track current host for restart let currentHostId = null; // Track current host for restart
let isInitialized = false; let isInitialized = false;
let isRestarting = false; // Flag to prevent session_ended during interrupt restart
let currentPermissionMode = 'default'; let currentPermissionMode = 'default';
let savedPermissionMode = 'default'; // Store mode before plan mode switch let savedPermissionMode = 'default'; // Store mode before plan mode switch
let inPlanMode = false; // Track if we're in plan mode (to require approval for ExitPlanMode) 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) { if (claudeProcess) {
console.log(`[${sessionId}] Killing existing Claude process`); console.log(`[${sessionId}] Killing existing Claude process`);
claudeProcess.kill(); 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 }); sessions.set(sessionId, { process: claudeProcess, project: projectPath, host: host, hostId: hostId, user: wsUser });
sendToClient('session_started', { // Only send session_started if not a silent restart (e.g., after interrupt)
sessionId, if (!silent) {
project: projectPath sendToClient('session_started', {
}); sessionId,
project: projectPath
});
}
// Send control protocol initialization after process starts // Send control protocol initialization after process starts
const initializeControlProtocol = () => { const initializeControlProtocol = () => {
@@ -937,8 +941,11 @@ wss.on('connection', async (ws, req) => {
claudeProcess.on('close', (code) => { claudeProcess.on('close', (code) => {
console.log(`[${sessionId}] Claude process exited with code ${code}`); console.log(`[${sessionId}] Claude process exited with code ${code}`);
sendToClient('session_ended', { code }); // Don't send session_ended if we're restarting after interrupt
sessions.delete(sessionId); if (!isRestarting) {
sendToClient('session_ended', { code });
sessions.delete(sessionId);
}
claudeProcess = null; claudeProcess = null;
}); });
@@ -984,34 +991,40 @@ wss.on('connection', async (ws, req) => {
break; break;
case 'stop_generation': 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) { 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 // Save current state for restart
const restartProject = currentProject; const restartProject = currentProject;
const restartHost = currentHostId; const restartHost = currentHostId;
const restartPermissionMode = currentPermissionMode; const restartPermissionMode = currentPermissionMode;
// Kill the process // Notify frontend (no message - silent interrupt)
claudeProcess.kill('SIGKILL');
claudeProcess = null;
isInitialized = false;
// Notify frontend
sendToClient('generation_stopped', { sendToClient('generation_stopped', {
message: 'Generation stopped, reconnecting...',
timestamp: Date.now() timestamp: Date.now()
}); });
// Restart after a short delay // Listen for exit and restart
setTimeout(() => { claudeProcess.once('exit', (code) => {
console.log(`[${sessionId}] Restarting session with --continue`); console.log(`[${sessionId}] Claude exited with code ${code}, restarting with --continue`);
startClaudeSession(restartProject, true, restartHost); isInitialized = false;
// Restore permission mode after initialization // Restart with --continue to resume conversation (silent = no session_started message)
savedPermissionMode = restartPermissionMode; setTimeout(() => {
}, 500); startClaudeSession(restartProject, true, restartHost, true); // silent=true
savedPermissionMode = restartPermissionMode;
isRestarting = false; // Clear flag after restart
}, 200);
});
// Send SIGINT (graceful interrupt)
claudeProcess.kill('SIGINT');
} else { } else {
sendToClient('generation_stopped', { sendToClient('generation_stopped', {
message: 'No active process', message: 'No active process',