diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 1c7c7af1d..d556baaf1 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -30,6 +30,9 @@ export function initializeTerminalComponent() { pingTimeoutId: null, heartbeatMissed: 0, maxHeartbeatMisses: 3, + // Resize handling + resizeObserver: null, + resizeTimeout: null, init() { this.setupTerminal(); @@ -55,8 +58,18 @@ export function initializeTerminalComponent() { 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(); + } } }); }); @@ -70,6 +83,17 @@ export function initializeTerminalComponent() { 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() { @@ -79,15 +103,27 @@ export function initializeTerminalComponent() { 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.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() { @@ -308,8 +344,15 @@ export function initializeTerminalComponent() { 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') { @@ -418,7 +461,13 @@ export function initializeTerminalComponent() { makeFullscreen() { this.fullscreen = !this.fullscreen; this.$nextTick(() => { - this.resizeTerminal(); + // 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); }); }, @@ -426,25 +475,53 @@ export function initializeTerminalComponent() { if (!this.terminalActive || !this.term || !this.fitAddon) return; try { + // Force a refresh of the fit addon dimensions this.fitAddon.fit(); - const height = this.$refs.terminalWrapper.clientHeight; - const width = this.$refs.terminalWrapper.clientWidth; - const charSize = this.term._core._renderService._charSizeService; - if (!charSize.height || !charSize.width) { - // Fallback values if char size not available yet + // 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) { - this.term.resize(cols, rows); - this.sendMessage({ - resize: { cols: cols, rows: rows } - }); + // 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); diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php index 3f930f4fd..47965522e 100644 --- a/resources/views/livewire/project/shared/terminal.blade.php +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -17,17 +17,20 @@ @else
-
- -