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_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/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({ 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) };