564 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			564 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { Terminal } from '@xterm/xterm';
 | 
						|
import '@xterm/xterm/css/xterm.css';
 | 
						|
import { FitAddon } from '@xterm/addon-fit';
 | 
						|
 | 
						|
export function initializeTerminalComponent() {
 | 
						|
    function terminalData() {
 | 
						|
        return {
 | 
						|
            fullscreen: false,
 | 
						|
            terminalActive: false,
 | 
						|
            message: '(connection closed)',
 | 
						|
            term: null,
 | 
						|
            fitAddon: null,
 | 
						|
            socket: null,
 | 
						|
            commandBuffer: '',
 | 
						|
            pendingWrites: 0,
 | 
						|
            paused: false,
 | 
						|
            MAX_PENDING_WRITES: 5,
 | 
						|
            keepAliveInterval: null,
 | 
						|
            reconnectInterval: null,
 | 
						|
            // Enhanced connection management
 | 
						|
            connectionState: 'disconnected', // 'connecting', 'connected', 'disconnected', 'reconnecting'
 | 
						|
            reconnectAttempts: 0,
 | 
						|
            maxReconnectAttempts: 10,
 | 
						|
            baseReconnectDelay: 1000,
 | 
						|
            maxReconnectDelay: 30000,
 | 
						|
            connectionTimeout: 10000,
 | 
						|
            connectionTimeoutId: null,
 | 
						|
            lastPingTime: null,
 | 
						|
            pingTimeout: 35000, // 5 seconds longer than ping interval
 | 
						|
            pingTimeoutId: null,
 | 
						|
            heartbeatMissed: 0,
 | 
						|
            maxHeartbeatMisses: 3,
 | 
						|
            // Resize handling
 | 
						|
            resizeObserver: null,
 | 
						|
            resizeTimeout: null,
 | 
						|
 | 
						|
            init() {
 | 
						|
                this.setupTerminal();
 | 
						|
 | 
						|
                // Add a small delay for initial connection to ensure everything is ready
 | 
						|
                setTimeout(() => {
 | 
						|
                    this.initializeWebSocket();
 | 
						|
                }, 100);
 | 
						|
 | 
						|
                this.setupTerminalEventListeners();
 | 
						|
 | 
						|
                this.$wire.on('send-back-command', (command) => {
 | 
						|
                    this.sendCommandWhenReady({ command: command });
 | 
						|
                });
 | 
						|
 | 
						|
                this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000);
 | 
						|
 | 
						|
                this.$watch('terminalActive', (active) => {
 | 
						|
                    if (!active && this.keepAliveInterval) {
 | 
						|
                        clearInterval(this.keepAliveInterval);
 | 
						|
                    }
 | 
						|
                    this.$nextTick(() => {
 | 
						|
                        if (active) {
 | 
						|
                            this.$refs.terminalWrapper.style.display = 'block';
 | 
						|
                            this.resizeTerminal();
 | 
						|
 | 
						|
                            // Start observing terminal wrapper for resize changes
 | 
						|
                            if (this.resizeObserver && this.$refs.terminalWrapper) {
 | 
						|
                                this.resizeObserver.observe(this.$refs.terminalWrapper);
 | 
						|
                            }
 | 
						|
                        } else {
 | 
						|
                            this.$refs.terminalWrapper.style.display = 'none';
 | 
						|
 | 
						|
                            // Stop observing when terminal is inactive
 | 
						|
                            if (this.resizeObserver) {
 | 
						|
                                this.resizeObserver.disconnect();
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    });
 | 
						|
                });
 | 
						|
 | 
						|
                ['livewire:navigated', 'beforeunload'].forEach((event) => {
 | 
						|
                    document.addEventListener(event, () => {
 | 
						|
                        this.cleanup();
 | 
						|
                    }, { once: true });
 | 
						|
                });
 | 
						|
 | 
						|
                window.onresize = () => {
 | 
						|
                    this.resizeTerminal()
 | 
						|
                };
 | 
						|
 | 
						|
                // Set up ResizeObserver for more reliable terminal resizing
 | 
						|
                if (window.ResizeObserver) {
 | 
						|
                    this.resizeObserver = new ResizeObserver(() => {
 | 
						|
                        // Debounce resize calls to avoid performance issues
 | 
						|
                        clearTimeout(this.resizeTimeout);
 | 
						|
                        this.resizeTimeout = setTimeout(() => {
 | 
						|
                            this.resizeTerminal();
 | 
						|
                        }, 50);
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            cleanup() {
 | 
						|
                this.checkIfProcessIsRunningAndKillIt();
 | 
						|
                this.clearAllTimers();
 | 
						|
                this.connectionState = 'disconnected';
 | 
						|
                if (this.socket) {
 | 
						|
                    this.socket.close(1000, 'Client cleanup');
 | 
						|
                }
 | 
						|
 | 
						|
                // Clean up resize observer
 | 
						|
                if (this.resizeObserver) {
 | 
						|
                    this.resizeObserver.disconnect();
 | 
						|
                    this.resizeObserver = null;
 | 
						|
                }
 | 
						|
 | 
						|
                // Clear resize timeout
 | 
						|
                if (this.resizeTimeout) {
 | 
						|
                    clearTimeout(this.resizeTimeout);
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            clearAllTimers() {
 | 
						|
                [this.keepAliveInterval, this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId, this.resizeTimeout]
 | 
						|
                    .forEach(timer => timer && clearInterval(timer));
 | 
						|
                this.keepAliveInterval = null;
 | 
						|
                this.reconnectInterval = null;
 | 
						|
                this.connectionTimeoutId = null;
 | 
						|
                this.pingTimeoutId = null;
 | 
						|
                this.resizeTimeout = null;
 | 
						|
            },
 | 
						|
 | 
						|
            resetTerminal() {
 | 
						|
                if (this.term) {
 | 
						|
                    this.$wire.dispatch('error', 'Terminal websocket connection lost.');
 | 
						|
                    this.term.reset();
 | 
						|
                    this.term.clear();
 | 
						|
                    this.pendingWrites = 0;
 | 
						|
                    this.paused = false;
 | 
						|
                    this.commandBuffer = '';
 | 
						|
 | 
						|
                    // Notify parent component that terminal disconnected
 | 
						|
                    this.$wire.dispatch('terminalDisconnected');
 | 
						|
 | 
						|
                    // Force a refresh
 | 
						|
                    this.$nextTick(() => {
 | 
						|
                        this.resizeTerminal();
 | 
						|
                        this.term.focus();
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            setupTerminal() {
 | 
						|
                const terminalElement = document.getElementById('terminal');
 | 
						|
                if (terminalElement) {
 | 
						|
                    this.term = new Terminal({
 | 
						|
                        cols: 80,
 | 
						|
                        rows: 30,
 | 
						|
                        fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
 | 
						|
                        cursorBlink: true,
 | 
						|
                        rendererType: 'canvas',
 | 
						|
                        convertEol: true,
 | 
						|
                        disableStdin: false
 | 
						|
                    });
 | 
						|
                    this.fitAddon = new FitAddon();
 | 
						|
                    this.term.loadAddon(this.fitAddon);
 | 
						|
                    this.$nextTick(() => {
 | 
						|
                        this.resizeTerminal();
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            initializeWebSocket() {
 | 
						|
                if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
 | 
						|
                    console.log('[Terminal] WebSocket already connecting/connected, skipping');
 | 
						|
                    return; // Already connecting or connected
 | 
						|
                }
 | 
						|
 | 
						|
                this.connectionState = 'connecting';
 | 
						|
                this.clearAllTimers();
 | 
						|
 | 
						|
                // Ensure terminal config is available
 | 
						|
                if (!window.terminalConfig) {
 | 
						|
                    console.warn('[Terminal] Terminal config not available, using defaults');
 | 
						|
                    window.terminalConfig = {};
 | 
						|
                }
 | 
						|
 | 
						|
                const predefined = window.terminalConfig
 | 
						|
                const connectionString = {
 | 
						|
                    protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
 | 
						|
                    host: window.location.hostname,
 | 
						|
                    port: ":6002",
 | 
						|
                    path: '/terminal/ws'
 | 
						|
                }
 | 
						|
 | 
						|
                if (!window.location.port) {
 | 
						|
                    connectionString.port = ''
 | 
						|
                }
 | 
						|
                if (predefined.host) {
 | 
						|
                    connectionString.host = predefined.host
 | 
						|
                }
 | 
						|
                if (predefined.port) {
 | 
						|
                    connectionString.port = `:${predefined.port}`
 | 
						|
                }
 | 
						|
                if (predefined.protocol) {
 | 
						|
                    connectionString.protocol = predefined.protocol
 | 
						|
                }
 | 
						|
 | 
						|
                const url = `${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
 | 
						|
                console.log(`[Terminal] Attempting connection to: ${url}`);
 | 
						|
 | 
						|
                try {
 | 
						|
                    this.socket = new WebSocket(url);
 | 
						|
 | 
						|
                    // Set connection timeout - increased for initial connection
 | 
						|
                    const timeoutMs = this.reconnectAttempts === 0 ? 15000 : this.connectionTimeout;
 | 
						|
                    this.connectionTimeoutId = setTimeout(() => {
 | 
						|
                        if (this.connectionState === 'connecting') {
 | 
						|
                            console.error(`[Terminal] Connection timeout after ${timeoutMs}ms`);
 | 
						|
                            this.socket.close();
 | 
						|
                            this.handleConnectionError('Connection timeout');
 | 
						|
                        }
 | 
						|
                    }, timeoutMs);
 | 
						|
 | 
						|
                    this.socket.onopen = this.handleSocketOpen.bind(this);
 | 
						|
                    this.socket.onmessage = this.handleSocketMessage.bind(this);
 | 
						|
                    this.socket.onerror = this.handleSocketError.bind(this);
 | 
						|
                    this.socket.onclose = this.handleSocketClose.bind(this);
 | 
						|
 | 
						|
                } catch (error) {
 | 
						|
                    console.error('[Terminal] Failed to create WebSocket:', error);
 | 
						|
                    this.handleConnectionError(`Failed to create WebSocket connection: ${error.message}`);
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            handleSocketOpen() {
 | 
						|
                console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.');
 | 
						|
                this.connectionState = 'connected';
 | 
						|
                this.reconnectAttempts = 0;
 | 
						|
                this.heartbeatMissed = 0;
 | 
						|
                this.lastPingTime = Date.now();
 | 
						|
 | 
						|
                // Clear connection timeout
 | 
						|
                if (this.connectionTimeoutId) {
 | 
						|
                    clearTimeout(this.connectionTimeoutId);
 | 
						|
                    this.connectionTimeoutId = null;
 | 
						|
                }
 | 
						|
 | 
						|
                // Start ping timeout monitoring
 | 
						|
                this.resetPingTimeout();
 | 
						|
 | 
						|
                // Notify that WebSocket is ready for auto-connection
 | 
						|
                this.dispatchEvent('terminal-websocket-ready');
 | 
						|
            },
 | 
						|
 | 
						|
            handleSocketError(error) {
 | 
						|
                console.error('[Terminal] WebSocket error:', error);
 | 
						|
                console.error('[Terminal] WebSocket state:', this.socket ? this.socket.readyState : 'No socket');
 | 
						|
                console.error('[Terminal] Connection attempt:', this.reconnectAttempts + 1);
 | 
						|
                this.handleConnectionError('WebSocket error occurred');
 | 
						|
            },
 | 
						|
 | 
						|
            handleSocketClose(event) {
 | 
						|
                console.warn(`[Terminal] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason || 'No reason provided'}`);
 | 
						|
                console.log('[Terminal] Was clean close:', event.code === 1000);
 | 
						|
                console.log('[Terminal] Connection attempt:', this.reconnectAttempts + 1);
 | 
						|
 | 
						|
                this.connectionState = 'disconnected';
 | 
						|
                this.clearAllTimers();
 | 
						|
 | 
						|
                // Only reset terminal and reconnect if it wasn't a clean close
 | 
						|
                if (event.code !== 1000) {
 | 
						|
                    // Don't show terminal reset message on first connection attempt
 | 
						|
                    if (this.reconnectAttempts > 0) {
 | 
						|
                        this.resetTerminal();
 | 
						|
                        this.message = '(connection closed)';
 | 
						|
                        this.terminalActive = false;
 | 
						|
                    }
 | 
						|
                    this.scheduleReconnect();
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            handleConnectionError(reason) {
 | 
						|
                console.error(`[Terminal] Connection error: ${reason} (attempt ${this.reconnectAttempts + 1})`);
 | 
						|
                this.connectionState = 'disconnected';
 | 
						|
 | 
						|
                // Only dispatch error to UI after a few failed attempts to avoid immediate error on page load
 | 
						|
                if (this.reconnectAttempts >= 2) {
 | 
						|
                    this.$wire.dispatch('error', `Terminal connection error: ${reason}`);
 | 
						|
                }
 | 
						|
 | 
						|
                this.scheduleReconnect();
 | 
						|
            },
 | 
						|
 | 
						|
            scheduleReconnect() {
 | 
						|
                if (this.reconnectAttempts >= this.maxReconnectAttempts) {
 | 
						|
                    console.error('[Terminal] Max reconnection attempts reached');
 | 
						|
                    this.message = '(connection failed - max retries exceeded)';
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                this.connectionState = 'reconnecting';
 | 
						|
 | 
						|
                // Exponential backoff with jitter
 | 
						|
                const delay = Math.min(
 | 
						|
                    this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
 | 
						|
                    this.maxReconnectDelay
 | 
						|
                );
 | 
						|
 | 
						|
                console.warn(`[Terminal] Scheduling reconnect attempt ${this.reconnectAttempts + 1} in ${delay}ms`);
 | 
						|
 | 
						|
                this.reconnectInterval = setTimeout(() => {
 | 
						|
                    this.reconnectAttempts++;
 | 
						|
                    this.initializeWebSocket();
 | 
						|
                }, delay);
 | 
						|
            },
 | 
						|
 | 
						|
            sendMessage(message) {
 | 
						|
                if (this.socket && this.socket.readyState === WebSocket.OPEN) {
 | 
						|
                    this.socket.send(JSON.stringify(message));
 | 
						|
                } else {
 | 
						|
                    console.warn('[Terminal] WebSocket not ready, message not sent:', message);
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            sendCommandWhenReady(message) {
 | 
						|
                if (this.isWebSocketReady()) {
 | 
						|
                    this.sendMessage(message);
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            handleSocketMessage(event) {
 | 
						|
                // Handle pong responses
 | 
						|
                if (event.data === 'pong') {
 | 
						|
                    this.heartbeatMissed = 0;
 | 
						|
                    this.lastPingTime = Date.now();
 | 
						|
                    this.resetPingTimeout();
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                if (event.data === 'pty-ready') {
 | 
						|
                    if (!this.term._initialized) {
 | 
						|
                        this.term.open(document.getElementById('terminal'));
 | 
						|
                        this.term._initialized = true;
 | 
						|
                    } else {
 | 
						|
                        this.term.reset();
 | 
						|
                    }
 | 
						|
                    this.terminalActive = true;
 | 
						|
                    this.term.focus();
 | 
						|
                    document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded-sm');
 | 
						|
 | 
						|
                    // Initial resize after terminal is ready
 | 
						|
                    this.resizeTerminal();
 | 
						|
 | 
						|
                    // Additional resize after a short delay to ensure proper sizing
 | 
						|
                    setTimeout(() => {
 | 
						|
                        this.resizeTerminal();
 | 
						|
                    }, 200);
 | 
						|
 | 
						|
                    // Notify parent component that terminal is connected
 | 
						|
                    this.$wire.dispatch('terminalConnected');
 | 
						|
                } else if (event.data === 'unprocessable') {
 | 
						|
                    if (this.term) this.term.reset();
 | 
						|
                    this.terminalActive = false;
 | 
						|
                    this.message = '(sorry, something went wrong, please try again)';
 | 
						|
 | 
						|
                    // Notify parent component that terminal connection failed
 | 
						|
                    this.$wire.dispatch('terminalDisconnected');
 | 
						|
                } else if (event.data === 'pty-exited') {
 | 
						|
                    this.terminalActive = false;
 | 
						|
                    this.term.reset();
 | 
						|
                    this.commandBuffer = '';
 | 
						|
 | 
						|
                    // Notify parent component that terminal disconnected
 | 
						|
                    this.$wire.dispatch('terminalDisconnected');
 | 
						|
                } else {
 | 
						|
                    try {
 | 
						|
                        this.pendingWrites++;
 | 
						|
                        this.term.write(event.data, (err) => {
 | 
						|
                            if (err) {
 | 
						|
                                console.error('[Terminal] Write error:', err);
 | 
						|
                            }
 | 
						|
                            this.flowControlCallback();
 | 
						|
                        });
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('[Terminal] Write operation failed:', error);
 | 
						|
                        this.pendingWrites = Math.max(0, this.pendingWrites - 1);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            flowControlCallback() {
 | 
						|
                this.pendingWrites = Math.max(0, this.pendingWrites - 1);
 | 
						|
 | 
						|
                if (this.pendingWrites > this.MAX_PENDING_WRITES && !this.paused) {
 | 
						|
                    this.paused = true;
 | 
						|
                    this.sendMessage({ pause: true });
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                if (this.pendingWrites <= Math.floor(this.MAX_PENDING_WRITES / 2) && this.paused) {
 | 
						|
                    this.paused = false;
 | 
						|
                    this.sendMessage({ resume: true });
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            setupTerminalEventListeners() {
 | 
						|
                if (!this.term) return;
 | 
						|
 | 
						|
                this.term.onData((data) => {
 | 
						|
                    this.sendMessage({ message: data });
 | 
						|
                    if (data === '\r') {
 | 
						|
                        this.commandBuffer = '';
 | 
						|
                    } else {
 | 
						|
                        this.commandBuffer += data;
 | 
						|
                    }
 | 
						|
                });
 | 
						|
 | 
						|
                // Copy and paste functionality
 | 
						|
                this.term.attachCustomKeyEventHandler((arg) => {
 | 
						|
                    if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
 | 
						|
                        return false;
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
 | 
						|
                        const selection = this.term.getSelection();
 | 
						|
                        if (selection) {
 | 
						|
                            navigator.clipboard.writeText(selection);
 | 
						|
                            return false;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    return true;
 | 
						|
                });
 | 
						|
            },
 | 
						|
 | 
						|
            keepAlive() {
 | 
						|
                if (this.socket && this.socket.readyState === WebSocket.OPEN) {
 | 
						|
                    this.sendMessage({ ping: true });
 | 
						|
                } else if (this.connectionState === 'disconnected') {
 | 
						|
                    // Attempt to reconnect if we're disconnected
 | 
						|
                    this.initializeWebSocket();
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            resetPingTimeout() {
 | 
						|
                if (this.pingTimeoutId) {
 | 
						|
                    clearTimeout(this.pingTimeoutId);
 | 
						|
                }
 | 
						|
 | 
						|
                this.pingTimeoutId = setTimeout(() => {
 | 
						|
                    this.heartbeatMissed++;
 | 
						|
                    console.warn(`[Terminal] Ping timeout - missed ${this.heartbeatMissed}/${this.maxHeartbeatMisses}`);
 | 
						|
 | 
						|
                    if (this.heartbeatMissed >= this.maxHeartbeatMisses) {
 | 
						|
                        console.error('[Terminal] Too many missed heartbeats, closing connection');
 | 
						|
                        this.socket.close(1001, 'Heartbeat timeout');
 | 
						|
                    }
 | 
						|
                }, this.pingTimeout);
 | 
						|
            },
 | 
						|
 | 
						|
            checkIfProcessIsRunningAndKillIt() {
 | 
						|
                this.sendMessage({ checkActive: 'force' });
 | 
						|
            },
 | 
						|
 | 
						|
            makeFullscreen() {
 | 
						|
                this.fullscreen = !this.fullscreen;
 | 
						|
                this.$nextTick(() => {
 | 
						|
                    // Force a layout reflow to ensure DOM changes are applied
 | 
						|
                    this.$refs.terminalWrapper.offsetHeight;
 | 
						|
 | 
						|
                    // Add a small delay to ensure CSS transitions complete
 | 
						|
                    setTimeout(() => {
 | 
						|
                        this.resizeTerminal();
 | 
						|
                    }, 100);
 | 
						|
                });
 | 
						|
            },
 | 
						|
 | 
						|
            resizeTerminal() {
 | 
						|
                if (!this.terminalActive || !this.term || !this.fitAddon) return;
 | 
						|
 | 
						|
                try {
 | 
						|
                    // Force a refresh of the fit addon dimensions
 | 
						|
                    this.fitAddon.fit();
 | 
						|
 | 
						|
                    // Get fresh dimensions after fit
 | 
						|
                    const wrapperHeight = this.$refs.terminalWrapper.clientHeight;
 | 
						|
                    const wrapperWidth = this.$refs.terminalWrapper.clientWidth;
 | 
						|
 | 
						|
                    // Account for terminal container padding (px-2 py-1 = 8px left/right, 4px top/bottom)
 | 
						|
                    const horizontalPadding = 16; // 8px * 2 (left + right)
 | 
						|
                    const verticalPadding = 8; // 4px * 2 (top + bottom)
 | 
						|
                    const height = wrapperHeight - verticalPadding;
 | 
						|
                    const width = wrapperWidth - horizontalPadding;
 | 
						|
 | 
						|
                    // Check if dimensions are valid
 | 
						|
                    if (height <= 0 || width <= 0) {
 | 
						|
                        console.warn('[Terminal] Invalid wrapper dimensions, retrying...', { height, width });
 | 
						|
                        setTimeout(() => this.resizeTerminal(), 100);
 | 
						|
                        return;
 | 
						|
                    }
 | 
						|
 | 
						|
                    const charSize = this.term._core._renderService._charSizeService;
 | 
						|
 | 
						|
                    if (!charSize.height || !charSize.width) {
 | 
						|
                        // Fallback values if char size not available yet
 | 
						|
                        console.warn('[Terminal] Character size not available, retrying...');
 | 
						|
                        setTimeout(() => this.resizeTerminal(), 100);
 | 
						|
                        return;
 | 
						|
                    }
 | 
						|
 | 
						|
                    // Calculate new dimensions with padding considerations
 | 
						|
                    const rows = Math.floor(height / charSize.height) - 1;
 | 
						|
                    const cols = Math.floor(width / charSize.width) - 1;
 | 
						|
 | 
						|
                    if (rows > 0 && cols > 0) {
 | 
						|
                        // Check if dimensions actually changed to avoid unnecessary resizes
 | 
						|
                        const currentCols = this.term.cols;
 | 
						|
                        const currentRows = this.term.rows;
 | 
						|
 | 
						|
                        if (cols !== currentCols || rows !== currentRows) {
 | 
						|
                            console.log(`[Terminal] Resizing terminal: ${currentCols}x${currentRows} -> ${cols}x${rows}`);
 | 
						|
                            this.term.resize(cols, rows);
 | 
						|
                            this.sendMessage({
 | 
						|
                                resize: { cols: cols, rows: rows }
 | 
						|
                            });
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        console.warn('[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize });
 | 
						|
                    }
 | 
						|
                } catch (error) {
 | 
						|
                    console.error('[Terminal] Resize error:', error);
 | 
						|
                }
 | 
						|
            },
 | 
						|
 | 
						|
            // Utility method to get connection status for debugging
 | 
						|
            getConnectionStatus() {
 | 
						|
                return {
 | 
						|
                    state: this.connectionState,
 | 
						|
                    readyState: this.socket ? this.socket.readyState : 'No socket',
 | 
						|
                    reconnectAttempts: this.reconnectAttempts,
 | 
						|
                    pendingWrites: this.pendingWrites,
 | 
						|
                    paused: this.paused,
 | 
						|
                    lastPingTime: this.lastPingTime,
 | 
						|
                    heartbeatMissed: this.heartbeatMissed
 | 
						|
                };
 | 
						|
            },
 | 
						|
 | 
						|
            // Helper method to dispatch custom events
 | 
						|
            dispatchEvent(eventName, detail = null) {
 | 
						|
                const event = new CustomEvent(eventName, {
 | 
						|
                    detail: detail,
 | 
						|
                    bubbles: true
 | 
						|
                });
 | 
						|
                this.$el.dispatchEvent(event);
 | 
						|
            },
 | 
						|
 | 
						|
            // Check if WebSocket is ready for commands
 | 
						|
            isWebSocketReady() {
 | 
						|
                return this.connectionState === 'connected' &&
 | 
						|
                    this.socket &&
 | 
						|
                    this.socket.readyState === WebSocket.OPEN;
 | 
						|
            }
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    window.Alpine.data('terminalData', terminalData);
 | 
						|
}
 |