From 5948dcaed1c0f7b864ba5d4b920caa10030a6afc Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Fri, 30 Jan 2026 08:19:09 -0500 Subject: [PATCH] Add viewport-based PTY resize from dashboard --- public/index.html | 79 +++++++++++++++++++++++++++++++++++++++++++++++ src/server.ts | 39 +++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/public/index.html b/public/index.html index 4692d6e..c352e76 100644 --- a/public/index.html +++ b/public/index.html @@ -1134,6 +1134,14 @@ // Reset rendered length when collapsing or expanding (element gets recreated) session.outputRenderedLength = 0; renderSessions(); + + // If expanding, resize the PTY to fit the viewport + if (session.expanded) { + // Wait for DOM to update before measuring + setTimeout(() => { + handleSessionResize(Number(sessionId)); + }, 0); + } } }; @@ -1978,6 +1986,74 @@ // Auto-scroll control const scrollListeners = new Map(); + // Viewport-based PTY resize + let resizeDebounceTimer = null; + + function measureTerminalCols() { + // Create a hidden span with monospace font to measure character width + const $measure = document.createElement('span'); + $measure.style.fontFamily = '"SF Mono", Monaco, "Cascadia Code", "Courier New", monospace'; + $measure.style.fontSize = `${state.settings.fontSize}px`; + $measure.style.lineHeight = '1.5'; + $measure.style.visibility = 'hidden'; + $measure.style.position = 'absolute'; + $measure.textContent = 'X'.repeat(100); // Measure 100 chars + document.body.appendChild($measure); + + const charWidth = $measure.offsetWidth / 100; + document.body.removeChild($measure); + + // Get terminal container width (accounting for padding) + const $container = document.querySelector('.session-output'); + if (!$container) { + return 80; // Default fallback + } + + const containerWidth = $container.clientWidth - 32; // 16px padding on each side + const cols = Math.floor(containerWidth / charWidth); + + // Clamp to reasonable bounds + return Math.max(40, Math.min(cols, 300)); + } + + async function resizeSessionPTY(sessionId, cols, rows) { + try { + const res = await fetch(`/api/sessions/${sessionId}/resize`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ cols, rows }), + }); + if (!res.ok) { + console.error('Failed to resize session PTY:', await res.text()); + } + } catch (error) { + console.error('Error resizing session PTY:', error); + } + } + + function handleSessionResize(sessionId) { + const cols = measureTerminalCols(); + const rows = 24; // Reasonable default row count + resizeSessionPTY(sessionId, cols, rows); + } + + function handleWindowResize() { + if (resizeDebounceTimer) { + clearTimeout(resizeDebounceTimer); + } + + resizeDebounceTimer = setTimeout(() => { + // Resize all expanded sessions + state.sessions.forEach((session) => { + if (session.expanded) { + handleSessionResize(session.id); + } + }); + }, 300); // Debounce for 300ms + } + function attachScrollListener(sessionId, $outputContainer) { // Remove existing listener if present const existing = scrollListeners.get(sessionId); @@ -2041,6 +2117,9 @@ loadSettings(); applySettings(); connectSSE(); + + // Set up window resize listener for PTY viewport resize + window.addEventListener('resize', handleWindowResize); diff --git a/src/server.ts b/src/server.ts index 6cfbac6..d9dc1cd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -141,6 +141,45 @@ const server = Bun.serve({ return Response.json(sessions); } + // Resize endpoint for dashboard to resize CLI PTY + if ( + url.pathname.match(/^\/api\/sessions\/\d+\/resize$/) && + req.method === "POST" + ) { + const parts = url.pathname.split("/"); + const sessionId = Number.parseInt(parts[3] || "", 10); + + if (Number.isNaN(sessionId)) { + return new Response("Invalid session ID", { status: 400 }); + } + + const body = (await req.json()) as { cols?: unknown; rows?: unknown }; + if ( + typeof body.cols !== "number" || + typeof body.rows !== "number" || + body.cols <= 0 || + body.rows <= 0 + ) { + return new Response("Missing or invalid cols/rows", { status: 400 }); + } + + // Get WebSocket connection for this session + const ws = sessionWebSockets.get(sessionId); + if (!ws) { + return new Response("Session WebSocket not found", { status: 404 }); + } + + // Send resize command to CLI + const message: ServerMessage = { + type: "resize", + cols: body.cols, + rows: body.rows, + }; + ws.send(JSON.stringify(message)); + + return Response.json({ success: true }); + } + // Create prompt for a session if ( url.pathname.match(/^\/api\/sessions\/\d+\/prompts$/) &&