fix: terminal command execution via HTTP POST workaround

The WebSocket send mechanism fails with close code 1006 when client
tries to send data to server. Server never receives the message,
indicating a network/proxy layer issue that couldn't be fixed through
code changes or nginx configuration.

Solution: Bypass WebSocket send entirely by using HTTP POST to send
commands directly to the PTY.

Changes:
- Added sendTerminalInput() method to terminal-service.js that writes
  directly to PTY, bypassing WebSocket
- Added POST endpoint /claude/api/terminals/:id/input to server.js
- Modified launchCommand() in terminal.js to use fetch() with HTTP
  POST instead of WebSocket.send()

The WebSocket receive direction still works (server→client for output
display), only send direction (client→server for commands) is bypassed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-19 19:52:36 +00:00
Unverified
parent 990ea80edd
commit 815f7095fd
5 changed files with 130 additions and 38 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -683,7 +683,7 @@ class TerminalManager {
/** /**
* Launch a command in the terminal * Launch a command in the terminal
* Waits for terminal to be ready before sending command * Uses HTTP POST as workaround for WebSocket send issue
*/ */
async launchCommand(terminalId, command) { async launchCommand(terminalId, command) {
this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`); this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`);
@@ -700,44 +700,30 @@ class TerminalManager {
this.debugLog('CMD', `Terminal ${terminalId} is ready!`); this.debugLog('CMD', `Terminal ${terminalId} is ready!`);
// NO DELAY - send command immediately to avoid WebSocket closure // Use HTTP POST instead of WebSocket send (bypasses proxy issue)
this.debugLog('CMD', `Sending command immediately without delay...`); this.debugLog('CMD', `Sending command via HTTP POST: ${command.trim()}`);
const terminal = this.terminals.get(terminalId);
if (!terminal) {
this.debugLog('ERROR', `Terminal ${terminalId} not found in map`);
return;
}
if (!terminal.ws) {
this.debugLog('ERROR', `WebSocket not set for terminal ${terminalId}`);
return;
}
// Check WebSocket state
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
this.debugLog('CMD', `WebSocket state: ${readyStates[terminal.ws.readyState]} (${terminal.ws.readyState})`);
if (terminal.ws.readyState !== WebSocket.OPEN) {
this.debugLog('ERROR', `Cannot send - WebSocket not open`, {
wsState: terminal.ws.readyState,
stateName: readyStates[terminal.ws.readyState]
});
return;
}
// Send command to terminal immediately
const message = JSON.stringify({
type: 'input',
data: command
});
this.debugLog('CMD', `Sending to WebSocket: ${message}`);
try { try {
terminal.ws.send(message); const res = await fetch(`/claude/api/terminals/${terminalId}/input`, {
this.debugLog('CMD', `Command sent to terminal ${terminalId}: ${command.trim()}`); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: command })
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const result = await res.json();
if (result.success) {
this.debugLog('CMD', `Command sent to terminal ${terminalId} via HTTP: ${command.trim()}`);
} else {
this.debugLog('ERROR', `Failed to send command: ${result.error}`);
showToast(`Failed to send command: ${result.error}`, 'error');
}
} catch (error) { } catch (error) {
this.debugLog('ERROR', `Failed to send command`, { error: error.message, name: error.name }); this.debugLog('ERROR', `HTTP POST failed:`, { error: error.message });
showToast(`Failed to send command: ${error.message}`, 'error'); showToast(`Failed to send command: ${error.message}`, 'error');
} }
} }

View File

@@ -1506,6 +1506,29 @@ app.delete('/claude/api/terminals/:id', requireAuth, (req, res) => {
} }
}); });
// Send input to terminal via HTTP (WebSocket workaround)
app.post('/claude/api/terminals/:id/input', requireAuth, (req, res) => {
try {
const { data } = req.body;
if (!data) {
res.status(400).json({ error: 'Missing data parameter' });
return;
}
const result = terminalService.sendTerminalInput(req.params.id, data);
if (result.success) {
res.json({ success: true });
} else {
res.status(404).json({ error: result.error });
}
} catch (error) {
console.error('Error sending terminal input:', error);
res.status(500).json({ error: 'Failed to send input' });
}
});
// Get recent directories for terminal picker // Get recent directories for terminal picker
app.get('/claude/api/files/recent-dirs', requireAuth, (req, res) => { app.get('/claude/api/files/recent-dirs', requireAuth, (req, res) => {
try { try {

View File

@@ -12,6 +12,7 @@ class TerminalService {
this.terminals = new Map(); // terminalId -> { pty, ws, sessionId, workingDir, mode, createdAt } this.terminals = new Map(); // terminalId -> { pty, ws, sessionId, workingDir, mode, createdAt }
this.wsServer = null; this.wsServer = null;
this.logFile = path.join(process.env.HOME, 'obsidian-vault', '.claude-ide', 'terminal-logs.jsonl'); this.logFile = path.join(process.env.HOME, 'obsidian-vault', '.claude-ide', 'terminal-logs.jsonl');
this.pingInterval = null;
} }
/** /**
@@ -29,6 +30,7 @@ class TerminalService {
if (terminalMatch) { if (terminalMatch) {
const terminalId = terminalMatch[1]; const terminalId = terminalMatch[1];
console.log(`[TerminalService] Handling WebSocket upgrade for terminal ${terminalId}`);
this.wsServer.handleUpgrade(request, socket, head, (ws) => { this.wsServer.handleUpgrade(request, socket, head, (ws) => {
this.wsServer.emit('connection', ws, request, terminalId); this.wsServer.emit('connection', ws, request, terminalId);
}); });
@@ -37,12 +39,34 @@ class TerminalService {
// Handle WebSocket connections // Handle WebSocket connections
this.wsServer.on('connection', (ws, request, terminalId) => { this.wsServer.on('connection', (ws, request, terminalId) => {
console.log(`[TerminalService] WebSocket connection event received for terminal ${terminalId}`);
this.handleConnection(terminalId, ws); this.handleConnection(terminalId, ws);
}); });
// Setup ping interval to keep connections alive
this.setupPingInterval();
console.log('[TerminalService] WebSocket server initialized'); console.log('[TerminalService] WebSocket server initialized');
} }
/**
* Setup ping interval to keep WebSocket connections alive
*/
setupPingInterval() {
// Send ping to all clients every 30 seconds
this.pingInterval = setInterval(() => {
if (this.wsServer) {
this.wsServer.clients.forEach((ws) => {
if (ws.readyState === ws.OPEN) {
ws.ping();
}
});
}
}, 30000);
console.log('[TerminalService] Ping interval configured (30s)');
}
/** /**
* Create a new terminal PTY * Create a new terminal PTY
*/ */
@@ -145,6 +169,18 @@ class TerminalService {
} }
}); });
// Handle WebSocket ping (respond with pong)
ws.on('ping', () => {
console.log(`[TerminalService] Ping received from ${terminalId}`);
ws.pong();
});
// Handle WebSocket pong (response to our ping)
ws.on('pong', () => {
console.log(`[TerminalService] Pong received from ${terminalId}`);
terminal.lastActivity = new Date().toISOString();
});
// Handle PTY output - send to client // Handle PTY output - send to client
terminal.pty.onData((data) => { terminal.pty.onData((data) => {
console.log(`[TerminalService] PTY data from ${terminalId}: ${data.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}`); console.log(`[TerminalService] PTY data from ${terminalId}: ${data.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}`);
@@ -196,8 +232,21 @@ class TerminalService {
mode: terminal.mode mode: terminal.mode
}); });
console.log(`[TerminalService] Sending ready message to ${terminalId}: ${readyMessage}`); console.log(`[TerminalService] Sending ready message to ${terminalId}: ${readyMessage}`);
ws.send(readyMessage);
console.log(`[TerminalService] Ready message sent successfully`); try {
ws.send(readyMessage);
console.log(`[TerminalService] Ready message sent successfully`);
// Send a ping immediately after ready to ensure connection stays alive
setTimeout(() => {
if (ws.readyState === ws.OPEN) {
ws.ping();
console.log(`[TerminalService] Sent ping after ready message`);
}
}, 100);
} catch (error) {
console.error(`[TerminalService] Error sending ready message:`, error);
}
} }
/** /**
@@ -267,6 +316,34 @@ class TerminalService {
return { success: true, mode }; return { success: true, mode };
} }
/**
* Send input to terminal via HTTP (WebSocket workaround)
*/
sendTerminalInput(terminalId, data) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
if (!terminal.pty) {
return { success: false, error: 'PTY not found' };
}
try {
// Write directly to PTY
terminal.pty.write(data);
terminal.lastActivity = new Date().toISOString();
console.log(`[TerminalService] Wrote to PTY ${terminalId}: ${data.replace(/\n/g, '\\n')}`);
return { success: true };
} catch (error) {
console.error(`[TerminalService] Error writing to PTY ${terminalId}:`, error);
return { success: false, error: error.message };
}
}
/** /**
* Close terminal and kill PTY process * Close terminal and kill PTY process
*/ */
@@ -373,6 +450,12 @@ class TerminalService {
async cleanup() { async cleanup() {
console.log('[TerminalService] Cleaning up all terminals...'); console.log('[TerminalService] Cleaning up all terminals...');
// Clear ping interval
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
for (const [id, terminal] of this.terminals.entries()) { for (const [id, terminal] of this.terminals.entries()) {
try { try {
if (terminal.pty) { if (terminal.pty) {