fix: Prevent terminal disconnects when browser tab loses focus

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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-08 20:48:03 +01:00
parent e1d5da3622
commit dca6d9f7aa
2 changed files with 42 additions and 14 deletions

View File

@ -11,20 +11,6 @@ class Terminal extends Component
{ {
public bool $hasShell = true; 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 private function checkShellAvailability(Server $server, string $container): bool
{ {
$escapedContainer = escapeshellarg($container); $escapedContainer = escapeshellarg($container);

View File

@ -33,6 +33,9 @@ export function initializeTerminalComponent() {
// Resize handling // Resize handling
resizeObserver: null, resizeObserver: null,
resizeTimeout: null, resizeTimeout: null,
// Visibility handling - prevent disconnects when tab loses focus
isDocumentVisible: true,
wasConnectedBeforeHidden: false,
init() { init() {
this.setupTerminal(); this.setupTerminal();
@ -92,6 +95,11 @@ export function initializeTerminalComponent() {
}, { once: true }); }, { once: true });
}); });
// Handle visibility changes to prevent disconnects when tab loses focus
document.addEventListener('visibilitychange', () => {
this.handleVisibilityChange();
});
window.onresize = () => { window.onresize = () => {
this.resizeTerminal() this.resizeTerminal()
}; };
@ -451,6 +459,11 @@ export function initializeTerminalComponent() {
}, },
keepAlive() { keepAlive() {
// Skip keepalive when document is hidden to prevent unnecessary disconnects
if (!this.isDocumentVisible) {
return;
}
if (this.socket && this.socket.readyState === WebSocket.OPEN) { if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.sendMessage({ ping: true }); this.sendMessage({ ping: true });
} else if (this.connectionState === 'disconnected') { } 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() { resetPingTimeout() {
if (this.pingTimeoutId) { if (this.pingTimeoutId) {
clearTimeout(this.pingTimeoutId); clearTimeout(this.pingTimeoutId);