Fix scrollback snapping back to bottom after scrolling up

Wheel-up now clears stickToBottom immediately on the raw scroll event, wheel-down re-checks the resting position, and the auto-scroll effect re-checks stickToBottom right before scrollToItem(0). The scroll-state effect could miss a wheel scroll that began and ended within one frame, so the next incoming line pulled the buffer back to the bottom.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW57ZvxRmxcDF7AFACLSoV
This commit is contained in:
2026-10-29 18:09:13 +00:00
co-authored by Claude Sonnet 5.5
parent ed5d9a9c78
commit fff75700ac
+26 -1
View File
@@ -818,7 +818,7 @@ fun MessageScrollback(
.first { item -> .first { item ->
item == null || (item.index < capturedList.size && item.key == capturedList[item.index].seq) item == null || (item.index < capturedList.size && item.key == capturedList[item.index].seq)
} }
listState.scrollToItem(0) if (stickToBottom) listState.scrollToItem(0)
} }
} }
@@ -923,6 +923,31 @@ fun MessageScrollback(
.height(snappedHeight) .height(snappedHeight)
.onGloballyPositioned { containerCoords[0] = it } .onGloballyPositioned { containerCoords[0] = it }
.pointerHoverIcon(PointerIcon.Text) .pointerHoverIcon(PointerIcon.Text)
// Wheel-up means the user is reading backscroll: stop sticking to the bottom RIGHT NOW,
// on the raw wheel event. The scroll-state effect up top can miss a wheel scroll that starts
// and finishes inside one frame (isScrollInProgress is never seen as true), which left
// stickToBottom set and let the next incoming line yank the buffer back down.
// Wheel-down re-checks the resting position shortly after and re-enables sticking if the
// user is back at the bottom.
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
if (event.type == PointerEventType.Scroll) {
val dy = event.changes.firstOrNull()?.scrollDelta?.y ?: 0f
if (dy < 0f) {
stickToBottom = false
} else if (dy > 0f) {
coroutineScope.launch {
delay(200)
stickToBottom = listState.firstVisibleItemIndex == 0 &&
listState.firstVisibleItemScrollOffset <= stickToBottomThresholdPx
}
}
}
}
}
}
// Remembers the row under a right-click for the menu's "Copy line". Watches the // Remembers the row under a right-click for the menu's "Copy line". Watches the
// Initial pass, because ContextMenuArea takes the right-press there, before // Initial pass, because ContextMenuArea takes the right-press there, before
// the handler below would see it. // the handler below would see it.