diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index 439e0e4f8f..1af79f6b22 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte b/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte
index 6c75ba670b..4e9fc63dc8 100644
--- a/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte
+++ b/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte
@@ -16,6 +16,21 @@
let inputContent = $state(content);
+ import {
+ AUTO_SCROLL_AT_BOTTOM_THRESHOLD,
+ AUTO_SCROLL_INTERVAL,
+ INITIAL_SCROLL_DELAY
+ } from '$lib/constants/auto-scroll';
+ import { onMount } from 'svelte';
+
+ let disableAutoScroll = $derived(Boolean(config().disableAutoScroll));
+ let autoScrollEnabled = $state(true);
+ let scrollContainer: HTMLTextAreaElement | null = $state(null);
+ let lastScrollTop = $state(0);
+ let scrollInterval: ReturnType | undefined;
+ let scrollTimeout: ReturnType | undefined;
+ let userScrolledUp = $state(false);
+
let isRouter = $derived(isRouterMode());
// Sync local input with store content
@@ -29,6 +44,12 @@
}
async function handleGenerate() {
+ if (!disableAutoScroll) {
+ userScrolledUp = false;
+ autoScrollEnabled = true;
+ scrollToBottom();
+ }
+
if (notebookModel == null) {
notebookModel = activeModelId;
}
@@ -93,6 +114,68 @@
notebookModel = modelName;
}
});
+
+ function handleScroll() {
+ if (disableAutoScroll || !scrollContainer) return;
+
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
+ const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
+
+ if (scrollTop < lastScrollTop && !isAtBottom) {
+ userScrolledUp = true;
+ autoScrollEnabled = false;
+ } else if (isAtBottom && userScrolledUp) {
+ userScrolledUp = false;
+ autoScrollEnabled = true;
+ }
+
+ if (scrollTimeout) {
+ clearTimeout(scrollTimeout);
+ }
+
+ scrollTimeout = setTimeout(() => {
+ if (isAtBottom) {
+ userScrolledUp = false;
+ autoScrollEnabled = true;
+ }
+ }, AUTO_SCROLL_INTERVAL);
+
+ lastScrollTop = scrollTop;
+ }
+
+ function scrollToBottom(behavior: ScrollBehavior = 'smooth') {
+ if (disableAutoScroll) return;
+
+ scrollContainer?.scrollTo({
+ top: scrollContainer?.scrollHeight,
+ behavior
+ });
+ }
+
+ onMount(() => {
+ if (!disableAutoScroll) {
+ setTimeout(() => scrollToBottom('instant'), INITIAL_SCROLL_DELAY);
+ }
+ });
+
+ $effect(() => {
+ if (disableAutoScroll) {
+ autoScrollEnabled = false;
+ if (scrollInterval) {
+ clearInterval(scrollInterval);
+ scrollInterval = undefined;
+ }
+ return;
+ }
+
+ if (notebookStore.isGenerating && autoScrollEnabled) {
+ scrollInterval = setInterval(() => scrollToBottom(), AUTO_SCROLL_INTERVAL);
+ } else if (scrollInterval) {
+ clearInterval(scrollInterval);
+ scrollInterval = undefined;
+ }
+ });