Files
SuperCharged-Claude-Code-Up…/services/terminal-service.js
uroma a7d2f37219 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.
2026-01-19 19:12:18 +00:00

394 lines
13 KiB
JavaScript

/**
* Terminal Service - Manages PTY processes and WebSocket connections
*/
const { spawn } = require('node-pty');
const { Server } = require('ws');
const fs = require('fs').promises;
const path = require('path');
class TerminalService {
constructor() {
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');
}
/**
* Setup WebSocket server for terminal I/O
*/
createServer(httpServer) {
this.wsServer = new Server({ noServer: true });
// Handle WebSocket upgrade requests
httpServer.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname;
// Match /claude/api/terminals/:id/ws
const terminalMatch = pathname.match(/^\/claude\/api\/terminals\/([^/]+)\/ws$/);
if (terminalMatch) {
const terminalId = terminalMatch[1];
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
this.wsServer.emit('connection', ws, request, terminalId);
});
}
});
// Handle WebSocket connections
this.wsServer.on('connection', (ws, request, terminalId) => {
this.handleConnection(terminalId, ws);
});
console.log('[TerminalService] WebSocket server initialized');
}
/**
* Create a new terminal PTY
*/
createTerminal(options) {
const {
workingDir = process.env.HOME,
sessionId = null,
mode = 'mixed',
shell = process.env.SHELL || '/bin/bash'
} = options;
// Generate unique terminal ID
const terminalId = `term-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try {
// Spawn PTY process
const pty = spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: workingDir,
env: process.env
});
// Store terminal info
const terminal = {
id: terminalId,
pty,
ws: null,
sessionId,
workingDir,
mode, // 'session', 'shell', or 'mixed'
createdAt: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
this.terminals.set(terminalId, terminal);
// Log terminal creation
this.logCommand(terminalId, null, `Terminal created in ${workingDir} (mode: ${mode})`);
console.log(`[TerminalService] Created terminal ${terminalId} in ${workingDir}`);
return { success: true, terminalId, terminal };
} catch (error) {
console.error(`[TerminalService] Failed to create terminal:`, error);
return { success: false, error: error.message };
}
}
/**
* Handle WebSocket connection for terminal I/O
*/
handleConnection(terminalId, ws) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
console.error(`[TerminalService] TERMINAL NOT FOUND: ${terminalId}`);
ws.close(1008, 'Terminal not found');
return;
}
terminal.ws = ws;
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();
// Log commands (basic detection of Enter key)
if (message.data === '\r') {
// Could extract and log command here
}
} 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) {
console.error('[TerminalService] Error handling message:', error);
}
});
// Handle PTY output - send to client
terminal.pty.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'data',
data: data
}));
}
});
// Handle PTY exit
terminal.pty.onExit(({ exitCode, signal }) => {
console.log(`[TerminalService] Terminal ${terminalId} exited: ${exitCode || signal}`);
this.logCommand(terminalId, null, `Terminal exited: ${exitCode || signal}`);
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'exit',
exitCode,
signal
}));
ws.close();
}
this.terminals.delete(terminalId);
});
// Handle WebSocket close
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
});
// Handle WebSocket error
ws.on('error', (error) => {
console.error(`[TerminalService] WebSocket error for terminal ${terminalId}:`, error);
});
// Send initial welcome message
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`);
}
/**
* Attach terminal to a Claude Code session
* Pipes session stdout/stderr to PTY
*/
attachToSession(terminalId, session) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
if (!session || !session.process) {
return { success: false, error: 'Invalid session' };
}
terminal.sessionId = session.id;
terminal.mode = 'session';
// Pipe session output to PTY
if (session.process.stdout) {
session.process.stdout.on('data', (data) => {
if (terminal.pty) {
terminal.pty.write(data.toString());
}
});
}
if (session.process.stderr) {
session.process.stderr.on('data', (data) => {
if (terminal.pty) {
// Write stderr in red
terminal.pty.write(`\x1b[31m${data.toString()}\x1b[0m`);
}
});
}
this.logCommand(terminalId, null, `Attached to session ${session.id}`);
console.log(`[TerminalService] Terminal ${terminalId} attached to session ${session.id}`);
return { success: true };
}
/**
* Set terminal mode (session/shell/mixed)
*/
setTerminalMode(terminalId, mode) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
terminal.mode = mode;
terminal.lastActivity = new Date().toISOString();
// Notify client of mode change
if (terminal.ws && terminal.ws.readyState === terminal.ws.OPEN) {
terminal.ws.send(JSON.stringify({
type: 'modeChanged',
mode
}));
}
return { success: true, mode };
}
/**
* Close terminal and kill PTY process
*/
closeTerminal(terminalId) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
try {
// Kill PTY process
if (terminal.pty) {
terminal.pty.kill();
}
// Close WebSocket
if (terminal.ws && terminal.ws.readyState === terminal.ws.OPEN) {
terminal.ws.close(1000, 'Terminal closed');
}
this.terminals.delete(terminalId);
this.logCommand(terminalId, null, 'Terminal closed');
console.log(`[TerminalService] Terminal ${terminalId} closed`);
return { success: true };
} catch (error) {
console.error(`[TerminalService] Error closing terminal ${terminalId}:`, error);
return { success: false, error: error.message };
}
}
/**
* Get list of active terminals
*/
listTerminals() {
const terminals = [];
for (const [id, terminal] of this.terminals.entries()) {
terminals.push({
id,
workingDir: terminal.workingDir,
sessionId: terminal.sessionId,
mode: terminal.mode,
createdAt: terminal.createdAt,
lastActivity: terminal.lastActivity
});
}
return terminals;
}
/**
* Get terminal by ID
*/
getTerminal(terminalId) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
return {
success: true,
terminal: {
id: terminal.id,
workingDir: terminal.workingDir,
sessionId: terminal.sessionId,
mode: terminal.mode,
createdAt: terminal.createdAt,
lastActivity: terminal.lastActivity
}
};
}
/**
* Log command to file
*/
async logCommand(terminalId, command, action) {
try {
const logEntry = {
timestamp: new Date().toISOString(),
terminalId,
command: command || action,
user: process.env.USER || 'unknown'
};
// Ensure directory exists
const logDir = path.dirname(this.logFile);
await fs.mkdir(logDir, { recursive: true });
// Append to log file
await fs.appendFile(this.logFile, JSON.stringify(logEntry) + '\n');
} catch (error) {
console.error('[TerminalService] Failed to log command:', error);
}
}
/**
* Cleanup all terminals (called on server shutdown)
*/
async cleanup() {
console.log('[TerminalService] Cleaning up all terminals...');
for (const [id, terminal] of this.terminals.entries()) {
try {
if (terminal.pty) {
terminal.pty.kill();
}
if (terminal.ws && terminal.ws.readyState === terminal.ws.OPEN) {
terminal.ws.close(1000, 'Server shutting down');
}
} catch (error) {
console.error(`[TerminalService] Error cleaning up terminal ${id}:`, error);
}
}
this.terminals.clear();
// Close WebSocket server
if (this.wsServer) {
this.wsServer.close();
}
console.log('[TerminalService] Cleanup complete');
}
}
// Export singleton instance
const terminalService = new TerminalService();
module.exports = terminalService;