/** Read-only view of the Core SSH sessions (permission `session.read`). */ import { call } from "../transport"; import type { SessionStatus } from "../types"; export function displayName(session: SessionStatus): string { return (session.adhoc ? session.label : `${session.label} — ${session.username}@${session.host}`) + (session.slot ? ` · T${session.slot}` : "") + (session.jump?.length ? ` ⇢ ${session.jump[0].host}` : ""); } export function reconcile(previousId: string, list: readonly SessionStatus[]): { id: string; reason: "keep" | "gone" | "auto" | "choose" } { const connected = list.filter((session) => session.state === "connected"); if (previousId) return connected.some((session) => session.sessionId === previousId) ? { id: previousId, reason: "keep" } : { id: "", reason: "gone" }; return connected.length === 1 ? { id: connected[0].sessionId, reason: "auto" } : { id: "", reason: "choose" }; } let askedFor = ""; /** * Several connections and none chosen: asks once which one the module should use. * Asks again only when the set of connections changes; closing the dialog leaves the menu as it is. */ export function offerChoice(select: HTMLSelectElement, list: readonly SessionStatus[], title: string): void { const connected = list.filter((session) => session.state === "connected"); const key = connected.map((session) => session.sessionId).sort().join("|"); if (connected.length < 2 || select.value || key === askedFor || document.querySelector("dialog[open]")) return; askedFor = key; const dialog = document.createElement("dialog"); dialog.style.cssText = "min-width:340px;max-width:560px;padding:18px;border:1px solid rgba(151,183,218,.25);border-radius:12px;color:#edf6ff;background:#0c1a2a"; const heading = document.createElement("div"); heading.textContent = title; heading.style.cssText = "margin-bottom:12px;font-size:14px;font-weight:600"; dialog.append(heading); for (const session of connected) { const button = document.createElement("button"); button.type = "button"; button.textContent = displayName(session); button.style.cssText = "display:block;width:100%;margin-top:6px;padding:10px 12px;border:1px solid rgba(151,183,218,.25);border-radius:8px;color:inherit;background:rgba(255,255,255,.04);text-align:start;font:inherit;cursor:pointer"; button.addEventListener("click", () => { dialog.close(); if (!select.value && [...select.options].some((option) => option.value === session.sessionId)) { select.value = session.sessionId; select.dispatchEvent(new Event("change", { bubbles: true })); } }); dialog.append(button); } dialog.addEventListener("close", () => dialog.remove()); document.body.append(dialog); dialog.showModal(); } export const sessions = Object.freeze({ list: () => call("module_sessions_list", "plugin:sessions:list"), displayName, reconcile, offerChoice });