Fix blank RDP screen in multi-session view: scale display + fit on activate (#44)

The multi-session RemoteDesktop tabs (05e78f0) appended the Guacamole display
into an off-DOM container before connecting, and never called display.scale().
Frames arrived while the canvas was detached/zero-sized, so the desktop rendered
but painted to an invisible area — connected-but-blank.

Fix: track the display per session, fit/scale it to the visible panel when a
session becomes active, on the display's own onresize, and on window resize.
Verified the VM/xrdp/guacd side streams frames fine (13-36 img frames in direct
guacd tests); this was purely the client-side mount/scale regression.

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:12:00 -04:00 committed by GitHub
parent 6c1f167f15
commit a7fbbabeb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -17,6 +17,19 @@ interface Session {
interface SessionHandle {
client: any
container: HTMLDivElement
display: any
}
// Scale the Guacamole display to fit its visible container. The display canvas
// renders at the remote resolution; without this it can paint to a 0-sized /
// unscaled area (blank screen) when the element wasn't in the live DOM at connect.
function fitDisplay(handle: SessionHandle | null | undefined, host: HTMLElement | null) {
if (!handle || !host) return
const w = handle.display.getWidth()
const h = handle.display.getHeight()
if (!w || !h) return
const scale = Math.min(host.clientWidth / w, host.clientHeight / h, 1) || 1
handle.display.scale(scale)
}
export default function RemoteDesktop() {
@ -25,6 +38,8 @@ export default function RemoteDesktop() {
const [activeSessionId, setActiveSessionId] = useState<string | null>(null)
const displayHostRef = useRef<HTMLDivElement>(null)
const handlesRef = useRef<Map<string, SessionHandle>>(new Map())
const activeSessionIdRef = useRef<string | null>(null)
activeSessionIdRef.current = activeSessionId
useEffect(() => {
api.listIntegrations().then(({ integrations }) => {
@ -57,7 +72,8 @@ export default function RemoteDesktop() {
// so the tunnel URL itself must not already contain one.
const tunnel = new Guacamole.WebSocketTunnel(`${proto}://${window.location.host}/api/guacamole`)
const client = new Guacamole.Client(tunnel)
handlesRef.current.set(sessionId, { client, container })
const display = client.getDisplay()
handlesRef.current.set(sessionId, { client, container, display })
client.onerror = (err: { message?: string }) => {
patchSession(sessionId, { status: 'error', errorMessage: err?.message ?? 'Connection failed' })
@ -65,8 +81,13 @@ export default function RemoteDesktop() {
client.onstatechange = (state: number) => {
if (state === 3) patchSession(sessionId, { status: 'connected' })
}
// Re-fit whenever the remote desktop reports a new size, but only while this
// session is the visible one.
display.onresize = () => {
if (activeSessionIdRef.current === sessionId) fitDisplay(handlesRef.current.get(sessionId), displayHostRef.current)
}
container.appendChild(client.getDisplay().getElement())
container.appendChild(display.getElement())
client.connect(`token=${encodeURIComponent(token ?? '')}&integrationId=${host.id}`)
}
@ -91,9 +112,24 @@ export default function RemoteDesktop() {
if (!host) return
host.innerHTML = ''
const active = activeSessionId ? handlesRef.current.get(activeSessionId) : null
if (active) host.appendChild(active.container)
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])
// Keep the active session scaled to the panel as the window/panel resizes.
useEffect(() => {
const onResize = () => {
const active = activeSessionIdRef.current ? handlesRef.current.get(activeSessionIdRef.current) : null
fitDisplay(active, displayHostRef.current)
}
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
const activeSession = sessions.find((s) => s.sessionId === activeSessionId)
return (