diff --git a/database.sqlite b/database.sqlite index 8b63d091..cc5ee420 100644 Binary files a/database.sqlite and b/database.sqlite differ diff --git a/database.sqlite-shm b/database.sqlite-shm index f8c74514..e6cdcd3f 100644 Binary files a/database.sqlite-shm and b/database.sqlite-shm differ diff --git a/database.sqlite-wal b/database.sqlite-wal index c8f2aa4f..bc92952e 100644 Binary files a/database.sqlite-wal and b/database.sqlite-wal differ diff --git a/public/claude-ide/index.html b/public/claude-ide/index.html index 8189625d..01c19e79 100644 --- a/public/claude-ide/index.html +++ b/public/claude-ide/index.html @@ -224,6 +224,17 @@

Click "+ New Terminal" to get started

+ + +
+
+

🐛 Terminal Debug Panel

+ +
+
+
Waiting for terminal activity...
+
+
diff --git a/public/claude-ide/terminal.js b/public/claude-ide/terminal.js index 3a4ff5e0..192ac7ac 100644 --- a/public/claude-ide/terminal.js +++ b/public/claude-ide/terminal.js @@ -9,6 +9,7 @@ class TerminalManager { this.xtermLoaded = false; this.terminalsContainer = null; this.terminalTabsContainer = null; + this.debugMessages = []; // Bind methods this.createTerminal = this.createTerminal.bind(this); @@ -18,6 +19,44 @@ class TerminalManager { this.clearScreen = this.clearScreen.bind(this); } + /** + * Log debug message to both console and visual debug panel + */ + debugLog(category, message, data = null) { + const timestamp = new Date().toLocaleTimeString(); + const logEntry = `[${timestamp}] [${category}] ${message}`; + + // Log to console + console.log(logEntry, data || ''); + + // Add to debug panel + this.debugMessages.push({ timestamp, category, message, data }); + if (this.debugMessages.length > 50) { + this.debugMessages.shift(); // Keep only last 50 messages + } + + const debugContent = document.getElementById('terminal-debug-content'); + if (debugContent) { + const colorMap = { + 'INIT': '#4a9eff', + 'WS': '#a78bfa', + 'CMD': '#51cf66', + 'ERROR': '#ff6b6b', + 'READY': '#ffd43b', + 'PTY': '#ffa94d' + }; + const color = colorMap[category] || '#e0e0e0'; + + debugContent.innerHTML = this.debugMessages.map(msg => { + const displayColor = colorMap[msg.category] || '#e0e0e0'; + return `
[${msg.timestamp}] [${msg.category}] ${msg.message}${msg.data ? ` - ${JSON.stringify(msg.data)}` : ''}
`; + }).join(''); + + // Auto-scroll to bottom + debugContent.scrollTop = debugContent.scrollHeight; + } + } + /** * Load xterm.js CSS dynamically */ @@ -170,28 +209,35 @@ class TerminalManager { terminalType = null } = options; + this.debugLog('INIT', `createTerminal called with options`, { workingDir, sessionId, mode, terminalType }); + // Show directory picker if no working directory provided const selection = workingDir ? { directory: workingDir, terminalType: terminalType || 'standard' } : await this.showDirectoryPicker(); if (!selection) { + this.debugLog('INIT', `User cancelled directory picker`); return null; } const { directory: selectedDir, terminalType: selectedTerminalType } = selection; + this.debugLog('INIT', `Directory selected: ${selectedDir}, terminalType: ${selectedTerminalType}`); // If no session provided and not skipping picker, show session picker let sessionSelection = null; if (!sessionId && !skipSessionPicker && selectedTerminalType !== 'claude-cli') { // Skip session picker if Claude Code CLI terminal is selected + this.debugLog('INIT', `Showing session picker...`); sessionSelection = await this.showSessionPicker(); // If user cancelled session picker, still create terminal but without session // sessionSelection will be null or { sessionId: string, source: 'web'|'local' } or { sessionId: 'new', source: 'new' } + this.debugLog('INIT', `Session picker result:`, sessionSelection); } try { // Create terminal via API + this.debugLog('INIT', `Calling /claude/api/terminals to create terminal...`); const res = await fetch('/claude/api/terminals', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -205,27 +251,37 @@ class TerminalManager { const data = await res.json(); if (!data.success) { + this.debugLog('ERROR', `API call failed:`, data); throw new Error(data.error || 'Failed to create terminal'); } const terminalId = data.terminalId; + this.debugLog('INIT', `Terminal created with ID: ${terminalId}`); // Create terminal UI + this.debugLog('INIT', `Creating terminal UI...`); await this.createTerminalUI(terminalId, selectedDir, mode); // Initialize xterm.js FIRST (before connecting WebSocket) // This ensures this.terminals map has the entry ready + this.debugLog('INIT', `Initializing xterm.js...`); await this.initializeXTerm(terminalId); + this.debugLog('INIT', `xterm.js initialized, terminal should be in map now`); // NOW connect WebSocket (terminal entry exists in map) // This waits for the 'ready' message from backend + this.debugLog('INIT', `Connecting WebSocket...`); await this.connectTerminal(terminalId); + this.debugLog('INIT', `WebSocket connected and ready`); // Switch to new terminal + this.debugLog('INIT', `Switching to terminal ${terminalId}...`); this.switchToTerminal(terminalId); + this.debugLog('INIT', `Switched to terminal ${terminalId}`); // Handle terminal type specific initialization if (selectedTerminalType === 'claude-cli') { + this.debugLog('CMD', `Claude Code CLI terminal selected, launching command...`); // Launch Claude CLI with skip permissions flag // Note: Keep mode as 'mixed' since we're not attaching to a session // connectTerminal now waits for 'ready' message, so PTY is definitely ready @@ -631,23 +687,28 @@ class TerminalManager { * Waits for terminal to be ready before sending command */ async launchCommand(terminalId, command) { - console.log(`[TerminalManager] launchCommand: terminalId=${terminalId}, command="${command.trim()}"`); + this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`); // Wait for terminal to be ready (max 5 seconds) - console.log(`[TerminalManager] Waiting for terminal ${terminalId} to be ready...`); + this.debugLog('CMD', `Waiting for terminal ${terminalId} to be ready...`); const ready = await this.waitForTerminalReady(terminalId, 5000); if (!ready) { - console.error(`[TerminalManager] Terminal ${terminalId} NOT ready (timeout after 5s)`); + this.debugLog('ERROR', `Terminal ${terminalId} NOT ready (timeout after 5s)`); showToast('Terminal not ready. Please try again.', 'error'); return; } - console.log(`[TerminalManager] Terminal ${terminalId} is ready! Sending command.`); + this.debugLog('CMD', `Terminal ${terminalId} is ready! Sending command.`); const terminal = this.terminals.get(terminalId); if (!terminal || !terminal.ws || terminal.ws.readyState !== WebSocket.OPEN) { - console.error(`[TerminalManager] Cannot send - WebSocket not ready. terminal=${!!terminal}, ws=${!!terminal?.ws}, state=${terminal?.ws?.readyState}`); + this.debugLog('ERROR', `Cannot send - WebSocket not ready`, { + hasTerminal: !!terminal, + hasWs: !!terminal?.ws, + wsState: terminal?.ws?.readyState, + stateName: terminal?.ws?.readyState === WebSocket.OPEN ? 'OPEN' : 'NOT OPEN' + }); return; } @@ -656,10 +717,10 @@ class TerminalManager { type: 'input', data: command }); - console.log(`[TerminalManager] Sending to WebSocket: ${message}`); + this.debugLog('CMD', `Sending to WebSocket: ${message}`); terminal.ws.send(message); - console.log(`[TerminalManager] Command sent to terminal ${terminalId}: ${command.trim()}`); + this.debugLog('CMD', `Command sent to terminal ${terminalId}: ${command.trim()}`); } /** @@ -820,12 +881,14 @@ class TerminalManager { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/claude/api/terminals/${terminalId}/ws`; + this.debugLog('WS', `Connecting to WebSocket for terminal ${terminalId}`, { url: wsUrl }); + return new Promise((resolve, reject) => { try { const ws = new WebSocket(wsUrl); ws.onopen = () => { - console.log(`[TerminalManager] WebSocket connected for terminal ${terminalId}`); + this.debugLog('WS', `WebSocket OPENED for terminal ${terminalId}`); // Store WebSocket in terminal entry // NOTE: This assumes initializeXTerm() has already been called @@ -834,8 +897,9 @@ class TerminalManager { if (terminal) { terminal.ws = ws; terminal.ready = false; // Will be set to true when 'ready' message received + this.debugLog('WS', `WebSocket stored in terminal map, waiting for 'ready' message`); } else { - console.error(`[TerminalManager] CRITICAL: Terminal ${terminalId} not found in map! WebSocket connection will be lost.`); + this.debugLog('ERROR', `CRITICAL: Terminal ${terminalId} not found in map!`, { terminalsInMap: Array.from(this.terminals.keys()) }); reject(new Error(`Terminal ${terminalId} not initialized`)); ws.close(); return; @@ -843,30 +907,35 @@ class TerminalManager { }; ws.onmessage = (event) => { - const message = JSON.parse(event.data); - this.handleTerminalMessage(terminalId, message); + try { + const message = JSON.parse(event.data); + this.debugLog('WS', `Message received: type="${message.type}"`, message); + 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; + // If this is the ready message, resolve the promise + if (message.type === 'ready') { + this.debugLog('READY', `✅ Ready message received for ${terminalId}, PTY initialized`, message); + const terminal = this.terminals.get(terminalId); + if (terminal) { + terminal.ready = true; + } + resolve(); } - resolve(); + } catch (error) { + this.debugLog('ERROR', `Failed to parse WebSocket message`, { error: error.message, data: event.data }); } }; ws.onerror = (error) => { - console.error(`[TerminalManager] WebSocket error for terminal ${terminalId}:`, error); + this.debugLog('ERROR', `WebSocket error for terminal ${terminalId}`, error); reject(error); }; - ws.onclose = () => { - console.log(`[TerminalManager] WebSocket closed for terminal ${terminalId}`); + ws.onclose = (event) => { + this.debugLog('WS', `WebSocket CLOSED for terminal ${terminalId}`, { code: event.code, reason: event.reason, wasClean: event.wasClean }); }; } catch (error) { - console.error('[TerminalManager] Error connecting WebSocket:', error); + this.debugLog('ERROR', `Exception connecting WebSocket`, error); reject(error); } }); @@ -876,39 +945,42 @@ class TerminalManager { * Handle terminal message from WebSocket */ handleTerminalMessage(terminalId, message) { - console.log(`[TerminalManager] Received message from backend: type="${message.type}", terminalId="${terminalId}"`, message); + this.debugLog('WS', `Handling message: type="${message.type}"`, message); const terminal = this.terminals.get(terminalId); if (!terminal) { - console.error(`[TerminalManager] Cannot handle message - terminal ${terminalId} not found in map`); + this.debugLog('ERROR', `Cannot handle message - terminal ${terminalId} not found in map`, { terminalsInMap: Array.from(this.terminals.keys()) }); return; } switch (message.type) { case 'ready': - console.log(`[TerminalManager] ✅ Ready message received for ${terminalId}, PTY is initialized`); + this.debugLog('READY', `PTY initialized for ${terminalId}`); break; case 'data': // Write to xterm.js if (terminal.terminal) { + this.debugLog('PTY', `Writing data to xterm.js (${message.data.length} chars)`); terminal.terminal.write(message.data); + } else { + this.debugLog('ERROR', `No xterm.js instance for terminal ${terminalId}`); } break; case 'exit': - console.log(`[TerminalManager] Terminal ${terminalId} exited: ${message.exitCode}`); + this.debugLog('PTY', `Terminal exited: ${message.exitCode || signal}`); showToast(`Terminal exited: ${message.exitCode || 'terminated'}`, 'info'); break; case 'modeChanged': - // Update mode display + this.debugLog('INIT', `Mode changed to ${message.mode}`); this.updateModeDisplay(terminalId, message.mode); break; default: - console.log(`[TerminalManager] Unknown message type: ${message.type}`); + this.debugLog('WS', `Unknown message type: ${message.type}`); } } @@ -916,9 +988,11 @@ class TerminalManager { * Initialize xterm.js instance for terminal */ async initializeXTerm(terminalId) { + this.debugLog('INIT', `initializeXTerm called for ${terminalId}`); const container = document.getElementById(`xterm-${terminalId}`); if (!container) { + this.debugLog('ERROR', `Terminal container not found: ${terminalId}`); throw new Error(`Terminal container not found: ${terminalId}`); } @@ -997,6 +1071,8 @@ class TerminalManager { ready: false // Will be set to true when 'ready' message received }); + this.debugLog('INIT', `xterm.js instance stored in map for ${terminalId}, terminals now has`, { terminalIds: Array.from(this.terminals.keys()) }); + return terminal; } diff --git a/services/terminal-service.js b/services/terminal-service.js index 014f7efb..2d141a3c 100644 --- a/services/terminal-service.js +++ b/services/terminal-service.js @@ -100,6 +100,7 @@ class TerminalService { const terminal = this.terminals.get(terminalId); if (!terminal) { + console.error(`[TerminalService] TERMINAL NOT FOUND: ${terminalId}`); ws.close(1008, 'Terminal not found'); return; } @@ -108,14 +109,18 @@ class TerminalService { terminal.lastActivity = new Date().toISOString(); console.log(`[TerminalService] WebSocket connected for terminal ${terminalId}`); + console.log(`[TerminalService] Terminal info - workingDir: ${terminal.workingDir}, mode: ${terminal.mode}`); // Handle incoming messages from client (user input) ws.on('message', (data) => { + console.log(`[TerminalService] Message received from ${terminalId}: ${data.toString()}`); try { const message = JSON.parse(data); + console.log(`[TerminalService] Parsed message type: ${message.type}`); if (message.type === 'input') { // User typed something - send to PTY + console.log(`[TerminalService] Writing to PTY: "${message.data.replace(/\n/g, '\\n')}"`); terminal.pty.write(message.data); terminal.lastActivity = new Date().toISOString(); @@ -125,6 +130,7 @@ class TerminalService { } } else if (message.type === 'resize') { // Handle terminal resize + console.log(`[TerminalService] Resize to ${message.cols}x${message.rows}`); terminal.pty.resize(message.cols, message.rows); } } catch (error) { @@ -160,8 +166,8 @@ class TerminalService { }); // Handle WebSocket close - ws.on('close', () => { - console.log(`[TerminalService] WebSocket closed for terminal ${terminalId}`); + ws.on('close', (code, reason) => { + console.log(`[TerminalService] WebSocket closed for terminal ${terminalId} - code: ${code}, reason: ${reason || 'none'}`); // Don't kill PTY immediately - allow reconnection // PTY will be killed after timeout or explicit close @@ -173,12 +179,15 @@ class TerminalService { }); // Send initial welcome message - ws.send(JSON.stringify({ + const readyMessage = JSON.stringify({ type: 'ready', terminalId, workingDir: terminal.workingDir, mode: terminal.mode - })); + }); + console.log(`[TerminalService] Sending ready message to ${terminalId}: ${readyMessage}`); + ws.send(readyMessage); + console.log(`[TerminalService] Ready message sent successfully`); } /**