import { useEffect, useRef, useState } from 'react' import Guacamole from 'guacamole-common-js' import { api, getToken, type Integration } from '../lib/api' const TEXT_SECONDARY = '#7A7D85' type SessionStatus = 'connecting' | 'connected' | 'error' interface Session { sessionId: string hostId: number name: string status: SessionStatus errorMessage: string } interface SessionHandle { client: any container: HTMLDivElement } export default function RemoteDesktop() { const [hosts, setHosts] = useState([]) const [sessions, setSessions] = useState([]) const [activeSessionId, setActiveSessionId] = useState(null) const displayHostRef = useRef(null) const handlesRef = useRef>(new Map()) useEffect(() => { api.listIntegrations().then(({ integrations }) => { setHosts(integrations.filter((i) => i.type === 'remote_desktop')) }) }, []) useEffect(() => { return () => { handlesRef.current.forEach(({ client }) => client.disconnect()) } }, []) function patchSession(sessionId: string, patch: Partial) { setSessions((prev) => prev.map((s) => (s.sessionId === sessionId ? { ...s, ...patch } : s))) } function openSession(host: Integration) { const sessionId = crypto.randomUUID() const session: Session = { sessionId, hostId: host.id, name: host.name, status: 'connecting', errorMessage: '' } setSessions((prev) => [...prev, session]) setActiveSessionId(sessionId) const container = document.createElement('div') container.className = 'h-full w-full overflow-auto' const token = getToken() const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' // Guacamole.WebSocketTunnel appends its own "?" query string on connect(), // 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 }) client.onerror = (err: { message?: string }) => { patchSession(sessionId, { status: 'error', errorMessage: err?.message ?? 'Connection failed' }) } client.onstatechange = (state: number) => { if (state === 3) patchSession(sessionId, { status: 'connected' }) } container.appendChild(client.getDisplay().getElement()) client.connect(`token=${encodeURIComponent(token ?? '')}&integrationId=${host.id}`) } function closeSession(sessionId: string) { const handle = handlesRef.current.get(sessionId) handle?.client.disconnect() handle?.container.remove() handlesRef.current.delete(sessionId) setSessions((prev) => { const next = prev.filter((s) => s.sessionId !== sessionId) if (activeSessionId === sessionId) { setActiveSessionId(next.length > 0 ? next[next.length - 1].sessionId : null) } return next }) } // 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. useEffect(() => { const host = displayHostRef.current if (!host) return host.innerHTML = '' const active = activeSessionId ? handlesRef.current.get(activeSessionId) : null if (active) host.appendChild(active.container) }, [activeSessionId]) const activeSession = sessions.find((s) => s.sessionId === activeSessionId) return (

Remote Desktops

{hosts.length === 0 && (

No remote desktop integrations configured. Add one in Settings → Integrations.

)} {hosts.map((h) => ( ))}
{sessions.length === 0 && (

Select a remote desktop to connect

)} {sessions.map((s) => (
setActiveSessionId(s.sessionId)} className="flex shrink-0 cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-sm" style={{ background: activeSessionId === s.sessionId ? 'rgba(200,164,52,0.15)' : 'transparent', color: activeSessionId === s.sessionId ? '#C8A434' : '#E8E6E0', }} > {s.name}
))}
{activeSession && (

{activeSession.name}

{activeSession.status === 'connecting' && 'Connecting…'} {activeSession.status === 'connected' && 'Connected'} {activeSession.status === 'error' && `Error: ${activeSession.errorMessage}`}

)}
) }