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 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 });
// 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}`);
// 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
// Restart with --continue to resume conversation (silent = no session_started message)
setTimeout(() => {
startClaudeSession(restartProject, true, restartHost, true); // silent=true
savedPermissionMode = restartPermissionMode;
}, 500);
isRestarting = false; // Clear flag after restart
}, 200);
});
// Send SIGINT (graceful interrupt)
claudeProcess.kill('SIGINT');
} else {
sendToClient('generation_stopped', {
message: 'No active process',