feat: implement HTTP polling to bypass WebSocket issue
The WebSocket closes immediately with code 1006 due to nginx/proxy issues. This implementation completely bypasses WebSocket for terminal output by using HTTP polling instead. Architecture: - Server buffers PTY output in memory (outputBuffer array) - Frontend polls every 100ms via GET /terminals/:id/output?since=N - Output entries have monotonically increasing index - Old output is automatically cleaned up after 5 minutes - Commands sent via HTTP POST (already implemented) Changes: - terminal-service.js: Added bufferOutput(), getTerminalOutput() - terminal.js: Added startPolling(), stopPolling(), sendTerminalResize() - server.js: Added GET /terminals/:id/output and POST /terminals/:id/resize - No WebSocket needed for output display (keeps legacy compatibility) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* Terminal Manager - Frontend for xterm.js terminals
|
||||
* Terminal Manager - Frontend for xterm.js terminals with HTTP polling
|
||||
* Uses HTTP polling to bypass WebSocket/SSL issues
|
||||
*/
|
||||
|
||||
class TerminalManager {
|
||||
constructor() {
|
||||
this.terminals = new Map(); // terminalId -> { terminal, ws, fitAddon, container, mode, ready }
|
||||
this.terminals = new Map(); // terminalId -> { terminal, fitAddon, container, mode, polling, currentOutputIndex }
|
||||
this.activeTerminalId = null;
|
||||
this.xtermLoaded = false;
|
||||
this.terminalsContainer = null;
|
||||
this.terminalTabsContainer = null;
|
||||
this.debugMessages = [];
|
||||
this.pollingInterval = 100; // ms between polls
|
||||
this.pollingTimers = new Map(); // terminalId -> timer
|
||||
|
||||
// Bind methods
|
||||
this.createTerminal = this.createTerminal.bind(this);
|
||||
@@ -39,11 +42,12 @@ class TerminalManager {
|
||||
if (debugContent) {
|
||||
const colorMap = {
|
||||
'INIT': '#4a9eff',
|
||||
'WS': '#a78bfa',
|
||||
'HTTP': '#a78bfa',
|
||||
'CMD': '#51cf66',
|
||||
'ERROR': '#ff6b6b',
|
||||
'READY': '#ffd43b',
|
||||
'PTY': '#ffa94d'
|
||||
'PTY': '#ffa94d',
|
||||
'POLL': '#ff922b'
|
||||
};
|
||||
const color = colorMap[category] || '#e0e0e0';
|
||||
|
||||
@@ -262,17 +266,14 @@ class TerminalManager {
|
||||
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
|
||||
// Initialize xterm.js
|
||||
this.debugLog('INIT', `Initializing xterm.js...`);
|
||||
await this.initializeXTerm(terminalId);
|
||||
this.debugLog('INIT', `xterm.js initialized, terminal should be in map now`);
|
||||
this.debugLog('INIT', `xterm.js initialized`);
|
||||
|
||||
// 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`);
|
||||
// Start HTTP polling for output (NO WebSocket needed!)
|
||||
this.debugLog('POLL', `Starting HTTP polling for ${terminalId}...`);
|
||||
this.startPolling(terminalId);
|
||||
|
||||
// Switch to new terminal
|
||||
this.debugLog('INIT', `Switching to terminal ${terminalId}...`);
|
||||
@@ -282,9 +283,7 @@ class TerminalManager {
|
||||
// Handle terminal type specific initialization
|
||||
if (selectedTerminalType === 'claude-cli') {
|
||||
this.debugLog('CMD', `Claude Code CLI terminal selected, launching command...`);
|
||||
// TEMPORARY: Test with simple echo command first
|
||||
// TODO: Change back to 'claude --dangerously-skip-permissions\n' after testing
|
||||
await this.launchCommand(terminalId, 'echo "WebSocket test"\n');
|
||||
await this.launchCommand(terminalId, 'claude --dangerously-skip-permissions\n');
|
||||
|
||||
if (!silent) {
|
||||
showToast('Claude Code CLI terminal created', 'success');
|
||||
@@ -641,22 +640,13 @@ class TerminalManager {
|
||||
* Launch Claude CLI in terminal with optional session
|
||||
*/
|
||||
async launchClaudeCLI(terminalId, sessionId, source = 'web') {
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (!terminal || !terminal.ws || terminal.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('[TerminalManager] Terminal not ready for CLI launch');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct command
|
||||
const command = sessionId
|
||||
? `claude --session ${sessionId}\n`
|
||||
: 'claude\n';
|
||||
|
||||
// Send command to terminal
|
||||
terminal.ws.send(JSON.stringify({
|
||||
type: 'input',
|
||||
data: command
|
||||
}));
|
||||
// Send command to terminal via HTTP
|
||||
await this.sendTerminalInput(terminalId, command);
|
||||
|
||||
// Update mode to session
|
||||
await this.setMode(terminalId, 'session');
|
||||
@@ -675,87 +665,96 @@ class TerminalManager {
|
||||
}
|
||||
|
||||
// Store session ID and source in terminal data
|
||||
if (sessionId) {
|
||||
terminal.sessionId = sessionId;
|
||||
terminal.sessionSource = source;
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (terminal) {
|
||||
if (sessionId) {
|
||||
terminal.sessionId = sessionId;
|
||||
terminal.sessionSource = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a command in the terminal
|
||||
* Uses HTTP POST as workaround for WebSocket send issue
|
||||
* Launch a command in the terminal via HTTP
|
||||
*/
|
||||
async launchCommand(terminalId, command) {
|
||||
this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`);
|
||||
|
||||
// Wait for terminal to be ready (max 5 seconds)
|
||||
this.debugLog('CMD', `Waiting for terminal ${terminalId} to be ready...`);
|
||||
const ready = await this.waitForTerminalReady(terminalId, 5000);
|
||||
|
||||
if (!ready) {
|
||||
this.debugLog('ERROR', `Terminal ${terminalId} NOT ready (timeout after 5s)`);
|
||||
showToast('Terminal not ready. Please try again.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.debugLog('CMD', `Terminal ${terminalId} is ready!`);
|
||||
|
||||
// Use HTTP POST instead of WebSocket send (bypasses proxy issue)
|
||||
this.debugLog('CMD', `Sending command via HTTP POST: ${command.trim()}`);
|
||||
|
||||
try {
|
||||
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', `HTTP POST failed:`, { error: error.message });
|
||||
showToast(`Failed to send command: ${error.message}`, 'error');
|
||||
}
|
||||
await this.sendTerminalInput(terminalId, command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for terminal to be ready
|
||||
* Start HTTP polling for terminal output
|
||||
*/
|
||||
async waitForTerminalReady(terminalId, timeout = 5000) {
|
||||
const startTime = Date.now();
|
||||
startPolling(terminalId) {
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (!terminal) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const checkReady = () => {
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
terminal.currentOutputIndex = 0;
|
||||
terminal.polling = true;
|
||||
|
||||
if (terminal && terminal.ready) {
|
||||
console.log(`[TerminalManager] Terminal ${terminalId} is ready`);
|
||||
resolve(true);
|
||||
return;
|
||||
const poll = async () => {
|
||||
const term = this.terminals.get(terminalId);
|
||||
if (!term || !term.polling) {
|
||||
this.debugLog('POLL', `Stopped polling for ${terminalId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/claude/api/terminals/${terminalId}/output?since=${term.currentOutputIndex}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.output && data.output.length > 0) {
|
||||
this.debugLog('POLL', `Received ${data.output.length} output entries for ${terminalId}`);
|
||||
|
||||
// Write output to xterm.js
|
||||
for (const entry of data.output) {
|
||||
if (entry.type === 'data') {
|
||||
if (term.terminal) {
|
||||
term.terminal.write(entry.data);
|
||||
}
|
||||
} else if (entry.type === 'exit') {
|
||||
this.debugLog('PTY', `Terminal exited: ${entry.exitCode || entry.signal}`);
|
||||
showToast(`Terminal exited: ${entry.exitCode || 'terminated'}`, 'info');
|
||||
} else if (entry.type === 'modeChanged') {
|
||||
this.debugLog('INIT', `Mode changed to ${entry.mode}`);
|
||||
this.updateModeDisplay(terminalId, entry.mode);
|
||||
}
|
||||
}
|
||||
|
||||
// Update current index
|
||||
term.currentOutputIndex = data.currentIndex;
|
||||
}
|
||||
} catch (error) {
|
||||
this.debugLog('ERROR', `Polling error for ${terminalId}:`, { error: error.message });
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > timeout) {
|
||||
console.error(`[TerminalManager] Terminal ${terminalId} ready timeout`);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
// Schedule next poll
|
||||
if (term.polling) {
|
||||
this.pollingTimers.set(terminalId, setTimeout(poll, this.pollingInterval));
|
||||
}
|
||||
};
|
||||
|
||||
// Check again in 100ms
|
||||
setTimeout(checkReady, 100);
|
||||
};
|
||||
// Start polling
|
||||
poll();
|
||||
this.debugLog('POLL', `Started polling for ${terminalId} at ${this.pollingInterval}ms interval`);
|
||||
}
|
||||
|
||||
checkReady();
|
||||
});
|
||||
/**
|
||||
* Stop HTTP polling for terminal
|
||||
*/
|
||||
stopPolling(terminalId) {
|
||||
const timer = this.pollingTimers.get(terminalId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.pollingTimers.delete(terminalId);
|
||||
}
|
||||
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (terminal) {
|
||||
terminal.polling = false;
|
||||
}
|
||||
|
||||
this.debugLog('POLL', `Stopped polling for ${terminalId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -878,163 +877,6 @@ class TerminalManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect terminal WebSocket
|
||||
* Waits for 'ready' message from backend before resolving
|
||||
*/
|
||||
async connectTerminal(terminalId) {
|
||||
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 = () => {
|
||||
this.debugLog('WS', `✅ WebSocket OPENED for terminal ${terminalId}`);
|
||||
this.debugLog('WS', `Checking if terminal exists in map...`);
|
||||
|
||||
// Store WebSocket in terminal entry
|
||||
// NOTE: This assumes initializeXTerm() has already been called
|
||||
// and this.terminals has the entry
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (terminal) {
|
||||
this.debugLog('WS', `✅ Terminal found in map, storing WebSocket`);
|
||||
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 {
|
||||
this.debugLog('ERROR', `❌ CRITICAL: Terminal ${terminalId} not found in map!`);
|
||||
this.debugLog('ERROR', `Available terminals:`, {
|
||||
count: this.terminals.size,
|
||||
ids: Array.from(this.terminals.keys())
|
||||
});
|
||||
reject(new Error(`Terminal ${terminalId} not initialized`));
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
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') {
|
||||
this.debugLog('READY', `✅ Ready message received for ${terminalId}, PTY initialized`, message);
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (terminal) {
|
||||
terminal.ready = true;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
this.debugLog('ERROR', `Failed to parse WebSocket message`, { error: error.message, data: event.data });
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
this.debugLog('ERROR', `✖✖✖ WebSocket ERROR for terminal ${terminalId} ✖✖✖`, {
|
||||
type: error.type || 'unknown',
|
||||
message: error.message || 'No error message',
|
||||
error: error
|
||||
});
|
||||
this.debugLog('ERROR', `Check browser console (F12) for full error details`);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
const closeReasons = {
|
||||
1000: 'Normal Closure',
|
||||
1001: 'Endpoint Going Away',
|
||||
1002: 'Protocol Error',
|
||||
1003: 'Unsupported Data',
|
||||
1004: '(Reserved)',
|
||||
1005: 'No Status Received',
|
||||
1006: 'Abnormal Closure (connection dropped)',
|
||||
1007: 'Invalid frame payload data',
|
||||
1008: 'Policy Violation',
|
||||
1009: 'Message Too Big',
|
||||
1010: 'Mandatory Extension',
|
||||
1011: 'Internal Server Error',
|
||||
1015: 'TLS Handshake'
|
||||
};
|
||||
|
||||
const closeReason = closeReasons[event.code] || `Unknown (${event.code})`;
|
||||
|
||||
this.debugLog('ERROR', `🔌 WebSocket CLOSED for terminal ${terminalId}`, {
|
||||
code: event.code,
|
||||
reasonName: closeReason,
|
||||
reason: event.reason || 'None provided',
|
||||
wasClean: event.wasClean,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (event.code === 1006) {
|
||||
this.debugLog('ERROR', `💡 Code 1006 means connection was dropped abnormally - check server logs`);
|
||||
this.debugLog('ERROR', `💡 Common causes: server crash, network issue, or timeout`);
|
||||
}
|
||||
|
||||
// Show terminals currently in map for debugging
|
||||
this.debugLog('ERROR', `Current terminals in map:`, {
|
||||
count: this.terminals.size,
|
||||
ids: Array.from(this.terminals.keys())
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
this.debugLog('ERROR', `Exception connecting WebSocket`, error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle terminal message from WebSocket
|
||||
*/
|
||||
handleTerminalMessage(terminalId, message) {
|
||||
this.debugLog('WS', `Handling message: type="${message.type}"`, message);
|
||||
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
|
||||
if (!terminal) {
|
||||
this.debugLog('ERROR', `Cannot handle message - terminal ${terminalId} not found in map`, { terminalsInMap: Array.from(this.terminals.keys()) });
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case 'ready':
|
||||
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':
|
||||
this.debugLog('PTY', `Terminal exited: ${message.exitCode || signal}`);
|
||||
showToast(`Terminal exited: ${message.exitCode || 'terminated'}`, 'info');
|
||||
break;
|
||||
|
||||
case 'modeChanged':
|
||||
this.debugLog('INIT', `Mode changed to ${message.mode}`);
|
||||
this.updateModeDisplay(terminalId, message.mode);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.debugLog('WS', `Unknown message type: ${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize xterm.js instance for terminal
|
||||
*/
|
||||
@@ -1101,70 +943,86 @@ class TerminalManager {
|
||||
// Auto-focus terminal when created
|
||||
setTimeout(() => terminal.focus(), 100);
|
||||
|
||||
// Handle user input
|
||||
// Handle user input - send via HTTP POST
|
||||
terminal.onData((data) => {
|
||||
this.sendTerminalInput(terminalId, data);
|
||||
});
|
||||
|
||||
// Handle resize
|
||||
// Handle resize - send via HTTP POST
|
||||
terminal.onResize(({ cols, rows }) => {
|
||||
this.sendTerminalResize(terminalId, cols, rows);
|
||||
});
|
||||
|
||||
// Store terminal instance in map
|
||||
// This MUST happen before connectTerminal() is called
|
||||
this.terminals.set(terminalId, {
|
||||
terminal,
|
||||
fitAddon,
|
||||
ws: null, // Will be set by connectTerminal()
|
||||
container,
|
||||
mode: 'mixed',
|
||||
ready: false // Will be set to true when 'ready' message received
|
||||
polling: false,
|
||||
currentOutputIndex: 0
|
||||
});
|
||||
|
||||
this.debugLog('INIT', `xterm.js instance stored in map for ${terminalId}, terminals now has`, { terminalIds: Array.from(this.terminals.keys()) });
|
||||
this.debugLog('INIT', `xterm.js instance stored in map for ${terminalId}`);
|
||||
|
||||
return terminal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send input to terminal
|
||||
* Send input to terminal via HTTP POST
|
||||
*/
|
||||
sendTerminalInput(terminalId, data) {
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
async sendTerminalInput(terminalId, data) {
|
||||
try {
|
||||
const res = await fetch(`/claude/api/terminals/${terminalId}/input`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data })
|
||||
});
|
||||
|
||||
if (!terminal || !terminal.ws || terminal.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!result.success) {
|
||||
this.debugLog('ERROR', `Failed to send input: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.debugLog('ERROR', `Error sending input:`, { error: error.message });
|
||||
}
|
||||
|
||||
terminal.ws.send(JSON.stringify({
|
||||
type: 'input',
|
||||
data
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send terminal resize
|
||||
* Send terminal resize via HTTP POST
|
||||
*/
|
||||
sendTerminalResize(terminalId, cols, rows) {
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
async sendTerminalResize(terminalId, cols, rows) {
|
||||
try {
|
||||
const res = await fetch(`/claude/api/terminals/${terminalId}/resize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cols, rows })
|
||||
});
|
||||
|
||||
if (!terminal || !terminal.ws || terminal.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!result.success) {
|
||||
this.debugLog('ERROR', `Failed to resize terminal: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.debugLog('ERROR', `Error resizing terminal:`, { error: error.message });
|
||||
}
|
||||
|
||||
terminal.ws.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols,
|
||||
rows
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to specific terminal
|
||||
*/
|
||||
async switchToTerminal(terminalId) {
|
||||
this.debugLog('INIT', `switchToTerminal called for ${terminalId}, checking if terminal in map...`);
|
||||
this.debugLog('INIT', `switchToTerminal called for ${terminalId}`);
|
||||
|
||||
// Hide all containers and remove active class
|
||||
document.querySelectorAll('.terminal-container').forEach(c => {
|
||||
@@ -1183,30 +1041,16 @@ class TerminalManager {
|
||||
container.classList.add('active');
|
||||
container.style.display = 'flex';
|
||||
|
||||
// Initialize xterm.js if not already done
|
||||
if (!this.terminals.has(terminalId)) {
|
||||
this.debugLog('INIT', `Terminal ${terminalId} NOT in map, calling initializeXTerm...`);
|
||||
await this.initializeXTerm(terminalId);
|
||||
} else {
|
||||
this.debugLog('INIT', `Terminal ${terminalId} already in map, skipping initialization`);
|
||||
}
|
||||
|
||||
// Fit terminal - DISABLED to avoid WebSocket closure issue
|
||||
// Fit terminal
|
||||
const terminal = this.terminals.get(terminalId);
|
||||
if (terminal && terminal.fitAddon) {
|
||||
this.debugLog('INIT', `Skipping fitAddon.fit() for ${terminalId} to avoid WebSocket closure`);
|
||||
// TODO: Investigate why fitAddon.fit() causes WebSocket to close
|
||||
// setTimeout(() => {
|
||||
// this.debugLog('INIT', `Executing fitAddon.fit() for ${terminalId}`);
|
||||
// try {
|
||||
// terminal.fitAddon.fit();
|
||||
// this.debugLog('INIT', `fitAddon.fit() completed for ${terminalId}`);
|
||||
// } catch (error) {
|
||||
// this.debugLog('ERROR', `fitAddon.fit() failed for ${terminalId}`, { error: error.message });
|
||||
// }
|
||||
// }, 100);
|
||||
} else {
|
||||
this.debugLog('INIT', `No fitAddon for terminal ${terminalId} (terminal=${!!terminal}, fitAddon=${!!terminal?.fitAddon})`);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
terminal.fitAddon.fit();
|
||||
} catch (error) {
|
||||
this.debugLog('ERROR', `fitAddon.fit() failed for ${terminalId}`, { error: error.message });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,6 +1069,9 @@ class TerminalManager {
|
||||
*/
|
||||
async closeTerminal(terminalId) {
|
||||
try {
|
||||
// Stop polling
|
||||
this.stopPolling(terminalId);
|
||||
|
||||
// Close via API
|
||||
await fetch(`/claude/api/terminals/${terminalId}`, {
|
||||
method: 'DELETE'
|
||||
|
||||
Reference in New Issue
Block a user