From d4db264a1a963541a9190e86bdd16f28a13ca805 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Wed, 13 May 2026 13:43:45 +1000 Subject: [PATCH 1/2] fix(app): anchor virtual timeline to bottom --- packages/app/src/pages/session.tsx | 6 +++++- .../app/src/pages/session/message-timeline.tsx | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 08fad258f6..b45ab7f9de 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -29,7 +29,7 @@ import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" import { checksum } from "@opencode-ai/core/util/encode" -import { useSearchParams } from "@solidjs/router" +import { useLocation, useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" @@ -196,6 +196,7 @@ export default function Page() { const comments = useComments() const terminal = useTerminal() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() + const location = useLocation() const { params, sessionKey, tabs, view } = useSessionLayout() createEffect(() => { @@ -1719,6 +1720,9 @@ export default function Page() { onUserScroll={markUserScroll} onHistoryScroll={historyLoader.onScrollerScroll} onAutoScrollInteraction={autoScroll.handleInteraction} + shouldAnchorBottom={() => + !location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled() + } centered={centered()} setContentRef={(el) => { content = el diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 6c6eda46e8..6ef62ea22b 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -107,6 +107,7 @@ function sameKeys(a: readonly string[] | undefined, b: readonly string[] | undef } const timelineCacheLimit = 16 +const timelineFallbackItemSize = 60 const timelineCache = new Map() function readTimelineCache(id: string, keys: readonly string[]) { @@ -451,6 +452,7 @@ export function MessageTimeline(props: { onUserScroll: () => void onHistoryScroll: () => void onAutoScrollInteraction: (event: MouseEvent) => void + shouldAnchorBottom: () => boolean centered: boolean setContentRef: (el: HTMLDivElement) => void historyShift: boolean @@ -713,6 +715,19 @@ export function MessageTimeline(props: { let cacheRowKeys = timelineRowKeys() let virtualizerSessionKey = cacheSessionKey let virtualizerRowKeys = cacheRowKeys + let bottomAnchorSessionKey = "" + + const maybeAnchorBottom = () => { + const key = sessionKey() + if (bottomAnchorSessionKey === key) return + if (!virtualizer) return + if (timelineRowKeys().length === 0) return + bottomAnchorSessionKey = key + if (!props.shouldAnchorBottom()) return + virtualizer.scrollToIndex(timelineRowKeys().length - 1, { align: "end" }) + } + + createEffect(maybeAnchorBottom) createEffect( on( @@ -1669,6 +1684,7 @@ export function MessageTimeline(props: { From 44c0ec7847963d2757001dec418eb2e1d16f10cb Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Wed, 13 May 2026 13:54:07 +1000 Subject: [PATCH 2/2] fix(app): simplify timeline bottom anchor --- packages/app/src/pages/session/message-timeline.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 6ef62ea22b..f8134976a8 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -721,14 +721,13 @@ export function MessageTimeline(props: { const key = sessionKey() if (bottomAnchorSessionKey === key) return if (!virtualizer) return - if (timelineRowKeys().length === 0) return + const keys = timelineRowKeys() + if (keys.length === 0) return bottomAnchorSessionKey = key if (!props.shouldAnchorBottom()) return - virtualizer.scrollToIndex(timelineRowKeys().length - 1, { align: "end" }) + virtualizer.scrollToIndex(keys.length - 1, { align: "end" }) } - createEffect(maybeAnchorBottom) - createEffect( on( () => [sessionKey(), timelineRowKeys()] as const, @@ -739,6 +738,7 @@ export function MessageTimeline(props: { if (virtualizer) { virtualizerSessionKey = cacheSessionKey virtualizerRowKeys = cacheRowKeys + maybeAnchorBottom() } }, { defer: true },