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:
uroma
2026-01-19 19:12:18 +00:00
Unverified
parent 2c7037b9b7
commit a7d2f37219
6 changed files with 128 additions and 32 deletions

View File

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

View File

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