fix: wait for PTY ready state before sending commands

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 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-19 18:49:48 +00:00
Unverified
parent 8c448315c1
commit 7043427658

View File

@@ -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;