diff --git a/app/contexts/TerminalSessionsContext.tsx b/app/contexts/TerminalSessionsContext.tsx index 28c48f7..53fe349 100644 --- a/app/contexts/TerminalSessionsContext.tsx +++ b/app/contexts/TerminalSessionsContext.tsx @@ -26,6 +26,8 @@ export type SessionType = | "docker" | "remoteDesktop"; +export type RemoteDesktopProtocol = "rdp" | "vnc" | "telnet"; + export interface TerminalSession { id: string; host: SSHHost; @@ -39,24 +41,49 @@ export interface TerminalSession { backendSessionId?: string | null; /** When set, the terminal should attach to this backend session on connect. */ restoredSessionId?: string | null; + /** Selected protocol for remote desktop tabs. */ + remoteProtocol?: RemoteDesktopProtocol; } -const TYPE_LABELS: Record = { +const TYPE_LABELS: Record, string> = { terminal: "", stats: "Stats", filemanager: "Files", tunnel: "Tunnels", docker: "Docker", - remoteDesktop: "Remote", }; +function getSessionTypeLabel( + type: SessionType, + remoteProtocol?: RemoteDesktopProtocol, +): string { + if (type === "remoteDesktop") { + return remoteProtocol ? remoteProtocol.toUpperCase() : "Remote"; + } + return TYPE_LABELS[type]; +} + +function isSameSessionKind( + session: Pick, + host: SSHHost, + type: SessionType, + remoteProtocol?: RemoteDesktopProtocol, +): boolean { + if (session.host.id !== host.id || session.type !== type) return false; + if (type !== "remoteDesktop") return true; + return session.remoteProtocol === remoteProtocol; +} + /** open-tabs tabType is shared with the web app; map our session types to it. */ -function toTabType(type: SessionType): string { +function toTabType( + type: SessionType, + remoteProtocol?: RemoteDesktopProtocol, +): string { switch (type) { case "filemanager": return "files"; case "remoteDesktop": - return "rdp"; + return remoteProtocol ?? "rdp"; default: return type; } @@ -80,7 +107,11 @@ interface TerminalSessionsContextType { addSession: ( host: SSHHost, type?: SessionType, - opts?: { instanceId?: string; restoredSessionId?: string | null }, + opts?: { + instanceId?: string; + restoredSessionId?: string | null; + remoteProtocol?: RemoteDesktopProtocol; + }, ) => string; removeSession: (sessionId: string) => void; setActiveSession: (sessionId: string) => void; @@ -89,7 +120,11 @@ interface TerminalSessionsContextType { setBackendSessionId: (sessionId: string, backendId: string | null) => void; /** Forget a server-side background tab record. */ forgetBackgroundTab: (recordId: string) => void; - navigateToSessions: (host?: SSHHost, type?: SessionType) => void; + navigateToSessions: ( + host?: SSHHost, + type?: SessionType, + opts?: { remoteProtocol?: RemoteDesktopProtocol }, + ) => void; isCustomKeyboardVisible: boolean; toggleCustomKeyboard: () => void; lastKeyboardHeight: number; @@ -139,17 +174,22 @@ export const TerminalSessionsProvider: React.FC< ( host: SSHHost, type: SessionType = "terminal", - opts?: { instanceId?: string; restoredSessionId?: string | null }, + opts?: { + instanceId?: string; + restoredSessionId?: string | null; + remoteProtocol?: RemoteDesktopProtocol; + }, ): string => { const instanceId = opts?.instanceId ?? uuid(); const sessionId = `${host.id}-${type}-${Date.now()}`; setSessions((prev) => { const existingSessions = prev.filter( - (session) => session.host.id === host.id && session.type === type, + (session) => + isSameSessionKind(session, host, type, opts?.remoteProtocol), ); - const typeLabel = TYPE_LABELS[type]; + const typeLabel = getSessionTypeLabel(type, opts?.remoteProtocol); let title = typeLabel ? `${host.name} - ${typeLabel}` : host.name; if (existingSessions.length > 0) { title = typeLabel @@ -167,12 +207,13 @@ export const TerminalSessionsProvider: React.FC< instanceId, backendSessionId: opts?.restoredSessionId ?? null, restoredSessionId: opts?.restoredSessionId ?? null, + remoteProtocol: opts?.remoteProtocol, }; // Persist this tab server-side for cross-device awareness. addOpenTab({ id: instanceId, - tabType: toTabType(type), + tabType: toTabType(type, opts?.remoteProtocol), hostId: host.id, label: title, tabOrder: prev.length, @@ -205,11 +246,16 @@ export const TerminalSessionsProvider: React.FC< let updatedSessions = prev.filter((session) => session.id !== sessionId); // Re-number sibling sessions of the same host/type. - const hostId = sessionToRemove.host.id; const sessionType = sessionToRemove.type; + const remoteProtocol = sessionToRemove.remoteProtocol; const sameHostSessions = updatedSessions.filter( (session) => - session.host.id === hostId && session.type === sessionType, + isSameSessionKind( + session, + sessionToRemove.host, + sessionType, + remoteProtocol, + ), ); if (sameHostSessions.length > 0) { @@ -221,7 +267,10 @@ export const TerminalSessionsProvider: React.FC< (s) => s.id === session.id, ); if (sessionIndex !== -1) { - const typeLabel = TYPE_LABELS[session.type]; + const typeLabel = getSessionTypeLabel( + session.type, + session.remoteProtocol, + ); const baseName = typeLabel ? `${session.host.name} - ${typeLabel}` : session.host.name; @@ -294,9 +343,13 @@ export const TerminalSessionsProvider: React.FC< }, []); const navigateToSessions = useCallback( - (host?: SSHHost, type: SessionType = "terminal") => { + ( + host?: SSHHost, + type: SessionType = "terminal", + opts?: { remoteProtocol?: RemoteDesktopProtocol }, + ) => { if (host) { - addSession(host, type); + addSession(host, type, opts); } router.push("/(tabs)/sessions"); }, diff --git a/app/main-axios.ts b/app/main-axios.ts index d633a55..51f9283 100644 --- a/app/main-axios.ts +++ b/app/main-axios.ts @@ -1007,9 +1007,13 @@ export async function exportSSHHostWithCredentials( export async function getGuacamoleTokenFromHost( hostId: number, + protocol?: "rdp" | "vnc" | "telnet", ): Promise<{ token: string }> { try { - const response = await authApi.post(`/guacamole/connect-host/${hostId}`); + const response = await authApi.post( + `/guacamole/connect-host/${hostId}`, + protocol ? { protocol } : {}, + ); return response.data; } catch (error) { handleApiError(error, "connect Guacamole host"); diff --git a/app/tabs/hosts/HostActionSheet.tsx b/app/tabs/hosts/HostActionSheet.tsx index 4b2f090..30b52a9 100644 --- a/app/tabs/hosts/HostActionSheet.tsx +++ b/app/tabs/hosts/HostActionSheet.tsx @@ -18,7 +18,10 @@ import { import { SSHHost } from "@/types"; import type { HostMetrics } from "@/app/tabs/hosts/navigation/Host"; import { useTerminalSessions } from "@/app/contexts/TerminalSessionsContext"; -import type { SessionType } from "@/app/contexts/TerminalSessionsContext"; +import type { + RemoteDesktopProtocol, + SessionType, +} from "@/app/contexts/TerminalSessionsContext"; import { BottomSheet, SheetRow, Text } from "@/app/components/ui"; import { useThemeColor } from "@/app/contexts/ThemeContext"; import { toast } from "@/app/utils/toast"; @@ -75,6 +78,11 @@ export function HostActionSheet({ onClose(); }; + const openRemoteDesktop = (protocol: RemoteDesktopProtocol) => { + navigateToSessions(host, "remoteDesktop", { remoteProtocol: protocol }); + onClose(); + }; + const copyAddress = async () => { await Clipboard.setStringAsync( `${host.username ? `${host.username}@` : ""}${host.ip}`, @@ -129,10 +137,32 @@ export function HostActionSheet({ // Separate protocol actions (RDP / VNC / Telnet), each with its own icon — // matches the web rather than lumping them into one "Remote Desktop" row. const protocolActions = [ - host.enableRdp ? { icon: Monitor, label: "RDP" } : null, - host.enableVnc ? { icon: MousePointerClick, label: "VNC" } : null, - host.enableTelnet ? { icon: MessagesSquare, label: "Telnet" } : null, - ].filter(Boolean) as { icon: typeof Monitor; label: string }[]; + host.enableRdp + ? { + icon: Monitor, + label: "RDP", + protocol: "rdp" as RemoteDesktopProtocol, + } + : null, + host.enableVnc + ? { + icon: MousePointerClick, + label: "VNC", + protocol: "vnc" as RemoteDesktopProtocol, + } + : null, + host.enableTelnet + ? { + icon: MessagesSquare, + label: "Telnet", + protocol: "telnet" as RemoteDesktopProtocol, + } + : null, + ].filter(Boolean) as { + icon: typeof Monitor; + label: string; + protocol: RemoteDesktopProtocol; + }[]; const dotColor = status === "online" @@ -191,12 +221,12 @@ export function HostActionSheet({ onPress={() => open(type)} /> ))} - {protocolActions.map(({ icon: Icon, label }) => ( + {protocolActions.map(({ icon: Icon, label, protocol }) => ( } label={label} - onPress={() => open("remoteDesktop")} + onPress={() => openRemoteDesktop(protocol)} /> ))} diff --git a/app/tabs/sessions/ConnectionsPanel.tsx b/app/tabs/sessions/ConnectionsPanel.tsx index b715ab0..bf48997 100644 --- a/app/tabs/sessions/ConnectionsPanel.tsx +++ b/app/tabs/sessions/ConnectionsPanel.tsx @@ -18,6 +18,7 @@ import { } from "@/app/main-axios"; import { useTerminalSessions, + type RemoteDesktopProtocol, type SessionType, } from "@/app/contexts/TerminalSessionsContext"; import { Text } from "@/app/components/ui"; @@ -78,6 +79,13 @@ function toSessionType(tabType: string): SessionType { } } +function toRemoteProtocol(tabType: string): RemoteDesktopProtocol | undefined { + if (tabType === "rdp" || tabType === "vnc" || tabType === "telnet") { + return tabType; + } + return undefined; +} + function SectionHeader({ label, count }: { label: string; count: number }) { return ( @@ -252,6 +260,7 @@ export function ConnectionsPanel({ onClose }: { onClose?: () => void }) { addSession(host, toSessionType(record.tabType), { instanceId: record.id, restoredSessionId: live?.sessionId ?? record.backendSessionId ?? null, + remoteProtocol: toRemoteProtocol(record.tabType), }); navigateToSessions(); onClose?.(); @@ -308,7 +317,11 @@ export function ConnectionsPanel({ onClose }: { onClose?: () => void }) { key={s.id} isActive={s.id === activeSessionId} isLive={isLive} - tabType={s.type} + tabType={ + s.type === "remoteDesktop" + ? (s.remoteProtocol ?? s.type) + : s.type + } name={s.host.name} subLabel={ s.host.username diff --git a/app/tabs/sessions/Sessions.tsx b/app/tabs/sessions/Sessions.tsx index f22e858..fe15a5c 100644 --- a/app/tabs/sessions/Sessions.tsx +++ b/app/tabs/sessions/Sessions.tsx @@ -738,6 +738,7 @@ export default function Sessions() { host={session.host} isVisible={session.id === activeSessionId} title={session.title} + protocol={session.remoteProtocol} onClose={() => handleTabClose(session.id)} /> ); diff --git a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx index 771fc9d..1afa88f 100644 --- a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx +++ b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx @@ -28,6 +28,7 @@ import { X, } from "lucide-react-native"; import type { SSHHost } from "@/types"; +import type { RemoteDesktopProtocol } from "@/app/contexts/TerminalSessionsContext"; import { getGuacamoleTokenFromHost, getGuacamoleWebSocketUrl, @@ -60,10 +61,16 @@ interface RemoteDesktopProps { host: SSHHost; isVisible: boolean; title: string; + protocol?: RemoteDesktopProtocol; onClose?: () => void; } -export function RemoteDesktop({ host, isVisible, title }: RemoteDesktopProps) { +export function RemoteDesktop({ + host, + isVisible, + title, + protocol, +}: RemoteDesktopProps) { const color = useThemeColor(); const { bottom: safeBottom } = useSafeAreaInsets(); @@ -134,7 +141,10 @@ export function RemoteDesktop({ host, isVisible, title }: RemoteDesktopProps) { try { setConnectionState("connecting"); setErrorMessage(null); - const { token } = await getGuacamoleTokenFromHost(Number(host.id)); + const { token } = await getGuacamoleTokenFromHost( + Number(host.id), + protocol, + ); // Use measured layout size; fall back to ref if layout fired already const measured = availableSizeRef.current; const remW = measured ? measured.w : 1280; @@ -147,7 +157,7 @@ export function RemoteDesktop({ host, isVisible, title }: RemoteDesktopProps) { error instanceof Error ? error.message : "Failed to start remote session", ); } - }, [host.id]); + }, [host.id, protocol]); useEffect(() => { connect(); @@ -573,7 +583,11 @@ export function RemoteDesktop({ host, isVisible, title }: RemoteDesktopProps) { }, [inject]); const canSendInput = connectionState === "connected"; - const protocol = (host.connectionType || "rdp").toUpperCase(); + const protocolLabel = ( + protocol ?? + host.connectionType ?? + "rdp" + ).toUpperCase(); // The keyboard height event is from the screen bottom. Our container's bottom // already sits SESSION_TAB_BAR_HEIGHT + safeBottom above the screen bottom @@ -795,7 +809,7 @@ export function RemoteDesktop({ host, isVisible, title }: RemoteDesktopProps) { {(connectionState === "connecting" || connectionState === "idle") && ( - Connecting {protocol} + Connecting {protocolLabel} {title} )}