diff --git a/electron/claude.ts b/electron/claude.ts index ea021f2..b2d8189 100644 --- a/electron/claude.ts +++ b/electron/claude.ts @@ -157,6 +157,16 @@ function startBridge(win: BrowserWindow): Promise<{ server: Server; port: number function which(cmd: string): Promise { return new Promise((resolve) => { + if (process.platform === 'win32') { + // `where` lists every PATH match; prefer something Windows can actually + // exec (npm ships an extensionless sh shim next to claude.cmd) + execFile('where.exe', [cmd], (err, stdout) => { + if (err) return resolve(null) + const lines = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) + resolve(lines.find((l) => /\.(exe|cmd|bat)$/i.test(l)) ?? lines[0] ?? null) + }) + return + } execFile('/bin/sh', ['-c', `command -v ${cmd}`], (err, stdout) => { resolve(err ? null : stdout.trim() || null) }) @@ -227,21 +237,45 @@ async function openSession( try { // lazy import: node-pty is native — a load failure must not break the app const pty = await import('node-pty') - // Watchdog wrapper: claude runs exec'd in the pty foreground (same pid - // as the wrapper, TUI unaffected); a background subshell nukes the whole - // process group if this Electron process dies hard — otherwise a busy - // claude tree survives holding inherited Chromium sockets (CDP port) - // and blocks the next launch. - const wrapper = - `(while kill -0 ${process.pid} 2>/dev/null; do sleep 3; done; ` + - `kill -HUP -$$ 2>/dev/null; sleep 2; kill -9 -$$ 2>/dev/null) & exec "$0" "$@"` - const p = pty.spawn('/bin/bash', ['-c', wrapper, bin, ...args], { + // Kadr may itself have been launched from a Claude Code session (agent + // workflows, `claude` in a terminal). The inherited CLAUDECODE / + // CLAUDE_CODE_* markers make the embedded CLI believe it is a nested + // child session: transcript saving turns off and /resume comes up empty. + // Strip them first so claude-env.json can still set them deliberately. + const env: Record = {} + for (const [k, v] of Object.entries(process.env)) { + if (v === undefined || k === 'CLAUDECODE' || k.startsWith('CLAUDE_CODE_')) continue + env[k] = v + } + Object.assign(env, cfg.env) + + const ptyOpts = { name: 'xterm-256color', cols: Math.max(20, cols), rows: Math.max(5, rows), cwd: dir, - env: { ...process.env, ...cfg.env } as Record - }) + env + } + let p: IPty + if (process.platform === 'win32') { + // ConPTY: no bash watchdog — closing the pty tears down the attached + // console tree, so a hard Electron death takes claude with it. npm's + // .cmd shim can't be CreateProcess'd directly; route through cmd.exe. + const viaCmd = /\.(cmd|bat)$/i.test(bin) + p = viaCmd + ? pty.spawn(process.env.ComSpec || 'cmd.exe', ['/c', bin, ...args], ptyOpts) + : pty.spawn(bin, args, ptyOpts) + } else { + // Watchdog wrapper: claude runs exec'd in the pty foreground (same pid + // as the wrapper, TUI unaffected); a background subshell nukes the whole + // process group if this Electron process dies hard — otherwise a busy + // claude tree survives holding inherited Chromium sockets (CDP port) + // and blocks the next launch. + const wrapper = + `(while kill -0 ${process.pid} 2>/dev/null; do sleep 3; done; ` + + `kill -HUP -$$ 2>/dev/null; sleep 2; kill -9 -$$ 2>/dev/null) & exec "$0" "$@"` + p = pty.spawn('/bin/bash', ['-c', wrapper, bin, ...args], ptyOpts) + } p.onData((data) => win.webContents.send('claude:data', data)) p.onExit(({ exitCode }) => { // only announce deaths of the CURRENT session: deliberate closes @@ -264,13 +298,19 @@ function closeSession() { if (!session) return const s = session session = null - // HUP the whole process group (claude + its MCP server children), then - // escalate: a busy tree that shrugs off SIGHUP must not outlive the panel - const pid = s.pty.pid - try { process.kill(-pid, 'SIGHUP') } catch { try { s.pty.kill() } catch { /* dead */ } } - setTimeout(() => { - try { process.kill(-pid, 'SIGKILL') } catch { /* already gone */ } - }, 1500) + if (process.platform === 'win32') { + // ConPTY teardown kills the attached console tree; group signals with a + // negative pid are a POSIX-only concept and throw on Windows + try { s.pty.kill() } catch { /* dead */ } + } else { + // HUP the whole process group (claude + its MCP server children), then + // escalate: a busy tree that shrugs off SIGHUP must not outlive the panel + const pid = s.pty.pid + try { process.kill(-pid, 'SIGHUP') } catch { try { s.pty.kill() } catch { /* dead */ } } + setTimeout(() => { + try { process.kill(-pid, 'SIGKILL') } catch { /* already gone */ } + }, 1500) + } s.server.close() } diff --git a/package-lock.json b/package-lock.json index 1fe0513..cde58c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kadr", - "version": "0.1.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kadr", - "version": "0.1.0", + "version": "0.3.0", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": {