From 7043427658054c3de9977441f92a6e8ae5669132 Mon Sep 17 00:00:00 2001 From: uroma Date: Mon, 19 Jan 2026 18:49:48 +0000 Subject: [PATCH] fix: wait for PTY ready state before sending commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement proper ready-state handshake to fix command execution timing. Root Cause: WebSocket connection was opening immediately, but the backend PTY (pseudo-terminal) wasn't ready to receive input yet. Commands sent too early were lost, causing claude --dangerously-skip-permissions to never execute. Broken Flow: 1. WebSocket opens → connectTerminal() resolves immediately 2. Command sent → PTY not ready, command lost 3. Terminal shows cursor but Claude CLI never starts Fixed Flow: 1. WebSocket opens → Wait for 'ready' message from backend 2. Backend sends 'ready' → PTY is now initialized 3. Command sent → PTY receives it successfully 4. Claude CLI starts Changes: - Add 'ready' flag to terminal state (default false) - connectTerminal() now waits for 'ready' message before resolving - Add waitForTerminalReady() helper with 5s timeout - launchCommand() checks ready state before sending - Enhanced error handling and console logging Resolves: "terminal does not show or execute claude cli" Co-Authored-By: Claude Sonnet 4.5 --- public/claude-ide/terminal.js | 67 +++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/public/claude-ide/terminal.js b/public/claude-ide/terminal.js index 123470e0..77fd1c88 100644 --- a/public/claude-ide/terminal.js +++ b/public/claude-ide/terminal.js @@ -4,7 +4,7 @@ class TerminalManager { constructor() { - this.terminals = new Map(); // terminalId -> { terminal, ws, fitAddon, container, mode } + this.terminals = new Map(); // terminalId -> { terminal, ws, fitAddon, container, mode, ready } this.activeTerminalId = null; this.xtermLoaded = false; this.terminalsContainer = null; @@ -218,6 +218,7 @@ class TerminalManager { await this.initializeXTerm(terminalId); // NOW connect WebSocket (terminal entry exists in map) + // This waits for the 'ready' message from backend await this.connectTerminal(terminalId); // Switch to new terminal @@ -227,7 +228,7 @@ class TerminalManager { if (selectedTerminalType === 'claude-cli') { // Launch Claude CLI with skip permissions flag // Note: Keep mode as 'mixed' since we're not attaching to a session - // WebSocket is already connected (await connectTerminal), so no delay needed + // connectTerminal now waits for 'ready' message, so PTY is definitely ready await this.launchCommand(terminalId, 'claude --dangerously-skip-permissions\n'); if (!silent) { @@ -627,11 +628,21 @@ class TerminalManager { /** * Launch a command in the terminal + * Waits for terminal to be ready before sending command */ async launchCommand(terminalId, command) { + // Wait for terminal to be ready (max 5 seconds) + const ready = await this.waitForTerminalReady(terminalId, 5000); + + if (!ready) { + console.error('[TerminalManager] Terminal not ready for command launch (timeout)'); + showToast('Terminal not ready. Please try again.', 'error'); + return; + } + const terminal = this.terminals.get(terminalId); if (!terminal || !terminal.ws || terminal.ws.readyState !== WebSocket.OPEN) { - console.error('[TerminalManager] Terminal not ready for command launch'); + console.error('[TerminalManager] Terminal not ready for command launch (WebSocket not connected)'); return; } @@ -644,6 +655,36 @@ class TerminalManager { console.log(`[TerminalManager] Launched command in terminal ${terminalId}: ${command.trim()}`); } + /** + * Wait for terminal to be ready + */ + async waitForTerminalReady(terminalId, timeout = 5000) { + const startTime = Date.now(); + + return new Promise((resolve) => { + const checkReady = () => { + const terminal = this.terminals.get(terminalId); + + if (terminal && terminal.ready) { + console.log(`[TerminalManager] Terminal ${terminalId} is ready`); + resolve(true); + return; + } + + if (Date.now() - startTime > timeout) { + console.error(`[TerminalManager] Terminal ${terminalId} ready timeout`); + resolve(false); + return; + } + + // Check again in 100ms + setTimeout(checkReady, 100); + }; + + checkReady(); + }); + } + /** * Extract project name from session metadata */ @@ -766,6 +807,7 @@ class TerminalManager { /** * Connect terminal WebSocket + * Waits for 'ready' message from backend before resolving */ async connectTerminal(terminalId) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; @@ -776,7 +818,7 @@ class TerminalManager { const ws = new WebSocket(wsUrl); ws.onopen = () => { - console.log(`[TerminalManager] Connected to terminal ${terminalId}`); + console.log(`[TerminalManager] WebSocket connected for terminal ${terminalId}`); // Store WebSocket in terminal entry // NOTE: This assumes initializeXTerm() has already been called @@ -784,18 +826,28 @@ class TerminalManager { const terminal = this.terminals.get(terminalId); if (terminal) { terminal.ws = ws; + terminal.ready = false; // Will be set to true when 'ready' message received } else { console.error(`[TerminalManager] CRITICAL: Terminal ${terminalId} not found in map! WebSocket connection will be lost.`); reject(new Error(`Terminal ${terminalId} not initialized`)); + ws.close(); return; } - - resolve(); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleTerminalMessage(terminalId, message); + + // If this is the ready message, resolve the promise + if (message.type === 'ready') { + console.log(`[TerminalManager] Terminal ${terminalId} is ready (PTY initialized)`); + const terminal = this.terminals.get(terminalId); + if (terminal) { + terminal.ready = true; + } + resolve(); + } }; ws.onerror = (error) => { @@ -926,7 +978,8 @@ class TerminalManager { fitAddon, ws: null, // Will be set by connectTerminal() container, - mode: 'mixed' + mode: 'mixed', + ready: false // Will be set to true when 'ready' message received }); return terminal;