Merge pull request #4 from Hona/fix/virtual-timeline-anchor-1778643000

fix(app): anchor virtual timeline to bottom
This commit is contained in:
Luke Parker
2026-05-13 13:57:59 +10:00
committed by GitHub
2 changed files with 22 additions and 1 deletions

View File

@@ -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

View File

@@ -107,6 +107,7 @@ function sameKeys(a: readonly string[] | undefined, b: readonly string[] | undef
}
const timelineCacheLimit = 16
const timelineFallbackItemSize = 60
const timelineCache = new Map<string, { keys: readonly string[]; cache: VirtualizerHandle["cache"] }>()
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,18 @@ 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
const keys = timelineRowKeys()
if (keys.length === 0) return
bottomAnchorSessionKey = key
if (!props.shouldAnchorBottom()) return
virtualizer.scrollToIndex(keys.length - 1, { align: "end" })
}
createEffect(
on(
@@ -724,6 +738,7 @@ export function MessageTimeline(props: {
if (virtualizer) {
virtualizerSessionKey = cacheSessionKey
virtualizerRowKeys = cacheRowKeys
maybeAnchorBottom()
}
},
{ defer: true },
@@ -1669,6 +1684,7 @@ export function MessageTimeline(props: {
<Virtualizer
data={timelineRowKeys()}
cache={virtualCache()}
itemSize={virtualCache() ? undefined : timelineFallbackItemSize}
scrollRef={root()}
shift={props.historyShift}
keepMounted={keepMounted()}
@@ -1681,6 +1697,7 @@ export function MessageTimeline(props: {
virtualizer = handle
virtualizerSessionKey = cacheSessionKey
virtualizerRowKeys = cacheRowKeys
maybeAnchorBottom()
scheduleContentRoot(root())
}}
>