wraith/frontend/components/session/SessionContainer.vue
Vantz Stockwell e2e03be2dd feat: RDP frontend — Guacamole client with custom JSON WebSocket tunnel
- useRdp.ts: JsonWsTunnel class extends Guacamole.Tunnel to bridge
  guacamole-common-js (expects raw protocol) with our JSON gateway
  (consistent with SSH/SFTP message envelope pattern). Parses
  length-prefixed Guacamole instructions, dispatches to Guacamole.Client.
  Handles mouse/keyboard input, clipboard send, and session lifecycle.
- RdpCanvas.vue: full-size container that mounts the Guacamole display
  canvas. Calls useRdp().connectRdp() on mount, cleans up on unmount.
  Exposes sendClipboard() and disconnect() for toolbar integration.
- RdpToolbar.vue: auto-hiding floating toolbar (3s idle timeout) with
  clipboard paste dialog, fullscreen toggle (HTML5 Fullscreen API),
  settings panel stub, and disconnect button.
- SessionContainer.vue: renders RdpCanvas + RdpToolbar when
  session.protocol === 'rdp', replacing the Phase 3 placeholder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:27:19 -04:00

89 lines
2.9 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
import { useSessionStore } from '~/stores/session.store'
const sessions = useSessionStore()
// Sessions with pending-XXX IDs are mounting TerminalInstance which will get a real UUID.
// SFTP sidebar only connects once we have a real (non-pending) backend session ID.
function isRealSession(id: string) {
return !id.startsWith('pending-')
}
// Per-session refs to RdpCanvas instances (keyed by session.id)
// Used to forward toolbar actions (clipboard, disconnect) to the canvas
const rdpCanvasRefs = ref<Record<string, any>>({})
function setRdpRef(sessionId: string, el: any) {
if (el) {
rdpCanvasRefs.value[sessionId] = el
} else {
delete rdpCanvasRefs.value[sessionId]
}
}
function handleRdpDisconnect(sessionId: string) {
rdpCanvasRefs.value[sessionId]?.disconnect()
}
function handleRdpClipboard(sessionId: string, text: string) {
rdpCanvasRefs.value[sessionId]?.sendClipboard(text)
}
</script>
<template>
<!-- Session container always in DOM when sessions exist, uses v-show for persistence -->
<div v-if="sessions.hasSessions" class="absolute inset-0 flex flex-col z-10 bg-gray-950">
<!-- Tab bar -->
<TerminalTabs />
<!-- Session panels — v-show keeps terminal alive when switching tabs -->
<div class="flex-1 overflow-hidden relative">
<div
v-for="session in sessions.sessions"
:key="session.id"
v-show="session.id === sessions.activeSessionId"
class="absolute inset-0 flex"
>
<!-- SSH session: SFTP sidebar + terminal -->
<template v-if="session.protocol === 'ssh'">
<!-- SFTP sidebar only renders once we have a real backend sessionId -->
<SftpSidebar
v-if="isRealSession(session.id)"
:session-id="session.id"
/>
<!-- Terminal — always renders, handles pending→real session ID transition internally -->
<div class="flex-1 overflow-hidden">
<TerminalInstance
:session-id="session.id"
:host-id="session.hostId"
:host-name="session.hostName"
:color="session.color"
/>
</div>
</template>
<!-- RDP session: Guacamole canvas + floating toolbar -->
<template v-else-if="session.protocol === 'rdp'">
<div class="flex-1 overflow-hidden relative">
<RdpCanvas
:ref="(el) => setRdpRef(session.id, el)"
:host-id="session.hostId"
:host-name="session.hostName"
:color="session.color"
/>
<RdpToolbar
:host-name="session.hostName"
@disconnect="handleRdpDisconnect(session.id)"
@send-clipboard="(text) => handleRdpClipboard(session.id, text)"
/>
</div>
</template>
</div>
</div>
<!-- Transfer status bar -->
<TransferStatus :transfers="[]" />
</div>
</template>