Merge pull request #274 from kiranjd/fix/chat-scroll-to-bottom

fix(ui): scroll chat to bottom on initial load
This commit is contained in:
Peter Steinberger
2026-01-06 06:49:29 +00:00
committed by GitHub
2 changed files with 36 additions and 19 deletions

View File

@@ -54,6 +54,7 @@
- Control UI: animate reading indicator dots (honors reduced-motion). - Control UI: animate reading indicator dots (honors reduced-motion).
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping). - Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
- Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268. - Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268.
- Control UI: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
- Status: show runtime (docker/direct) and move shortcuts to `/help`. - Status: show runtime (docker/direct) and move shortcuts to `/help`.
- Status: show model auth source (api-key/oauth). - Status: show model auth source (api-key/oauth).
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split. - Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.

View File

@@ -437,25 +437,41 @@ export class ClawdbotApp extends LitElement {
clearTimeout(this.chatScrollTimeout); clearTimeout(this.chatScrollTimeout);
this.chatScrollTimeout = null; this.chatScrollTimeout = null;
} }
this.chatScrollFrame = requestAnimationFrame(() => { const pickScrollTarget = () => {
this.chatScrollFrame = null;
const container = this.querySelector(".chat-thread") as HTMLElement | null; const container = this.querySelector(".chat-thread") as HTMLElement | null;
if (!container) return; if (container) {
const distanceFromBottom = const overflowY = getComputedStyle(container).overflowY;
container.scrollHeight - container.scrollTop - container.clientHeight; const canScroll =
const shouldStick = force || distanceFromBottom < 140; overflowY === "auto" ||
if (!shouldStick) return; overflowY === "scroll" ||
if (force) this.chatHasAutoScrolled = true; container.scrollHeight - container.clientHeight > 1;
container.scrollTop = container.scrollHeight; if (canScroll) return container;
this.chatScrollTimeout = window.setTimeout(() => { }
this.chatScrollTimeout = null; return (document.scrollingElement ?? document.documentElement) as HTMLElement | null;
const latest = this.querySelector(".chat-thread") as HTMLElement | null; };
if (!latest) return; // Wait for Lit render to complete, then scroll
const latestDistanceFromBottom = void this.updateComplete.then(() => {
latest.scrollHeight - latest.scrollTop - latest.clientHeight; this.chatScrollFrame = requestAnimationFrame(() => {
if (!force && latestDistanceFromBottom >= 180) return; this.chatScrollFrame = null;
latest.scrollTop = latest.scrollHeight; const target = pickScrollTarget();
}, 120); if (!target) return;
const distanceFromBottom =
target.scrollHeight - target.scrollTop - target.clientHeight;
const shouldStick = force || distanceFromBottom < 200;
if (!shouldStick) return;
if (force) this.chatHasAutoScrolled = true;
target.scrollTop = target.scrollHeight;
const retryDelay = force ? 150 : 120;
this.chatScrollTimeout = window.setTimeout(() => {
this.chatScrollTimeout = null;
const latest = pickScrollTarget();
if (!latest) return;
const latestDistanceFromBottom =
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
if (!force && latestDistanceFromBottom >= 250) return;
latest.scrollTop = latest.scrollHeight;
}, retryDelay);
});
}); });
} }
@@ -689,7 +705,7 @@ export class ClawdbotApp extends LitElement {
if (this.tab === "nodes") await loadNodes(this); if (this.tab === "nodes") await loadNodes(this);
if (this.tab === "chat") { if (this.tab === "chat") {
await Promise.all([loadChatHistory(this), loadSessions(this)]); await Promise.all([loadChatHistory(this), loadSessions(this)]);
this.scheduleChatScroll(); this.scheduleChatScroll(!this.chatHasAutoScrolled);
} }
if (this.tab === "config") { if (this.tab === "config") {
await loadConfigSchema(this); await loadConfigSchema(this);