init helder
This commit is contained in:
79
src/main/pty-service.ts
Normal file
79
src/main/pty-service.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import type { WebContents } from 'electron'
|
||||
import { getRoot } from './project'
|
||||
import { getConfig } from './config'
|
||||
|
||||
/**
|
||||
* Real PTYs. The agent pane is a shell that auto-launches the `claude` CLI; the
|
||||
* bottom pane is a plain shell. node-pty is a native module — loaded defensively
|
||||
* so the app still launches (with a friendly message) if it wasn't rebuilt for
|
||||
* this Electron via `npm run rebuild`.
|
||||
*
|
||||
* Shell + ai command/autoLaunch come from `.helder/config.json` (terminal.shell,
|
||||
* ai.command, ai.autoLaunch) via the config module.
|
||||
*/
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
type PtyModule = typeof import('node-pty')
|
||||
let pty: PtyModule | null = null
|
||||
try {
|
||||
pty = require('node-pty') as PtyModule
|
||||
} catch (e) {
|
||||
console.error('[helder] node-pty unavailable — run `npm run rebuild`:', (e as Error).message)
|
||||
}
|
||||
|
||||
const terms = new Map<number, import('node-pty').IPty>()
|
||||
let seq = 0
|
||||
|
||||
function defaultShell(): string {
|
||||
const configured = getConfig().terminal.shell
|
||||
if (configured) return configured
|
||||
if (process.platform === 'win32') return process.env.COMSPEC || 'powershell.exe'
|
||||
return process.env.SHELL || '/bin/zsh'
|
||||
}
|
||||
|
||||
export function ptyAvailable(): boolean {
|
||||
return !!pty
|
||||
}
|
||||
|
||||
export function createPty(sender: WebContents, kind: 'agent' | 'shell', cols: number, rows: number): number {
|
||||
if (!pty) return -1
|
||||
const cwd = getRoot() || process.env.HOME || process.cwd()
|
||||
const proc = pty.spawn(defaultShell(), [], {
|
||||
name: 'xterm-color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
cwd,
|
||||
env: process.env as { [key: string]: string },
|
||||
})
|
||||
const id = ++seq
|
||||
terms.set(id, proc)
|
||||
|
||||
proc.onData((data) => { if (!sender.isDestroyed()) sender.send('pty:data', { id, data }) })
|
||||
proc.onExit(() => { terms.delete(id); if (!sender.isDestroyed()) sender.send('pty:exit', { id }) })
|
||||
|
||||
const ai = getConfig().ai
|
||||
if (kind === 'agent' && ai.autoLaunch) {
|
||||
// small delay so the shell prompt is ready before we type the command
|
||||
setTimeout(() => { try { proc.write(ai.command + '\r') } catch { /* exited */ } }, 350)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
export function writePty(id: number, data: string): void {
|
||||
terms.get(id)?.write(data)
|
||||
}
|
||||
|
||||
export function resizePty(id: number, cols: number, rows: number): void {
|
||||
try { terms.get(id)?.resize(cols, rows) } catch { /* race with exit */ }
|
||||
}
|
||||
|
||||
export function killPty(id: number): void {
|
||||
const p = terms.get(id)
|
||||
if (p) { try { p.kill() } catch { /* already gone */ } terms.delete(id) }
|
||||
}
|
||||
|
||||
export function killAllPtys(): void {
|
||||
for (const p of terms.values()) { try { p.kill() } catch { /* noop */ } }
|
||||
terms.clear()
|
||||
}
|
||||
Reference in New Issue
Block a user