import { useCallback, useEffect, useRef, useState, type ClipboardEvent, type KeyboardEvent, type MouseEvent, } from 'react' import { useTranslation } from 'react-i18next' import { Cast, ClipboardPaste, FolderInput, FolderOpen, Maximize2, Minimize2, Monitor, RefreshCw } from 'lucide-react' import { useTabStore } from '../../stores/tabStore' import { useSessionStore } from '../../stores/sessionStore' import { useSettingsStore } from '../../stores/settingsStore' import { useDesktopFullscreenStore } from '../../stores/desktopFullscreenStore' import StatusDot from '../common/StatusDot' import type { DesktopFrameEvent, DesktopPointerButton, DesktopProtocol, RdpConfig, VncConfig, } from '../../../shared/types/desktop.types' import { onRdpClipboard, onRdpFileDownload, onRdpFrame, onRdpStatus, rdpClipboardSet, rdpConnect, rdpDisconnect, rdpInputKey, rdpInputPointer, rdpResize, rdpUploadFile, } from '../../platform/rdp' import { onVncClipboard, onVncFrame, onVncStatus, vncClipboardSet, vncConnect, vncDisconnect, vncInputKey, vncInputPointer, } from '../../platform/vnc' import { readClipboardText, writeClipboardText } from '../../platform/clipboard' import { sessionToRdpConfig, sessionToVncConfig } from '../../utils/sessionProtocol' import { clampDesktopResolution, desktopCanvasDisplaySize, isUsableDesktopPane, resolutionFromPane, type DesktopResolution, } from '../../utils/desktopResolution' import { reconnectDelayMs } from '../../utils/disconnectReason' import { subscribeTabReconnect } from '../../utils/tabReconnect' interface DesktopSessionViewProps { tabId: string isActive: boolean } function decodeBase64Bytes(base64: string): Uint8Array { const binary = atob(base64) const buffer = new ArrayBuffer(binary.length) const bytes = new Uint8Array(buffer) const len = binary.length let i = 0 for (; i + 4 <= len; i += 4) { bytes[i] = binary.charCodeAt(i) bytes[i + 1] = binary.charCodeAt(i + 1) bytes[i + 2] = binary.charCodeAt(i + 2) bytes[i + 3] = binary.charCodeAt(i + 3) } for (; i < len; i++) bytes[i] = binary.charCodeAt(i) return bytes } function decodeRgbaBase64(base64: string): Uint8ClampedArray { return new Uint8ClampedArray(decodeBase64Bytes(base64).buffer) } export default function DesktopSessionView({ tabId, isActive }: DesktopSessionViewProps) { const { t } = useTranslation('desktop') const canvasRef = useRef(null) const paneRef = useRef(null) const canvasCtxRef = useRef(null) const nativeSessionIdRef = useRef(null) const isActiveRef = useRef(isActive) const fileInputRef = useRef(null) const buttonsRef = useRef({ left: false, middle: false, right: false }) const pointerRafRef = useRef(null) const paintRafRef = useRef(null) const hideBarTimerRef = useRef(null) const pendingFramesRef = useRef([]) const lastRemoteResizeRef = useRef<{ width: number; height: number } | null>(null) const remoteResizeTimerRef = useRef(null) const connectedRef = useRef(false) const pendingPointerRef = useRef<{ x: number y: number button?: DesktopPointerButton pressed?: boolean } | null>(null) const tab = useTabStore((state) => state.tabs.find((item) => item.id === tabId)) const sessions = useSessionStore((state) => state.sessions) const updateTabStatus = useTabStore((state) => state.updateTabStatus) const fullscreenActive = useDesktopFullscreenStore((state) => state.active && state.tabId === tabId) const enterFullscreen = useDesktopFullscreenStore((state) => state.enter) const exitFullscreen = useDesktopFullscreenStore((state) => state.exit) const toggleFullscreen = useDesktopFullscreenStore((state) => state.toggle) const [statusMessage, setStatusMessage] = useState(() => t('status.connecting')) const [error, setError] = useState(null) const [fbSize, setFbSize] = useState({ width: 1920, height: 1080 }) const [resolvedConfig, setResolvedConfig] = useState(null) const [chromeVisible, setChromeVisible] = useState(true) const [reconnectGeneration, setReconnectGeneration] = useState(0) const reconnectAttemptRef = useRef(0) const reconnectTimerRef = useRef(null) const intentionalDisconnectRef = useRef(false) const hadConnectedRef = useRef(false) const pasteLockRef = useRef(0) const protocol: DesktopProtocol = tab?.type === 'vnc' ? 'vnc' : 'rdp' isActiveRef.current = isActive useEffect(() => { let cancelled = false const resolve = async () => { if (protocol === 'rdp' && tab?.rdpConfig) { if (!cancelled) setResolvedConfig(tab.rdpConfig) return } if (protocol === 'vnc' && tab?.vncConfig) { if (!cancelled) setResolvedConfig(tab.vncConfig) return } if (tab?.sessionId) { const local = sessions.find((item) => item.id === tab.sessionId) if (local) { if (!cancelled) { setResolvedConfig(protocol === 'rdp' ? sessionToRdpConfig(local) : sessionToVncConfig(local)) } return } const response = await window.opsxApi?.sessionGetById?.(tab.sessionId) if (!cancelled && response?.success && response.session) { setResolvedConfig( protocol === 'rdp' ? sessionToRdpConfig(response.session) : sessionToVncConfig(response.session) ) } } } void resolve() return () => { cancelled = true } }, [protocol, sessions, tab?.rdpConfig, tab?.sessionId, tab?.vncConfig]) const clearDesktopReconnectTimer = useCallback(() => { if (reconnectTimerRef.current != null) { window.clearTimeout(reconnectTimerRef.current) reconnectTimerRef.current = null } }, []) const scheduleDesktopReconnect = useCallback(() => { const { settings } = useSettingsStore.getState() if (!settings.autoReconnect) return if (intentionalDisconnectRef.current) return if (!hadConnectedRef.current) return const maxAttempts = settings.maxReconnectAttempts ?? 5 const baseDelay = settings.reconnectBaseDelayMs ?? 1500 if (reconnectAttemptRef.current >= maxAttempts) { setError(t('status.reconnectLimit', { max: maxAttempts })) updateTabStatus(tabId, 'error') return } reconnectAttemptRef.current += 1 const attempt = reconnectAttemptRef.current const delay = reconnectDelayMs(attempt, baseDelay) updateTabStatus(tabId, 'connecting') setStatusMessage(t('status.reconnecting', { attempt, max: maxAttempts })) clearDesktopReconnectTimer() reconnectTimerRef.current = window.setTimeout(() => { reconnectTimerRef.current = null setReconnectGeneration((value) => value + 1) }, delay) }, [clearDesktopReconnectTimer, t, tabId, updateTabStatus]) const handleDesktopReconnect = useCallback(() => { clearDesktopReconnectTimer() reconnectAttemptRef.current = 0 intentionalDisconnectRef.current = false setError(null) setReconnectGeneration((value) => value + 1) }, [clearDesktopReconnectTimer]) useEffect(() => { return () => clearDesktopReconnectTimer() }, [clearDesktopReconnectTimer]) useEffect(() => subscribeTabReconnect(tabId, handleDesktopReconnect), [tabId, handleDesktopReconnect]) useEffect(() => { if (!resolvedConfig) return let disposed = false let unlistenStatus: (() => void) | undefined let unlistenFrame: (() => void) | undefined let unlistenClipboard: (() => void) | undefined let unlistenFile: (() => void) | undefined const ensureCanvas = (fbWidth: number, fbHeight: number) => { const canvas = canvasRef.current if (!canvas) return null if (canvas.width !== fbWidth || canvas.height !== fbHeight) { canvas.width = fbWidth canvas.height = fbHeight canvasCtxRef.current = null setFbSize({ width: fbWidth, height: fbHeight }) } if (!canvasCtxRef.current) { canvasCtxRef.current = canvas.getContext('2d', { alpha: false }) } return canvasCtxRef.current } const paintOne = async (payload: DesktopFrameEvent) => { const ctx = ensureCanvas(payload.fbWidth, payload.fbHeight) if (!ctx) return if (payload.encoding === 'jpeg') { const bytes = decodeBase64Bytes(payload.dataBase64) const blob = new Blob([bytes], { type: 'image/jpeg' }) const bitmap = await createImageBitmap(blob) if (disposed) { bitmap.close() return } ctx.drawImage(bitmap, payload.x, payload.y) bitmap.close() return } const rgba = decodeRgbaBase64(payload.dataBase64) const image = new ImageData(rgba, payload.width, payload.height) ctx.putImageData(image, payload.x, payload.y) } const flushPendingFrames = () => { paintRafRef.current = null const batch = pendingFramesRef.current pendingFramesRef.current = [] if (!batch.length || !isActiveRef.current) return void (async () => { for (const frame of batch) { if (disposed) return try { await paintOne(frame) } catch { // Ignore a single bad frame; keep painting the rest. } } })() } const queueFrame = (payload: DesktopFrameEvent) => { if (!isActiveRef.current) return pendingFramesRef.current.push(payload) if (paintRafRef.current == null) { paintRafRef.current = window.requestAnimationFrame(flushPendingFrames) } } updateTabStatus(tabId, 'connecting') setStatusMessage(t('status.connecting')) setError(null) connectedRef.current = false lastRemoteResizeRef.current = null const waitForPaneResolution = async (fallback: DesktopResolution): Promise => { for (let i = 0; i < 24; i++) { const pane = paneRef.current const width = pane?.clientWidth ?? 0 const height = pane?.clientHeight ?? 0 if (pane && isUsableDesktopPane(width, height)) { return resolutionFromPane(width, height, fallback) } await new Promise((resolve) => { window.requestAnimationFrame(() => resolve()) }) } const pane = paneRef.current return resolutionFromPane(pane?.clientWidth ?? 0, pane?.clientHeight ?? 0, fallback) } void (async () => { try { if (protocol === 'rdp') { unlistenStatus = await onRdpStatus((payload) => { if (nativeSessionIdRef.current && payload.sessionId !== nativeSessionIdRef.current) return if (!nativeSessionIdRef.current) { if (payload.status === 'disconnected' || payload.status === 'error') return nativeSessionIdRef.current = payload.sessionId } if (payload.width && payload.height) { setFbSize({ width: payload.width, height: payload.height }) lastRemoteResizeRef.current = { width: payload.width, height: payload.height } } if (payload.status === 'connected') { connectedRef.current = true hadConnectedRef.current = true reconnectAttemptRef.current = 0 updateTabStatus(tabId, 'connected') setStatusMessage(payload.message || t('status.connected')) } else if (payload.status === 'error') { connectedRef.current = false updateTabStatus(tabId, 'error') setError(payload.message || t('status.connectFailed')) if (!disposed) scheduleDesktopReconnect() } else if (payload.status === 'disconnected') { connectedRef.current = false updateTabStatus(tabId, 'disconnected') setStatusMessage(t('status.disconnected')) if (!disposed) scheduleDesktopReconnect() } else if (payload.message) { setStatusMessage(payload.message) } }) unlistenFrame = await onRdpFrame((payload) => { if (payload.sessionId !== nativeSessionIdRef.current) return queueFrame(payload) }) unlistenClipboard = await onRdpClipboard((payload) => { if (payload.sessionId !== nativeSessionIdRef.current) return if (payload.text) { void writeClipboardText(payload.text).catch(() => undefined) setStatusMessage(t('status.clipboardSynced')) } }) unlistenFile = await onRdpFileDownload((payload) => { if (payload.sessionId !== nativeSessionIdRef.current) return setStatusMessage(t('status.savedToDownloads', { name: payload.name })) }) const rdpConfig = resolvedConfig as RdpConfig const paneSize = await waitForPaneResolution(clampDesktopResolution(rdpConfig.width, rdpConfig.height)) if (disposed) return lastRemoteResizeRef.current = paneSize const sessionId = await rdpConnect({ ...rdpConfig, width: paneSize.width, height: paneSize.height, }) if (disposed) { await rdpDisconnect(sessionId) return } nativeSessionIdRef.current = sessionId } else { unlistenStatus = await onVncStatus((payload) => { if (nativeSessionIdRef.current && payload.sessionId !== nativeSessionIdRef.current) return if (!nativeSessionIdRef.current) { if (payload.status === 'disconnected' || payload.status === 'error') return nativeSessionIdRef.current = payload.sessionId } if (payload.status === 'connected') { connectedRef.current = true hadConnectedRef.current = true reconnectAttemptRef.current = 0 updateTabStatus(tabId, 'connected') setStatusMessage(payload.message || t('status.connected')) } else if (payload.status === 'error') { connectedRef.current = false updateTabStatus(tabId, 'error') setError(payload.message || t('status.connectFailed')) if (!disposed) scheduleDesktopReconnect() } else if (payload.status === 'disconnected') { connectedRef.current = false updateTabStatus(tabId, 'disconnected') setStatusMessage(t('status.disconnected')) if (!disposed) scheduleDesktopReconnect() } else if (payload.message) { setStatusMessage(payload.message) } }) unlistenFrame = await onVncFrame((payload) => { if (payload.sessionId !== nativeSessionIdRef.current) return queueFrame(payload) }) unlistenClipboard = await onVncClipboard((payload) => { if (payload.sessionId !== nativeSessionIdRef.current) return if (payload.text) { void writeClipboardText(payload.text).catch(() => undefined) setStatusMessage(t('status.clipboardSynced')) } }) const sessionId = await vncConnect(resolvedConfig as VncConfig) if (disposed) { await vncDisconnect(sessionId) return } nativeSessionIdRef.current = sessionId } } catch (reason) { const message = reason instanceof Error ? reason.message : String(reason) setError(message) updateTabStatus(tabId, 'error') } })() return () => { disposed = true connectedRef.current = false if (remoteResizeTimerRef.current != null) { window.clearTimeout(remoteResizeTimerRef.current) remoteResizeTimerRef.current = null } if (paintRafRef.current != null) { window.cancelAnimationFrame(paintRafRef.current) paintRafRef.current = null } pendingFramesRef.current = [] unlistenStatus?.() unlistenFrame?.() unlistenClipboard?.() unlistenFile?.() const sessionId = nativeSessionIdRef.current nativeSessionIdRef.current = null if (sessionId) { if (protocol === 'rdp') void rdpDisconnect(sessionId) else void vncDisconnect(sessionId) } } }, [protocol, resolvedConfig, tabId, updateTabStatus, t, reconnectGeneration, scheduleDesktopReconnect]) useEffect(() => { if (!isActive) return canvasRef.current?.focus() }, [isActive]) // Fit canvas to pane; for RDP also request remote dynamic resolution (mstsc / FreeRDP style). useEffect(() => { const pane = paneRef.current const canvas = canvasRef.current if (!pane || !canvas || fbSize.width <= 0 || fbSize.height <= 0) return const updateFit = () => { const display = desktopCanvasDisplaySize( protocol, pane.clientWidth, pane.clientHeight, fbSize.width, fbSize.height ) canvas.style.width = `${display.width}px` canvas.style.height = `${display.height}px` } const requestRemoteResize = () => { if (protocol !== 'rdp' || !connectedRef.current || !isActive) return const sessionId = nativeSessionIdRef.current if (!sessionId) return const next = clampDesktopResolution(pane.clientWidth, pane.clientHeight) const last = lastRemoteResizeRef.current if (last && last.width === next.width && last.height === next.height) return // Skip tiny panes (drawer/animation) that would thrash the session. if (next.width < 640 || next.height < 480) return lastRemoteResizeRef.current = next void rdpResize(sessionId, next.width, next.height).catch(() => { // Allow retry on next size change if Display Control was not ready. lastRemoteResizeRef.current = last }) } const onPaneResize = () => { updateFit() if (remoteResizeTimerRef.current != null) { window.clearTimeout(remoteResizeTimerRef.current) } remoteResizeTimerRef.current = window.setTimeout(requestRemoteResize, 280) } updateFit() // After fullscreen / maximize layout settles, request matching remote size once. remoteResizeTimerRef.current = window.setTimeout(requestRemoteResize, fullscreenActive ? 120 : 280) const observer = new ResizeObserver(onPaneResize) observer.observe(pane) return () => { observer.disconnect() if (remoteResizeTimerRef.current != null) { window.clearTimeout(remoteResizeTimerRef.current) remoteResizeTimerRef.current = null } } }, [fbSize.height, fbSize.width, fullscreenActive, isActive, protocol]) // Leave OS fullscreen when this tab is hidden or unmounted. useEffect(() => { if (!isActive && fullscreenActive) { void exitFullscreen() } }, [exitFullscreen, fullscreenActive, isActive]) useEffect(() => { return () => { if (hideBarTimerRef.current != null) { window.clearTimeout(hideBarTimerRef.current) } // Only exit when this session view is truly torn down (tab closed), not on HMR-only paths. const state = useDesktopFullscreenStore.getState() if (state.active && state.tabId === tabId) { const stillOpen = useTabStore.getState().tabs.some((item) => item.id === tabId) if (!stillOpen) void state.exit() } } }, [tabId]) useEffect(() => { if (!fullscreenActive) { setChromeVisible(true) if (hideBarTimerRef.current != null) { window.clearTimeout(hideBarTimerRef.current) hideBarTimerRef.current = null } return } // If the user leaves OS fullscreen via system UI, drop session chrome-hide. const timer = window.setInterval(() => { void (async () => { try { const state = useDesktopFullscreenStore.getState() if (!state.active || state.tabId !== tabId) return if (Date.now() - state.enteredAt < 1600) return const osFs = await window.opsxApi?.isFullscreen?.() if (osFs === false) { useDesktopFullscreenStore.setState({ active: false, tabId: null, enteredAt: 0 }) } } catch { // ignore } })() }, 1000) return () => window.clearInterval(timer) }, [fullscreenActive, tabId]) const scheduleHideChrome = useCallback(() => { if (!fullscreenActive) return if (hideBarTimerRef.current != null) window.clearTimeout(hideBarTimerRef.current) hideBarTimerRef.current = window.setTimeout(() => { setChromeVisible(false) hideBarTimerRef.current = null }, 1800) }, [fullscreenActive]) const revealChrome = useCallback(() => { setChromeVisible(true) scheduleHideChrome() }, [scheduleHideChrome]) const flushPointer = () => { pointerRafRef.current = null const sessionId = nativeSessionIdRef.current const payload = pendingPointerRef.current pendingPointerRef.current = null if (!sessionId || !payload) return if (protocol === 'rdp') void rdpInputPointer(sessionId, payload) else void vncInputPointer(sessionId, payload) } const mapPointer = (event: MouseEvent, pressed?: boolean) => { const canvas = canvasRef.current const sessionId = nativeSessionIdRef.current if (!canvas || !sessionId) return const rect = canvas.getBoundingClientRect() const scaleX = canvas.width / Math.max(rect.width, 1) const scaleY = canvas.height / Math.max(rect.height, 1) const x = Math.max(0, Math.min(canvas.width - 1, Math.round((event.clientX - rect.left) * scaleX))) const y = Math.max(0, Math.min(canvas.height - 1, Math.round((event.clientY - rect.top) * scaleY))) const isMove = pressed === undefined let button: DesktopPointerButton | undefined let pressedOut: boolean | undefined = pressed if (!isMove) { button = event.button === 1 ? 'middle' : event.button === 2 ? 'right' : 'left' if (button === 'left') buttonsRef.current.left = !!pressed if (button === 'middle') buttonsRef.current.middle = !!pressed if (button === 'right') buttonsRef.current.right = !!pressed } else if (buttonsRef.current.left) { button = 'left' pressedOut = true } else if (buttonsRef.current.right) { button = 'right' pressedOut = true } else if (buttonsRef.current.middle) { button = 'middle' pressedOut = true } const payload = { x, y, button, pressed: pressedOut, moving: isMove } if (!isMove) { // Position first, then button — improves Windows hit-testing reliability. if (protocol === 'rdp') { void rdpInputPointer(sessionId, { x, y, moving: true }) void rdpInputPointer(sessionId, payload) } else { void vncInputPointer(sessionId, payload) } return } pendingPointerRef.current = payload if (pointerRafRef.current == null) { pointerRafRef.current = window.requestAnimationFrame(flushPointer) } } const pasteToRemote = async (textFromEvent?: string) => { const sessionId = nativeSessionIdRef.current if (!sessionId) return const now = Date.now() if (now - pasteLockRef.current < 400) return pasteLockRef.current = now try { const text = textFromEvent ?? (await readClipboardText()) if (!text) return if (protocol === 'rdp') await rdpClipboardSet(sessionId, text) else await vncClipboardSet(sessionId, text) setStatusMessage(t('status.pastedToRemote')) } catch (reason) { setStatusMessage(reason instanceof Error ? reason.message : String(reason)) } } const onPasteClipboard = (event: ClipboardEvent) => { event.preventDefault() event.stopPropagation() const text = event.clipboardData?.getData('text/plain') void pasteToRemote(text || undefined) } const onKey = (event: KeyboardEvent, pressed: boolean) => { const sessionId = nativeSessionIdRef.current if (!sessionId) return // Local fullscreen controls — never forward to the remote desktop. if (event.key === 'F11') { event.preventDefault() event.stopPropagation() if (pressed) void toggleFullscreen(tabId) return } if (fullscreenActive && event.key === 'Escape') { event.preventDefault() event.stopPropagation() if (pressed) void exitFullscreen() return } event.preventDefault() const isPasteChord = (event.metaKey || event.ctrlKey) && event.code === 'KeyV' && !event.altKey && !event.shiftKey if (isPasteChord) { // Swallow Cmd/Ctrl+V (keydown and keyup). Native VNC types the text as // keysyms so remote terminals do not wrap it in bracketed paste. if (pressed) void pasteToRemote() return } const payload = { key: event.key, code: event.code, pressed, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, shiftKey: event.shiftKey, } if (protocol === 'rdp') void rdpInputKey(sessionId, payload) else void vncInputKey(sessionId, payload) } const onShareFileSelected = async (fileList: FileList | null) => { const sessionId = nativeSessionIdRef.current const file = fileList?.[0] if (!sessionId || !file || protocol !== 'rdp') return if (file.size > 50 * 1024 * 1024) { setError(t('status.fileTooLarge')) return } const buffer = new Uint8Array(await file.arrayBuffer()) let binary = '' for (let i = 0; i < buffer.length; i++) binary += String.fromCharCode(buffer[i]) await rdpUploadFile(sessionId, file.name, btoa(binary)) setStatusMessage(t('status.fileShared', { name: file.name })) } const openSharedFolder = async () => { try { await window.opsxApi?.openDownloadsFolder?.() } catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)) } } const Icon = protocol === 'rdp' ? Monitor : Cast const hostLabel = resolvedConfig && 'host' in resolvedConfig ? resolvedConfig.host : tab?.title const connected = tab?.connectionStatus === 'connected' const toolBtn = 'inline-flex h-7 w-7 items-center justify-center rounded-md text-content-secondary transition-colors hover:bg-dark-700 hover:text-content-primary disabled:cursor-not-allowed disabled:opacity-40' const toolbar = (
{ if (fullscreenActive) { setChromeVisible(true) if (hideBarTimerRef.current != null) { window.clearTimeout(hideBarTimerRef.current) hideBarTimerRef.current = null } } }} onMouseLeave={() => { if (fullscreenActive) scheduleHideChrome() }} > {hostLabel} {fbSize.width}×{fbSize.height} {tab?.connectionStatus === 'connecting' ? ( ) : ( )} {statusMessage} {error && {error}} {fullscreenActive && ( {t('toolbar.exitFullscreenHint')} )}
{protocol === 'rdp' && ( <> { void onShareFileSelected(event.target.files) event.target.value = '' }} /> )}
) return (
{ if (!fullscreenActive) return if (event.clientY <= 56) revealChrome() }} > {fullscreenActive ? (
{toolbar}
) : ( toolbar )}
event.preventDefault()} onMouseMove={(event) => mapPointer(event)} onMouseDown={(event) => mapPointer(event, true)} onMouseUp={(event) => mapPointer(event, false)} onMouseLeave={() => { buttonsRef.current = { left: false, middle: false, right: false } }} onPaste={onPasteClipboard} onKeyDown={(event) => onKey(event, true)} onKeyUp={(event) => onKey(event, false)} />
) }