From 8e14cfab38e7c30f304e703c7b4970542d53c4bd Mon Sep 17 00:00:00 2001 From: michelhelsdingen Date: Tue, 3 Feb 2026 13:49:58 +0100 Subject: [PATCH 1/2] fix(mode-switch): signal forwarding prevents orphaned processes (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## THE FIX Forward SIGTERM, SIGINT, SIGHUP to child Claude CLI process. This ensures child processes are killed when parent is terminated. ## ROOT CAUSE When switching between local/remote modes, the parent process was killed but child Claude CLI processes remained alive ("orphaned"). These orphaned processes continued to hold stdin, causing: - Duplicate/garbled characters when typing - "Competing processes" fighting for terminal input ## SOLUTION (from GitHub slopus/happy#430) ```javascript const forwardSignal = (signal) => { if (child.pid && !child.killed) { child.kill(signal); } }; process.on('SIGTERM', () => forwardSignal('SIGTERM')); process.on('SIGINT', () => forwardSignal('SIGINT')); process.on('SIGHUP', () => forwardSignal('SIGHUP')); ``` ## FILES CHANGED - scripts/claude_version_utils.cjs - binary launcher signal forwarding - src/claude/claudeLocal.ts - TypeScript launcher signal forwarding ## TESTED ✅ Fresh local mode - typing works ✅ Switch to remote mode - typing works ✅ Switch back to local mode - typing works (was broken) ✅ Multiple mode switches - stable ## IMPORTANT This is the ONLY fix needed. No stdin cleanup, no removeAllListeners(), no setRawMode changes required. Signal forwarding alone solves it. Closes #11 References: slopus/happy#430, PR #127 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- package.json | 2 +- scripts/claude_version_utils.cjs | 13 +++++++++++++ src/claude/claudeLocal.ts | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 87040fab..db50f070 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "happy-coder", - "version": "0.14.0-0", + "version": "0.14.1-0", "description": "Mobile and Web client for Claude Code and Codex", "author": "Kirill Dubovitskiy", "license": "MIT", diff --git a/scripts/claude_version_utils.cjs b/scripts/claude_version_utils.cjs index 2184917a..daef5246 100644 --- a/scripts/claude_version_utils.cjs +++ b/scripts/claude_version_utils.cjs @@ -497,6 +497,19 @@ function runClaudeCli(cliPath) { stdio: 'inherit', env: process.env }); + + // Forward signals to child process so it gets killed when parent is killed + // This prevents orphaned Claude processes when switching between local/remote modes + // Fix for issue #11 / GitHub slopus/happy#430 + const forwardSignal = (signal) => { + if (child.pid && !child.killed) { + child.kill(signal); + } + }; + process.on('SIGTERM', () => forwardSignal('SIGTERM')); + process.on('SIGINT', () => forwardSignal('SIGINT')); + process.on('SIGHUP', () => forwardSignal('SIGHUP')); + child.on('exit', (code) => { process.exit(code || 0); }); diff --git a/src/claude/claudeLocal.ts b/src/claude/claudeLocal.ts index d4f7ac0b..92922404 100644 --- a/src/claude/claudeLocal.ts +++ b/src/claude/claudeLocal.ts @@ -232,6 +232,29 @@ export async function claudeLocal(opts: { env, }); + // Forward signals to child process to prevent orphaned processes + // Fix for issue #11 / GitHub slopus/happy#430 + // Note: signal: opts.abort handles programmatic abort (mode switching), + // but direct OS signals (e.g., kill, Ctrl+C) need explicit forwarding + const forwardSignal = (signal: NodeJS.Signals) => { + if (child.pid && !child.killed) { + child.kill(signal); + } + }; + const onSigterm = () => forwardSignal('SIGTERM'); + const onSigint = () => forwardSignal('SIGINT'); + const onSighup = () => forwardSignal('SIGHUP'); + process.on('SIGTERM', onSigterm); + process.on('SIGINT', onSigint); + process.on('SIGHUP', onSighup); + + // Cleanup signal handlers when child exits to avoid leaks + child.on('exit', () => { + process.off('SIGTERM', onSigterm); + process.off('SIGINT', onSigint); + process.off('SIGHUP', onSighup); + }); + // Listen to the custom fd (fd 3) for thinking state tracking if (child.stdio[3]) { const rl = createInterface({ From fd1d37907ee07b824090a5385caad0e2c4231da4 Mon Sep 17 00:00:00 2001 From: michelhelsdingen Date: Fri, 6 Feb 2026 13:32:59 +0100 Subject: [PATCH 2/2] fix: re-enable terminal title control via change_title MCP tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-apply terminal title feature (originally df7262c) that was lost when upstream main diverged. Sets iTerm2/Terminal.app window title via OSC escape sequences when change_title is invoked. - Add CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 to launcher to prevent Claude Code from overwriting the terminal title - Add setTerminalTitle() using OSC 0 escape sequence for direct window title control - change_title now sets both terminal title and Happy app title Tested: title updates work in iTerm2, remote→local mode switch still functions correctly (no stdin regression). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- scripts/claude_local_launcher.cjs | 3 +++ src/claude/utils/startHappyServer.ts | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/claude_local_launcher.cjs b/scripts/claude_local_launcher.cjs index 6afab097..cec15407 100644 --- a/scripts/claude_local_launcher.cjs +++ b/scripts/claude_local_launcher.cjs @@ -3,6 +3,9 @@ const fs = require('fs'); // Disable autoupdater (never works really) process.env.DISABLE_AUTOUPDATER = '1'; +// Disable Claude Code's terminal title setting so Happy CLI can control it +process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE = '1'; + // Helper to write JSON messages to fd 3 function writeMessage(message) { try { diff --git a/src/claude/utils/startHappyServer.ts b/src/claude/utils/startHappyServer.ts index 9a1bb21b..72559602 100644 --- a/src/claude/utils/startHappyServer.ts +++ b/src/claude/utils/startHappyServer.ts @@ -12,18 +12,30 @@ import { logger } from "@/ui/logger"; import { ApiSessionClient } from "@/api/apiSession"; import { randomUUID } from "node:crypto"; +/** + * Set the terminal window title using OSC escape sequences. + * Works with iTerm2, Terminal.app, and most modern terminal emulators. + * Uses OSC 0 which sets both window title and icon name. + */ +function setTerminalTitle(title: string): void { + process.stdout.write(`\x1b]0;${title}\x07`); +} + export async function startHappyServer(client: ApiSessionClient) { // Handler that sends title updates via the client const handler = async (title: string) => { logger.debug('[happyMCP] Changing title to:', title); try { - // Send title as a summary message, similar to title generator + // Set the terminal window title (iTerm2, Terminal.app, etc.) + setTerminalTitle(title); + + // Also send title as a summary message to Happy server (for mobile app) client.sendClaudeSessionMessage({ type: 'summary', summary: title, leafUuid: randomUUID() }); - + return { success: true }; } catch (error) { return { success: false, error: String(error) };