diff --git a/public/index.html b/public/index.html index 4b706a9..f213924 100644 --- a/public/index.html +++ b/public/index.html @@ -194,12 +194,45 @@ border-top: 1px solid #3a3a3a; max-height: var(--terminal-height); overflow-y: auto; + position: relative; } .session.expanded .session-output { display: block; } + .pin-scroll-btn { + position: absolute; + bottom: 12px; + right: 12px; + background: rgba(42, 42, 42, 0.9); + border: 1px solid #3a3a3a; + border-radius: 6px; + padding: 8px 12px; + font-size: 16px; + cursor: pointer; + transition: background 0.2s, opacity 0.2s; + z-index: 10; + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + } + + .pin-scroll-btn:hover { + background: rgba(58, 58, 58, 0.95); + } + + .pin-scroll-btn:active { + opacity: 0.7; + } + + .pin-scroll-btn.pinned { + background: rgba(255, 152, 0, 0.2); + border-color: #ff9800; + } + .terminal { font-family: "SF Mono", Monaco, "Cascadia Code", "Courier New", monospace; font-size: var(--font-size); @@ -1009,6 +1042,7 @@ idle_since: null, git_branch: null, git_files_json: null, + autoScroll: true, }); renderSessions(); }); @@ -1280,11 +1314,24 @@
${renderStatsWidget(s)} -
+
+
${s.output}
`).join(''); + + // Attach scroll listeners after rendering + sessionsToRender.forEach(s => { + const $output = document.getElementById(`session-output-${s.id}`); + if ($output) { + attachScrollListener(s.id, $output); + } + }); } function renderSessionOutput(sessionId) { @@ -1299,7 +1346,8 @@ $output.innerHTML += newChunk; session.outputRenderedLength = session.output.length; } - if (session.expanded) { + // Only auto-scroll if session is expanded and autoScroll is enabled + if (session.expanded && session.autoScroll) { $output.parentElement.scrollTop = $output.parentElement.scrollHeight; } } @@ -1915,6 +1963,68 @@ }).join(''); } + // Auto-scroll control + const scrollListeners = new Map(); + + function attachScrollListener(sessionId, $outputContainer) { + // Remove existing listener if present + const existing = scrollListeners.get(sessionId); + if (existing) { + $outputContainer.removeEventListener('scroll', existing); + } + + const listener = () => { + const session = state.sessions.get(sessionId); + if (!session) return; + + const { scrollTop, clientHeight, scrollHeight } = $outputContainer; + const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; + + if (session.autoScroll !== isAtBottom) { + session.autoScroll = isAtBottom; + updatePinButton(sessionId); + } + }; + + $outputContainer.addEventListener('scroll', listener); + scrollListeners.set(sessionId, listener); + } + + window.toggleAutoScroll = (sessionId) => { + const session = state.sessions.get(sessionId); + if (!session) return; + + session.autoScroll = !session.autoScroll; + + // If enabling auto-scroll, scroll to bottom immediately + if (session.autoScroll) { + const $outputContainer = document.getElementById(`session-output-${sessionId}`); + if ($outputContainer) { + $outputContainer.scrollTop = $outputContainer.scrollHeight; + } + } + + updatePinButton(sessionId); + }; + + function updatePinButton(sessionId) { + const session = state.sessions.get(sessionId); + if (!session) return; + + const $btn = document.querySelector(`#session-output-${sessionId} .pin-scroll-btn`); + if ($btn) { + if (session.autoScroll) { + $btn.classList.add('pinned'); + $btn.textContent = '📌'; + $btn.title = 'Auto-scroll enabled'; + } else { + $btn.classList.remove('pinned'); + $btn.textContent = '📍'; + $btn.title = 'Auto-scroll disabled'; + } + } + } + // Initialize loadSettings(); applySettings();