/** * public/cell-monitor.ts — read-only "monitor" view of a session in a grid cell. * * A monitored quadrant renders the session's current screen via periodic * GET /live-sessions/:id/preview snapshots (RingBuffer.tail on the server) into a * read-only xterm. It NEVER attaches a WebSocket and NEVER sends a resize, so — * unlike a live interactive quadrant (latest-writer-wins) — it does not drive the * shared PTY size. That lets you WATCH a session in a small quadrant without * shrinking it for another device that is using it full-screen (the cross-device * shrink the split-grid design flagged). * * The snapshot is scaled with a CSS transform to fit the cell, like the launcher * thumbnails, so the real screen stays readable regardless of the cell size. */ import { Terminal } from '@xterm/xterm' import { fetchPreview, PREVIEW_CLEAR, PREVIEW_THEME } from './preview-grid.js' /** How often a monitored quadrant re-fetches the session's screen snapshot. */ const MONITOR_POLL_MS = 2000 export interface CellMonitor { dispose(): void } /** Scale the rendered term to fit `host`, top-left anchored (no upscaling). */ function fitMonitor(host: HTMLElement, screen: HTMLElement): void { const w = screen.offsetWidth const h = screen.offsetHeight if (w === 0 || h === 0 || host.clientWidth === 0 || host.clientHeight === 0) return const scale = Math.min(1, host.clientWidth / w, host.clientHeight / h) screen.style.transformOrigin = 'top left' screen.style.transform = `scale(${scale})` } /** * Mount a polling read-only preview of `sessionId` into `host`. Returns a handle * whose dispose() stops the poll and tears down the terminal. Best-effort: a * failed fetch just skips that tick (no throw). */ export function mountCellMonitor(host: HTMLElement, sessionId: string): CellMonitor { const term = new Terminal({ disableStdin: true, cursorBlink: false, fontFamily: 'Menlo, Consolas, monospace', fontSize: 12, scrollback: 0, theme: PREVIEW_THEME, }) term.open(host) let disposed = false let timer: ReturnType | null = null const poll = async (): Promise => { const p = await fetchPreview(sessionId) if (disposed) return if (p) { const cols = Math.max(2, p.cols) const rows = Math.max(2, p.rows) if (term.cols !== cols || term.rows !== rows) term.resize(cols, rows) term.reset() term.write(PREVIEW_CLEAR + p.data, () => { const screen = host.firstElementChild if (screen instanceof HTMLElement) fitMonitor(host, screen) }) } if (!disposed) timer = setTimeout(() => void poll(), MONITOR_POLL_MS) } void poll() return { dispose(): void { disposed = true if (timer !== null) { clearTimeout(timer) timer = null } term.dispose() }, } }