Skip to content

Commit 93f5134

Browse files
built-by-asclaude
andcommitted
Use dedicated command runner PTY for claude MCP operations
- Spawn a single persistent PTY at app startup for running claude commands - Replaces execAsync with login shell to avoid PATH issues with NVM - PTY has full interactive shell environment with proper PATH - Commands execute instantly (no shell spawn overhead) - Fixes "command not found: claude" in compiled DMG - Detects command completion using isTerminalReady helper 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 68cdbb4 commit 93f5134

File tree

1 file changed

+63
-13
lines changed

1 file changed

+63
-13
lines changed

main.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const execAsync = promisify(exec);
1515
let mainWindow: BrowserWindow;
1616
const activePtyProcesses = new Map<string, pty.IPty>();
1717
const mcpPollerPtyProcesses = new Map<string, pty.IPty>();
18+
let claudeCommandRunnerPty: pty.IPty | null = null;
1819
const store = new Store();
1920

2021
// Helper functions for session management
@@ -632,35 +633,81 @@ async function listMcpServers() {
632633
}
633634
}
634635

635-
// Get user's shell, with fallback
636-
function getUserShell(): string {
637-
return process.env.SHELL || (os.platform() === "darwin" ? "/bin/zsh" : "/bin/bash");
636+
// Spawn a dedicated PTY for running claude commands
637+
function spawnClaudeCommandRunner() {
638+
if (claudeCommandRunnerPty) {
639+
return;
640+
}
641+
642+
const shell = os.platform() === "darwin" ? "zsh" : "bash";
643+
claudeCommandRunnerPty = pty.spawn(shell, ["-l"], {
644+
name: "xterm-color",
645+
cols: 80,
646+
rows: 30,
647+
cwd: os.homedir(),
648+
env: process.env,
649+
});
638650
}
639651

640-
// Execute command in user's login shell
641-
async function execInLoginShell(command: string): Promise<string> {
642-
const userShell = getUserShell();
643-
// Wrap command to execute in login shell
644-
const wrappedCommand = `${userShell} -l -c '${command.replace(/'/g, "'\\''")}'`;
645-
const { stdout } = await execAsync(wrappedCommand);
646-
return stdout;
652+
// Execute claude command in dedicated PTY
653+
async function execClaudeCommand(command: string): Promise<string> {
654+
return new Promise((resolve, reject) => {
655+
if (!claudeCommandRunnerPty) {
656+
reject(new Error("Claude command runner PTY not initialized"));
657+
return;
658+
}
659+
660+
const pty = claudeCommandRunnerPty;
661+
let outputBuffer = "";
662+
let timeoutId: NodeJS.Timeout;
663+
let disposed = false;
664+
665+
const dataHandler = pty.onData((data: string) => {
666+
if (disposed) return;
667+
668+
outputBuffer += data;
669+
670+
// Check if command completed (prompt returned)
671+
if (isTerminalReady(data)) {
672+
disposed = true;
673+
clearTimeout(timeoutId);
674+
dataHandler.dispose();
675+
676+
// Extract just the command output (remove the command echo and prompt lines)
677+
const lines = outputBuffer.split('\n');
678+
const output = lines.slice(1, -1).join('\n').trim();
679+
resolve(output);
680+
}
681+
});
682+
683+
// Set timeout
684+
timeoutId = setTimeout(() => {
685+
if (!disposed) {
686+
disposed = true;
687+
dataHandler.dispose();
688+
reject(new Error("Command timeout"));
689+
}
690+
}, 10000);
691+
692+
pty.write(command + "\r");
693+
});
647694
}
648695

649696
async function addMcpServer(name: string, config: any) {
650697
// Use add-json to support full configuration including env vars, headers, etc.
651698
const jsonConfig = JSON.stringify(config).replace(/'/g, "'\\''"); // Escape single quotes for shell
652699
const command = `claude mcp add-json --scope user "${name}" '${jsonConfig}'`;
653-
await execInLoginShell(command);
700+
await execClaudeCommand(command);
654701
}
655702

656703
async function removeMcpServer(name: string) {
657704
const command = `claude mcp remove "${name}"`;
658-
await execInLoginShell(command);
705+
await execClaudeCommand(command);
659706
}
660707

661708
async function getMcpServerDetails(name: string) {
662709
try {
663-
const output = await execInLoginShell(`claude mcp get "${name}"`);
710+
const output = await execClaudeCommand(`claude mcp get "${name}"`);
664711

665712
// Parse the output to extract details
666713
const details: any = { name };
@@ -777,6 +824,9 @@ const createWindow = () => {
777824
app.whenReady().then(() => {
778825
createWindow();
779826

827+
// Spawn claude command runner PTY early so it's ready when needed (fire-and-forget)
828+
spawnClaudeCommandRunner();
829+
780830
// Handles launch from dock on macos
781831
app.on("activate", () => {
782832
if (BrowserWindow.getAllWindows().length === 0) {

0 commit comments

Comments
 (0)