From dca6d9f7aab40fb9e6ea24dcc3a85bea02cc33a6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:48:03 +0100 Subject: [PATCH] fix: Prevent terminal disconnects when browser tab loses focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add visibility API handling to pause heartbeat monitoring when the browser tab is hidden, preventing false disconnection timeouts. When the tab becomes visible again, verify the connection is still alive or attempt reconnection. Also remove the ApplicationStatusChanged event listener that was triggering terminal reloads whenever any application status changed across the team. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Project/Shared/Terminal.php | 14 -------- resources/js/terminal.js | 42 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index de2deeed4..3c2abc84c 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -11,20 +11,6 @@ class Terminal extends Component { public bool $hasShell = true; - public function getListeners() - { - $teamId = auth()->user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal', - ]; - } - - public function closeTerminal() - { - $this->dispatch('reloadWindow'); - } - private function checkShellAvailability(Server $server, string $container): bool { $escapedContainer = escapeshellarg($container); diff --git a/resources/js/terminal.js b/resources/js/terminal.js index b49aad9cf..6707bec98 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -33,6 +33,9 @@ export function initializeTerminalComponent() { // Resize handling resizeObserver: null, resizeTimeout: null, + // Visibility handling - prevent disconnects when tab loses focus + isDocumentVisible: true, + wasConnectedBeforeHidden: false, init() { this.setupTerminal(); @@ -92,6 +95,11 @@ export function initializeTerminalComponent() { }, { once: true }); }); + // Handle visibility changes to prevent disconnects when tab loses focus + document.addEventListener('visibilitychange', () => { + this.handleVisibilityChange(); + }); + window.onresize = () => { this.resizeTerminal() }; @@ -451,6 +459,11 @@ export function initializeTerminalComponent() { }, keepAlive() { + // Skip keepalive when document is hidden to prevent unnecessary disconnects + if (!this.isDocumentVisible) { + return; + } + if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.sendMessage({ ping: true }); } else if (this.connectionState === 'disconnected') { @@ -459,6 +472,35 @@ export function initializeTerminalComponent() { } }, + handleVisibilityChange() { + const wasVisible = this.isDocumentVisible; + this.isDocumentVisible = !document.hidden; + + if (!this.isDocumentVisible) { + // Tab is now hidden - pause heartbeat monitoring to prevent false disconnects + this.wasConnectedBeforeHidden = this.connectionState === 'connected'; + if (this.pingTimeoutId) { + clearTimeout(this.pingTimeoutId); + this.pingTimeoutId = null; + } + console.log('[Terminal] Tab hidden, pausing heartbeat monitoring'); + } else if (wasVisible === false) { + // Tab is now visible again + console.log('[Terminal] Tab visible, resuming connection management'); + + if (this.wasConnectedBeforeHidden && this.socket && this.socket.readyState === WebSocket.OPEN) { + // Send immediate ping to verify connection is still alive + this.heartbeatMissed = 0; + this.sendMessage({ ping: true }); + this.resetPingTimeout(); + } else if (this.wasConnectedBeforeHidden && this.connectionState !== 'connected') { + // Was connected before but now disconnected - attempt reconnection + this.reconnectAttempts = 0; + this.initializeWebSocket(); + } + } + }, + resetPingTimeout() { if (this.pingTimeoutId) { clearTimeout(this.pingTimeoutId);