From 116847ab587cc2ecc80132efcd2506e14d897cf3 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 31 Jan 2026 10:14:25 -0500 Subject: [PATCH] Add multiple fixes --- public/index.html | 135 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/public/index.html b/public/index.html index a91712b..0692106 100644 --- a/public/index.html +++ b/public/index.html @@ -220,6 +220,13 @@ align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + opacity: 0; + pointer-events: none; + } + + .session-output.scrolled-up .scroll-to-bottom-btn { + opacity: 1; + pointer-events: auto; } .scroll-to-bottom-btn:hover { @@ -235,9 +242,10 @@ .terminal { font-family: "SF Mono", Monaco, "Cascadia Code", "Courier New", monospace; font-size: var(--font-size); - line-height: 1.5; + line-height: 1.3; white-space: var(--wrap-mode); - word-wrap: break-word; + word-wrap: normal; + overflow-wrap: normal; color: #e0e0e0; } @@ -1012,7 +1020,7 @@ columns: 1, // 1, 2, 3, or 'fit' fontSize: 12, // px value terminalHeight: 400, // px value - wrapText: true, // true = pre-wrap, false = pre (horizontal scroll) + wrapText: false, // true = pre-wrap, false = pre (horizontal scroll) }, }; @@ -1043,26 +1051,30 @@ } const sessions = await res.json(); sessions.forEach(session => { - state.sessions.set(session.id, { + const existing = state.sessions.get(session.id); + const merged = { id: session.id, cwd: session.cwd, command: session.command, - output: '', - outputRenderedLength: 0, - expanded: false, - state: 'ready', - prompts: 0, - completions: 0, - tools: 0, - compressions: 0, - thinking_seconds: 0, - work_seconds: 0, - mode: 'normal', - model: null, - idle_since: null, - git_branch: null, - git_files_json: null, - }); + // Preserve any output already received via SSE before fetch completed + output: existing?.output ?? '', + outputRenderedLength: existing?.outputRenderedLength ?? 0, + // Preserve expansion state + expanded: existing?.expanded ?? false, + state: existing?.state ?? 'ready', + prompts: existing?.prompts ?? 0, + completions: existing?.completions ?? 0, + tools: existing?.tools ?? 0, + compressions: existing?.compressions ?? 0, + thinking_seconds: existing?.thinking_seconds ?? 0, + work_seconds: existing?.work_seconds ?? 0, + mode: existing?.mode ?? 'normal', + model: existing?.model ?? null, + idle_since: existing?.idle_since ?? null, + git_branch: existing?.git_branch ?? null, + git_files_json: existing?.git_files_json ?? null, + }; + state.sessions.set(session.id, merged); }); renderSessions(); } catch (error) { @@ -1135,13 +1147,36 @@ es.addEventListener('initial_state', (e) => { const data = JSON.parse(e.data); - const session = state.sessions.get(data.session_id); - if (session) { - // Replace output with current terminal state - session.output = data.html; - session.outputRenderedLength = 0; // Force re-render - renderSessionOutput(data.session_id); + let session = state.sessions.get(data.session_id); + if (!session) { + // Create a placeholder session so we don't drop the state + session = { + id: data.session_id, + cwd: '', + command: 'claude', + output: '', + outputRenderedLength: 0, + expanded: false, + state: 'ready', + prompts: 0, + completions: 0, + tools: 0, + compressions: 0, + thinking_seconds: 0, + work_seconds: 0, + mode: 'normal', + model: null, + idle_since: null, + git_branch: null, + git_files_json: null, + }; + state.sessions.set(data.session_id, session); + renderSessions(); } + // Replace output with current terminal state + session.output = data.html; + session.outputRenderedLength = 0; // Force re-render + renderSessionOutput(data.session_id); }); es.addEventListener('output', (e) => { @@ -1226,6 +1261,10 @@ // Wait for DOM to update before measuring setTimeout(() => { handleSessionResize(Number(sessionId)); + // After mount, ensure we start at bottom and initialize UI state + scrollToBottom(Number(sessionId)); + updateScrollButton(Number(sessionId)); + initSessionOutputUI(Number(sessionId)); }, 0); } } @@ -1426,6 +1465,12 @@ `).join(''); + + // Initialize scroll handlers and button visibility + sessionsToRender.forEach(s => { + initSessionOutputUI(s.id); + updateScrollButton(s.id); + }); } function renderSessionOutput(sessionId) { @@ -1434,17 +1479,47 @@ const $output = document.getElementById(`output-${sessionId}`); if ($output) { - // Always full replace since serializeAsHTML() returns full screen state - $output.innerHTML = session.output; + const $outputContainer = document.getElementById(`session-output-${sessionId}`); + const shouldStickToBottom = + !!($outputContainer && session.expanded && isAtBottom($outputContainer)); + + // Only mutate DOM when expanded to reduce work + if (session.expanded) { + $output.innerHTML = session.output; + } session.outputRenderedLength = session.output.length; - const $outputContainer = document.getElementById(`session-output-${sessionId}`); - if ($outputContainer && session.expanded) { + if ($outputContainer && session.expanded && shouldStickToBottom) { $outputContainer.scrollTop = $outputContainer.scrollHeight; } + + // Update scroll-to-bottom button visibility + updateScrollButton(sessionId); } } + function isAtBottom(el, threshold = 8) { + return el.scrollTop + el.clientHeight >= el.scrollHeight - threshold; + } + + function updateScrollButton(sessionId) { + const $outputContainer = document.getElementById(`session-output-${sessionId}`); + if (!$outputContainer) return; + const nearBottom = isAtBottom($outputContainer); + if (nearBottom) { + $outputContainer.classList.remove('scrolled-up'); + } else { + $outputContainer.classList.add('scrolled-up'); + } + } + + function initSessionOutputUI(sessionId) { + const $outputContainer = document.getElementById(`session-output-${sessionId}`); + if (!$outputContainer) return; + // Attach scroll listener to toggle the button visibility + $outputContainer.addEventListener('scroll', () => updateScrollButton(sessionId)); + } + function updateSessionCard(sessionId) { const session = state.sessions.get(sessionId); if (!session) return;