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 <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
This commit is contained in:
Samuel James 2026-06-22 15:31:43 -04:00 committed by GitHub
parent a7fbbabeb2
commit 58a46553d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -64,7 +64,13 @@ export default function RemoteDesktop() {
setActiveSessionId(sessionId) setActiveSessionId(sessionId)
const container = document.createElement('div') 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 token = getToken()
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' 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 // Show only the active session's display; keep all others mounted but hidden so
// background sessions stay connected while their tab isn't focused. // 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(() => { useEffect(() => {
const host = displayHostRef.current handlesRef.current.forEach((handle, id) => {
if (!host) return const visible = id === activeSessionId
host.innerHTML = '' handle.container.style.display = visible ? 'block' : 'none'
const active = activeSessionId ? handlesRef.current.get(activeSessionId) : null if (visible) fitDisplay(handle, displayHostRef.current)
if (active) { })
host.appendChild(active.container) }, [activeSessionId, sessions])
// 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])
// Keep the active session scaled to the panel as the window/panel resizes. // Keep the active session scaled to the panel as the window/panel resizes.
useEffect(() => { useEffect(() => {
@ -216,7 +218,7 @@ export default function RemoteDesktop() {
</div> </div>
)} )}
<div ref={displayHostRef} className="flex-1 overflow-auto" /> <div ref={displayHostRef} className="relative flex-1 overflow-hidden" />
</div> </div>
</div> </div>
) )