diff --git a/frontend/packages/shared/src/terminal.css b/frontend/packages/shared/src/terminal.css index d7cc5b64..6a39682c 100644 --- a/frontend/packages/shared/src/terminal.css +++ b/frontend/packages/shared/src/terminal.css @@ -36,6 +36,11 @@ overflow-y: auto; max-height: 32em; font-family: inherit; + /* Disable browser scroll anchoring — the terminal manages scroll + position manually (snapToBottom + loadMore compensation). Scroll + anchoring would double-compensate scrollTop during loadMore() + prepends, causing an erratic jump. */ + overflow-anchor: none; } .live.terminal { background: transparent; diff --git a/frontend/packages/shared/src/terminal.js b/frontend/packages/shared/src/terminal.js index 0f495fa7..aefb0223 100644 --- a/frontend/packages/shared/src/terminal.js +++ b/frontend/packages/shared/src/terminal.js @@ -316,6 +316,12 @@ export function create(opts) { histHasMore = body.has_more || false; if (typeof body.min_id === 'number') histMinId = body.min_id; + // Resolve load-more button state before capturing the scroll + // baseline so that any button removal is already reflected in + // beforeH — otherwise the button's height would be missing from + // the delta and the viewport would drift up by that amount. + updateLoadMoreBtn(); + if (events.length > 0) { // Render into a detached element; `log` is temporarily redirected // so that row/details/etc. append there instead of rootLog. @@ -338,9 +344,10 @@ export function create(opts) { const beforeH = rootLog.scrollHeight; while (tempEl.firstChild) rootLog.insertBefore(tempEl.firstChild, anchor); // Compensate scroll so the viewport stays on the same content. + // overflow-anchor: none on .live ensures the browser does not + // also auto-adjust scrollTop (which would double the delta). rootLog.scrollTop += rootLog.scrollHeight - beforeH; } - updateLoadMoreBtn(); } catch (err) { console.warn('loadMore failed', err); updateLoadMoreBtn();