@@ -15,6 +15,7 @@ const execAsync = promisify(exec);
1515let mainWindow : BrowserWindow ;
1616const activePtyProcesses = new Map < string , pty . IPty > ( ) ;
1717const mcpPollerPtyProcesses = new Map < string , pty . IPty > ( ) ;
18+ let claudeCommandRunnerPty : pty . IPty | null = null ;
1819const 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
649696async 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
656703async function removeMcpServer ( name : string ) {
657704 const command = `claude mcp remove "${ name } "` ;
658- await execInLoginShell ( command ) ;
705+ await execClaudeCommand ( command ) ;
659706}
660707
661708async 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 = () => {
777824app . 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