Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 58 additions & 18 deletions electron/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ function startBridge(win: BrowserWindow): Promise<{ server: Server; port: number

function which(cmd: string): Promise<string | null> {
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)
})
Expand Down Expand Up @@ -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<string, string> = {}
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<string, string>
})
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
Expand All @@ -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()
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.