/** * public/launcher.ts — the home "session chooser" shown when no tab is open. * * Opening the app no longer auto-creates or auto-restores tabs. Instead this * start screen lists the host's running sessions as live preview thumbnails * (read-only xterm, same as the manage page) so the user picks which to open — * or starts a new one. Sessions persist server-side, so opening one replays its * full scrollback. The thumbnail card + preview plumbing is shared with the * manage page via public/preview-grid.ts (DRY). */ import type { LiveSessionInfo } from '../src/types.js' import { el, relTime, makePreviewCard, updatePreviewCard, loadPreviewInto, fetchLiveSessions, type PreviewCard, } from './preview-grid.js' export interface LauncherHooks { onOpen: (id: string) => void onNew: () => void } const THUMB_W = 320 const THUMB_MAX_H = 200 const REFRESH_MS = 5000 export interface Launcher { setVisible(v: boolean): void refresh(): void } export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher { const root = el('div', 'launcher') root.style.display = 'none' host.appendChild(root) const head = el('div', 'launcher-head') head.append(el('div', 'launcher-title', 'Your sessions')) const sub = el('div', 'launcher-sub', '') const actions = el('div', 'launcher-actions') const newBtn = el('button', 'launcher-new', '+ New session') newBtn.addEventListener('click', () => hooks.onNew()) const manage = el('a', 'mg-btn', '🗂 Manage') as HTMLAnchorElement manage.href = '/manage.html' actions.append(newBtn, manage) head.append(sub, actions) root.append(head) const grid = el('div', 'mg-grid') root.append(grid) const cards = new Map() let timer: ReturnType | null = null function makeCard(s: LiveSessionInfo): PreviewCard { return makePreviewCard(s, { onOpen: (id) => hooks.onOpen(id) }) } async function refresh(): Promise { const sessions = await fetchLiveSessions() sub.textContent = sessions.length ? `${sessions.length} running on this host — pick one to open` : 'No sessions running yet' const seen = new Set() for (const s of sessions) { seen.add(s.id) let card = cards.get(s.id) if (!card) { card = makeCard(s) cards.set(s.id, card) grid.append(card.el) } updatePreviewCard(card, s) card.meta.textContent = `${s.cwd ?? 'unknown dir'} · ${s.cols}×${s.rows} · ${relTime(s.createdAt)} old` void loadPreviewInto(s.id, card, THUMB_W, THUMB_MAX_H) } for (const [id, card] of cards) { if (!seen.has(id)) { card.term.dispose() card.el.remove() cards.delete(id) } } grid.querySelector('.mg-empty')?.remove() if (sessions.length === 0) { grid.append(el('div', 'mg-empty', 'No running sessions. Click “New session” to start one.')) } } return { setVisible(v: boolean): void { root.style.display = v ? 'block' : 'none' if (v) { void refresh() if (timer === null) { timer = setInterval(() => { if (root.style.display !== 'none') void refresh() }, REFRESH_MS) } } else { if (timer !== null) { clearInterval(timer) timer = null } for (const card of cards.values()) card.term.dispose() cards.clear() grid.replaceChildren() } }, refresh(): void { void refresh() }, } }