diff --git a/CHANGELOG.md b/CHANGELOG.md index 051868266..e448c8c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - 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: 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 model auth source (api-key/oauth). - Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split. diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 0a8bc02be..1b46c70f8 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -437,34 +437,40 @@ export class ClawdbotApp extends LitElement { clearTimeout(this.chatScrollTimeout); this.chatScrollTimeout = null; } + const pickScrollTarget = () => { + const container = this.querySelector(".chat-thread") as HTMLElement | null; + if (container) { + const overflowY = getComputedStyle(container).overflowY; + const canScroll = + overflowY === "auto" || + overflowY === "scroll" || + container.scrollHeight - container.clientHeight > 1; + if (canScroll) return container; + } + return (document.scrollingElement ?? document.documentElement) as HTMLElement | null; + }; // Wait for Lit render to complete, then scroll void this.updateComplete.then(() => { this.chatScrollFrame = requestAnimationFrame(() => { this.chatScrollFrame = null; - if (force) { - // Force scroll window to bottom unconditionally - this.chatHasAutoScrolled = true; - window.scrollTo({ top: document.body.scrollHeight, behavior: "instant" }); - // Retry after images/content load - this.chatScrollTimeout = window.setTimeout(() => { - this.chatScrollTimeout = null; - window.scrollTo({ top: document.body.scrollHeight, behavior: "instant" }); - }, 150); - return; - } - // Stick to bottom if already near bottom + const target = pickScrollTarget(); + if (!target) return; const distanceFromBottom = - document.body.scrollHeight - window.scrollY - window.innerHeight; - const shouldStick = distanceFromBottom < 200; + target.scrollHeight - target.scrollTop - target.clientHeight; + const shouldStick = force || distanceFromBottom < 200; if (!shouldStick) return; - window.scrollTo({ top: document.body.scrollHeight, behavior: "instant" }); + 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 = - document.body.scrollHeight - window.scrollY - window.innerHeight; - if (latestDistanceFromBottom >= 250) return; - window.scrollTo({ top: document.body.scrollHeight, behavior: "instant" }); - }, 120); + latest.scrollHeight - latest.scrollTop - latest.clientHeight; + if (!force && latestDistanceFromBottom >= 250) return; + latest.scrollTop = latest.scrollHeight; + }, retryDelay); }); }); }