mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(vscode,ui): keep user scroll position while session is busy (#9236)
* fix(vscode,ui): keep user scroll position while session is busy Virtua's measurement-driven resize events race ahead of the debounced user-scroll detection in createAutoScroll, snapping the viewport back to the bottom while the user is mid-gesture. The QuestionDock's focus call on mount also triggers the browser's focus-into-view behavior, which yanks the view down whenever the user has scrolled up. Fixes #9198 * fix(ui): guard recentlyInteracted against initial lastInteraction=0
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix chat scroll jumping back to the bottom while the session is busy — you can now scroll up to read earlier context while a response is streaming or while a question is waiting for your answer.
|
||||
@@ -3,6 +3,11 @@ import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
|
||||
const DEBOUNCE_MS = 100
|
||||
// Grace window after a real user interaction (wheel/pointer/key/touch) during
|
||||
// which a ResizeObserver or non-user scroll event must not snap the view back
|
||||
// to the bottom. Long enough to cover a single scroll gesture plus the
|
||||
// DEBOUNCE_MS window used by handleScroll to flip userScrolled.
|
||||
const USER_INTERACTION_GRACE_MS = 300
|
||||
|
||||
export interface AutoScrollOptions {
|
||||
working: () => boolean
|
||||
@@ -18,6 +23,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
let cleanup: (() => void) | undefined
|
||||
let userInitiated = false
|
||||
let lastScrollTop: number | undefined
|
||||
let lastInteraction = 0
|
||||
|
||||
const threshold = () => options.bottomThreshold ?? 10
|
||||
|
||||
@@ -43,8 +49,12 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
if (scroll && nested && nested !== scroll) return
|
||||
}
|
||||
userInitiated = true
|
||||
lastInteraction = performance.now()
|
||||
}
|
||||
|
||||
const recentlyInteracted = () =>
|
||||
lastInteraction > 0 && performance.now() - lastInteraction < USER_INTERACTION_GRACE_MS
|
||||
|
||||
const scrollToBottomNow = (behavior: ScrollBehavior) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
@@ -119,7 +129,11 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
}
|
||||
|
||||
if (!store.userScrolled && !byUser) {
|
||||
if (el.scrollTop < (lastScrollTop ?? el.scrollTop)) {
|
||||
// virtua fires programmatic scroll events as it measures virtualized
|
||||
// items. Don't let those snap the view back to the bottom while the
|
||||
// user is mid-gesture — the wheel event fires before the scroll event,
|
||||
// so `recentlyInteracted()` is reliable here.
|
||||
if (el.scrollTop < (lastScrollTop ?? el.scrollTop) || recentlyInteracted()) {
|
||||
stop()
|
||||
} else {
|
||||
scrollToBottomNow("auto")
|
||||
@@ -163,6 +177,15 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
if (store.userScrolled) {
|
||||
return
|
||||
}
|
||||
// Virtualized lists (virtua) re-measure items during user scroll, firing
|
||||
// resize events that race ahead of handleScroll's DEBOUNCE_MS window.
|
||||
// If the user just interacted with the scroller and is no longer near
|
||||
// the bottom, treat the resize as a layout reflow on top of their
|
||||
// scroll — pause auto-follow instead of snapping back to the bottom.
|
||||
if (el && recentlyInteracted() && distanceFromBottom(el) > threshold()) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
// ResizeObserver fires after layout, before paint.
|
||||
// Keep the bottom locked in the same frame to avoid visible
|
||||
// "jump up then catch up" artifacts while streaming content.
|
||||
|
||||
@@ -247,13 +247,16 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
|
||||
// Keep keyboard navigation when the webview already has focus, but do not
|
||||
// steal focus from the editor, terminal, or other VS Code surfaces.
|
||||
// preventScroll avoids the browser's focus-into-view behavior fighting
|
||||
// createAutoScroll (and yanking the viewport back to the dock while the
|
||||
// user has scrolled up to read earlier context).
|
||||
createEffect(() => {
|
||||
void store.tab
|
||||
if (store.collapsed || store.editing || confirm()) return
|
||||
requestAnimationFrame(() => {
|
||||
if (!document.hasFocus()) return
|
||||
const btn = root?.querySelector<HTMLButtonElement>("button[data-slot='question-option']:not(:disabled)")
|
||||
btn?.focus()
|
||||
btn?.focus({ preventScroll: true })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user