From 58a46553d92f6a59b8238898234048530eba353a Mon Sep 17 00:00:00 2001 From: Samuel James <143277412+SamuelSJames@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:31:43 -0400 Subject: [PATCH] Fix RDP session drop/flicker: stop re-parenting Guacamole display (#45) Symptom: desktop flickers in for a moment then goes blank while the tab still says "connected"; guacd logs "User is not responding" and the client reconnects in a loop. Root cause: the multi-session view moved each session's display element between DOM nodes on tab switch (host.innerHTML='' + appendChild). Detaching a guacamole-common-js display from the DOM stalls its sync loop, so the client stops echoing guacd's sync instructions and guacd drops it as unresponsive. Proven out-of-band: a raw guacd client that echoes sync held a connection for 30s/116 syncs with no drop, while the browser dropped within seconds. Fix: mount each session's container into the display host ONCE and never move it; toggle visibility (display:none) to switch tabs so every session's display stays in the DOM and its sync loop keeps running. Containers are absolutely positioned in a relative host; close still removes the container. Co-authored-by: Samuel James Co-authored-by: Kiro --- src/pages/RemoteDesktop.tsx | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pages/RemoteDesktop.tsx b/src/pages/RemoteDesktop.tsx index 6a621a0..633d46c 100644 --- a/src/pages/RemoteDesktop.tsx +++ b/src/pages/RemoteDesktop.tsx @@ -64,7 +64,13 @@ export default function RemoteDesktop() { setActiveSessionId(sessionId) const container = document.createElement('div') - container.className = 'h-full w-full overflow-auto' + container.className = 'absolute inset-0 overflow-auto' + container.style.display = 'none' + // Mount the container into the display host ONCE and never move it again — + // re-parenting the Guacamole display between DOM nodes breaks its sync loop, + // which makes guacd drop the user as "not responding". We toggle visibility + // instead (see the activation effect below). + displayHostRef.current?.appendChild(container) const token = getToken() const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' @@ -105,20 +111,16 @@ export default function RemoteDesktop() { }) } - // Show only the active session's display element; keep the rest mounted off-DOM so - // background sessions stay connected while their tab isn't focused. + // Show only the active session's display; keep all others mounted but hidden so + // their sessions stay connected AND their sync loop keeps running (do NOT move + // them out of the DOM — that breaks Guacamole's sync and guacd drops them). useEffect(() => { - const host = displayHostRef.current - if (!host) return - host.innerHTML = '' - const active = activeSessionId ? handlesRef.current.get(activeSessionId) : null - if (active) { - host.appendChild(active.container) - // The display may have received its size while off-DOM (zero-sized), which - // leaves the canvas unscaled / invisible — re-fit now that it's visible. - fitDisplay(active, host) - } - }, [activeSessionId]) + handlesRef.current.forEach((handle, id) => { + const visible = id === activeSessionId + handle.container.style.display = visible ? 'block' : 'none' + if (visible) fitDisplay(handle, displayHostRef.current) + }) + }, [activeSessionId, sessions]) // Keep the active session scaled to the panel as the window/panel resizes. useEffect(() => { @@ -216,7 +218,7 @@ export default function RemoteDesktop() { )} -
+
)