From ffe473b082e5cbde2514bae06f6837ab3cb4ecab Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 2 Jul 2026 19:59:20 +0900 Subject: [PATCH] Add sticky auto-scroll for build output Implements #220 - if the page is scrolled to the very bottom while a build log is streaming, the scroll position "sticks" there and follows new output as it is rendered, like tail -f. Any upward scroll disengages this behaviour, and scrolling back to the bottom re-engages it, so no toggle button is needed. The sticky state is tracked with wheel/scroll event listeners rather than sampled from window.scrollY at render time, because scrolling is handled outside the main thread: while the main thread is busy rendering a large log chunk, window.scrollY may still report the old (bottom) position, which would wrongly yank the page back down and swallow the user's upward scroll. AI-assisted implementation. Co-Authored-By: Claude Fable 5 --- src/resources/js/app.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/resources/js/app.js b/src/resources/js/app.js index 54ae1a4..1a8e552 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -669,6 +669,23 @@ const Run = templateId => { latestNum: null, logComplete: false, }; + // if the page is scrolled to the bottom, the scroll position "sticks" + // there and follows new log output as it is rendered, like tail -f. + // Any upward scroll disengages this, scrolling back to the bottom + // re-engages it. This must be tracked with event listeners rather than + // sampled when rendering, because scrolling is handled outside the main + // thread: while the main thread is busy rendering a large log chunk, + // window.scrollY may still report the old (bottom) position, which + // would wrongly yank the page back down and swallow the user's scroll. + let stickToBottom = true; + window.addEventListener('wheel', evt => { + if (evt.deltaY < 0) + stickToBottom = false; + }); + window.addEventListener('scroll', () => { + stickToBottom = window.innerHeight + window.scrollY >= + document.documentElement.scrollHeight - 1; + }); const logFetcher = (vm, name, num) => { const abort = new AbortController(); fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => { @@ -705,6 +722,9 @@ const Run = templateId => { state.logComplete = true; } + if (stickToBottom) + window.scrollTo(0, document.documentElement.scrollHeight); + lastUiUpdate = Date.now(); tid = null; }