debug: add visual debug panel and comprehensive logging
- Added debug panel in terminal view that shows all terminal activity - Added debugLog() method to TerminalManager for consistent logging - Updated connectTerminal, handleTerminalMessage, launchCommand, createTerminal, initializeXTerm with detailed logging - Enhanced backend logging for WebSocket messages and close codes - Logs now show both to console and visual debug panel This should help diagnose the terminal command execution issue without requiring browser console access.
This commit is contained in:
@@ -224,6 +224,17 @@
|
||||
<p>Click "+ New Terminal" to get started</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug Panel -->
|
||||
<div id="terminal-debug-panel" style="margin: 20px; padding: 15px; background: #1a1a1a; border: 1px solid #ff6b6b; border-radius: 8px; font-family: monospace; font-size: 12px; color: #e0e0e0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h3 style="margin: 0; color: #ff6b6b;">🐛 Terminal Debug Panel</h3>
|
||||
<button onclick="document.getElementById('terminal-debug-panel').style.display = 'none'" style="background: #ff6b6b; border: none; color: white; padding: 4px 12px; border-radius: 4px; cursor: pointer;">Close</button>
|
||||
</div>
|
||||
<div id="terminal-debug-content" style="max-height: 300px; overflow-y: auto;">
|
||||
<div style="color: #888;">Waiting for terminal activity...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 `<div style="margin-bottom: 4px;"><span style="color: #888;">[${msg.timestamp}]</span> <span style="color: ${displayColor}; font-weight: bold;">[${msg.category}]</span> ${msg.message}${msg.data ? ` <span style="color: #888;">- ${JSON.stringify(msg.data)}</span>` : ''}</div>`;
|
||||
}).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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user