From 8f2968531f70bf0cc03dce703fbd5022be7ad1d3 Mon Sep 17 00:00:00 2001 From: simplaerai-sv Date: Sun, 16 Aug 2026 16:49:14 +0300 Subject: [PATCH 1/3] Windows support for the embedded Claude session (ConPTY) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude panel was POSIX-only and died on Windows with "File not found" before the CLI even started. Three win32 branches, POSIX path untouched: - which(): resolve via where.exe, preferring .exe/.cmd/.bat — npm puts an extensionless sh shim first, which Windows cannot exec - spawn: no bash watchdog wrapper; .cmd/.bat shims route through cmd.exe /c under ConPTY. Closing the pty tears down the attached console tree, so a hard Electron death still takes claude with it - closeSession(): pty.kill() instead of process.kill(-pid, SIGHUP) — negative-pid group signals throw on Windows Tested on Windows 11 (26200), Node 24, claude CLI 2.1.233: panel opens, all six kadr_* MCP tools reachable from the embedded session. Co-Authored-By: Claude Fable 5 --- electron/claude.ts | 62 +++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/electron/claude.ts b/electron/claude.ts index ea021f2..ce040ca 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,33 @@ 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], { + const ptyOpts = { name: 'xterm-256color', cols: Math.max(20, cols), rows: Math.max(5, rows), cwd: dir, env: { ...process.env, ...cfg.env } as Record - }) + } + 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 +286,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() } From e58caea896e1f7941f32e86e6da85036dd5ff85c Mon Sep 17 00:00:00 2001 From: simplaerai-sv Date: Sun, 16 Aug 2026 16:49:14 +0300 Subject: [PATCH 2/3] Sync package-lock version field to 0.3.0 npm install refreshed the stale 0.1.0 version stamp left behind by earlier releases; no dependency changes. Co-Authored-By: Claude Fable 5 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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": { From 934ea30e64bbe8fda2bcc8cd17c443ea3fd1b05c Mon Sep 17 00:00:00 2001 From: simplaerai-sv Date: Sun, 16 Aug 2026 16:58:38 +0300 Subject: [PATCH 3/3] Do not leak the host session CLAUDE_CODE_* markers into the panel Kadr launched from a Claude Code session (agent workflows, `claude` in a terminal) passes its whole environment to the embedded CLI, including CLAUDECODE and CLAUDE_CODE_* markers. The embedded session then treats itself as a nested child: transcript saving is disabled and /resume shows no history, so a closed panel is unrecoverable. Strip those keys before spawning; claude-env.json is applied afterwards and can still set them deliberately. Co-Authored-By: Claude Fable 5 --- electron/claude.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/electron/claude.ts b/electron/claude.ts index ce040ca..b2d8189 100644 --- a/electron/claude.ts +++ b/electron/claude.ts @@ -237,12 +237,24 @@ async function openSession( try { // lazy import: node-pty is native — a load failure must not break the app const pty = await import('node-pty') + // 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') {