diff --git a/database.sqlite-shm b/database.sqlite-shm index d5a87010..edd3e277 100644 Binary files a/database.sqlite-shm and b/database.sqlite-shm differ diff --git a/database.sqlite-wal b/database.sqlite-wal index 7cddc077..bc76b4bd 100644 Binary files a/database.sqlite-wal and b/database.sqlite-wal differ diff --git a/public/claude-ide/terminal.js b/public/claude-ide/terminal.js index 808ba9d9..f7cd86b9 100644 --- a/public/claude-ide/terminal.js +++ b/public/claude-ide/terminal.js @@ -683,7 +683,7 @@ class TerminalManager { /** * 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) { this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`); @@ -700,44 +700,30 @@ class TerminalManager { this.debugLog('CMD', `Terminal ${terminalId} is ready!`); - // NO DELAY - send command immediately to avoid WebSocket closure - this.debugLog('CMD', `Sending command immediately without delay...`); - - 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}`); + // Use HTTP POST instead of WebSocket send (bypasses proxy issue) + this.debugLog('CMD', `Sending command via HTTP POST: ${command.trim()}`); try { - terminal.ws.send(message); - this.debugLog('CMD', `Command sent to terminal ${terminalId}: ${command.trim()}`); + const res = await fetch(`/claude/api/terminals/${terminalId}/input`, { + 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) { - 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'); } } diff --git a/server.js b/server.js index 7760641c..2162ae44 100644 --- a/server.js +++ b/server.js @@ -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 app.get('/claude/api/files/recent-dirs', requireAuth, (req, res) => { try { diff --git a/services/terminal-service.js b/services/terminal-service.js index 7d80df68..3b68bbc4 100644 --- a/services/terminal-service.js +++ b/services/terminal-service.js @@ -12,6 +12,7 @@ class TerminalService { this.terminals = new Map(); // terminalId -> { pty, ws, sessionId, workingDir, mode, createdAt } this.wsServer = null; 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) { const terminalId = terminalMatch[1]; + console.log(`[TerminalService] Handling WebSocket upgrade for terminal ${terminalId}`); this.wsServer.handleUpgrade(request, socket, head, (ws) => { this.wsServer.emit('connection', ws, request, terminalId); }); @@ -37,12 +39,34 @@ class TerminalService { // Handle WebSocket connections this.wsServer.on('connection', (ws, request, terminalId) => { + console.log(`[TerminalService] WebSocket connection event received for terminal ${terminalId}`); this.handleConnection(terminalId, ws); }); + // Setup ping interval to keep connections alive + this.setupPingInterval(); + 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 */ @@ -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 terminal.pty.onData((data) => { 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 }); 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 }; } + /** + * 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 */ @@ -373,6 +450,12 @@ class TerminalService { async cleanup() { 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()) { try { if (terminal.pty) {