mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
Merge pull request #40433 from xxxigm/fix/desktop-chat-autoscroll
fix(desktop): stop chat transcript from jumping/flickering while reading (#37549)
This commit is contained in:
parent
69a293b419
commit
c50fb560ef
2 changed files with 56 additions and 2 deletions
|
|
@ -9,6 +9,28 @@ import { $busy, $messages, noteSessionActivity, setSessionAttention, setSessionW
|
|||
|
||||
import type { ClientSessionState } from '../../types'
|
||||
|
||||
// Shallow per-message identity check. When a flush carries no transcript
|
||||
// changes, `preserveLocalAssistantErrors` returns the same message objects in
|
||||
// the same order, so reference equality per slot is enough to detect "nothing
|
||||
// to publish" and avoid a needless `$messages` churn.
|
||||
function sameMessageList(a: ChatMessage[], b: ChatMessage[]): boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let index = 0; index < a.length; index += 1) {
|
||||
if (a[index] !== b[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
interface SessionStateCacheOptions {
|
||||
activeSessionId: string | null
|
||||
busyRef: MutableRefObject<boolean>
|
||||
|
|
@ -88,7 +110,20 @@ export function useSessionStateCache({
|
|||
return
|
||||
}
|
||||
|
||||
setMessages(preserveLocalAssistantErrors(pending.state.messages, $messages.get()))
|
||||
// `preserveLocalAssistantErrors` always returns a fresh array, so publishing
|
||||
// it unconditionally puts a new `$messages` reference on the store every
|
||||
// flush — including the periodic `session.info` heartbeats that don't touch
|
||||
// the transcript. That churns ChatView → runtimeMessageRepository → the
|
||||
// assistant-ui runtime → the virtualizer, which re-measures and visibly
|
||||
// jerks the scroll position while the user is reading. Skip the publish when
|
||||
// the merged result is content-identical to what's already on screen.
|
||||
const currentMessages = $messages.get()
|
||||
const nextMessages = preserveLocalAssistantErrors(pending.state.messages, currentMessages)
|
||||
|
||||
if (!sameMessageList(nextMessages, currentMessages)) {
|
||||
setMessages(nextMessages)
|
||||
}
|
||||
|
||||
setBusy(pending.state.busy)
|
||||
setMutableRef(busyRef, pending.state.busy)
|
||||
setAwaitingResponse(pending.state.awaitingResponse)
|
||||
|
|
|
|||
|
|
@ -264,8 +264,27 @@ function useThreadScrollAnchor({
|
|||
return
|
||||
}
|
||||
|
||||
// Already parked at the bottom: writing `scrollTop` is a no-op and the
|
||||
// browser fires NO scroll event, so arming the programmatic gate here would
|
||||
// leave it permanently set. Repeated pins (streaming heartbeats, the
|
||||
// post-run lock loop) then accumulate the gate, and the next genuine user
|
||||
// scroll-up is misread as one of our programmatic scrolls — re-arming
|
||||
// sticky-bottom and yanking the viewport back down. Refresh trackers, bail.
|
||||
const distFromBottom = el.scrollHeight - (el.scrollTop + el.clientHeight)
|
||||
|
||||
if (distFromBottom <= AT_BOTTOM_THRESHOLD) {
|
||||
lastTopRef.current = el.scrollTop
|
||||
lastHeightRef.current = el.scrollHeight
|
||||
lastClientHeightRef.current = el.clientHeight
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Hold the disarm gate across the scroll event the next line will fire.
|
||||
programmaticScrollPendingRef.current += 1
|
||||
// Set to 1 rather than incrementing: coalesced writes within a frame fire a
|
||||
// single scroll event, so a counter > 1 can never drain and would swallow a
|
||||
// later real user scroll.
|
||||
programmaticScrollPendingRef.current = 1
|
||||
scrollElementToBottom(el)
|
||||
lastTopRef.current = el.scrollTop
|
||||
lastHeightRef.current = el.scrollHeight
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue