mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-12 03:42:08 +00:00
fix(tui): steady transcript scrollbar (#20917)
* fix(tui): steady transcript scrollbar Keep the visible scrollbar tied to committed viewport position while virtual history can still prefetch against pending scroll targets, and preserve drag grab offset synchronously for native-feeling scrollbar drags. * fix(tui): smooth precision wheel scroll Replace the opt-scroll throttle with frame-sized coalescing so modifier wheel gestures stay line-precise without stepping.
This commit is contained in:
parent
53a024994a
commit
5ccab51fa8
6 changed files with 196 additions and 34 deletions
48
ui-tui/src/lib/precisionWheel.ts
Normal file
48
ui-tui/src/lib/precisionWheel.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const PRECISION_WHEEL_FRAME_MS = 16
|
||||
const PRECISION_WHEEL_STICKY_MS = 80
|
||||
|
||||
export type PrecisionWheelState = {
|
||||
active: boolean
|
||||
dir: 0 | -1 | 1
|
||||
lastEventAtMs: number
|
||||
lastScrollAtMs: number
|
||||
}
|
||||
|
||||
export type PrecisionWheelStep = {
|
||||
active: boolean
|
||||
entered: boolean
|
||||
rows: 0 | 1
|
||||
}
|
||||
|
||||
export function initPrecisionWheel(): PrecisionWheelState {
|
||||
return { active: false, dir: 0, lastEventAtMs: 0, lastScrollAtMs: 0 }
|
||||
}
|
||||
|
||||
export function computePrecisionWheelStep(
|
||||
state: PrecisionWheelState,
|
||||
dir: -1 | 1,
|
||||
hasModifier: boolean,
|
||||
now: number
|
||||
): PrecisionWheelStep {
|
||||
const active = hasModifier || now - state.lastEventAtMs < PRECISION_WHEEL_STICKY_MS
|
||||
|
||||
if (!active) {
|
||||
state.active = false
|
||||
|
||||
return { active: false, entered: false, rows: 0 }
|
||||
}
|
||||
|
||||
const entered = !state.active
|
||||
|
||||
state.active = true
|
||||
state.lastEventAtMs = now
|
||||
|
||||
if (dir === state.dir && now - state.lastScrollAtMs < PRECISION_WHEEL_FRAME_MS) {
|
||||
return { active: true, entered, rows: 0 }
|
||||
}
|
||||
|
||||
state.dir = dir
|
||||
state.lastScrollAtMs = now
|
||||
|
||||
return { active: true, entered, rows: 1 }
|
||||
}
|
||||
|
|
@ -11,6 +11,12 @@ export interface ViewportSnapshot {
|
|||
viewportHeight: number
|
||||
}
|
||||
|
||||
export interface ScrollbarSnapshot {
|
||||
scrollHeight: number
|
||||
top: number
|
||||
viewportHeight: number
|
||||
}
|
||||
|
||||
const EMPTY: ViewportSnapshot = {
|
||||
atBottom: true,
|
||||
bottom: 0,
|
||||
|
|
@ -20,6 +26,12 @@ const EMPTY: ViewportSnapshot = {
|
|||
viewportHeight: 0
|
||||
}
|
||||
|
||||
const EMPTY_SCROLLBAR: ScrollbarSnapshot = {
|
||||
scrollHeight: 0,
|
||||
top: 0,
|
||||
viewportHeight: 0
|
||||
}
|
||||
|
||||
export function getViewportSnapshot(s?: ScrollBoxHandle | null): ViewportSnapshot {
|
||||
if (!s) {
|
||||
return EMPTY
|
||||
|
|
@ -52,6 +64,26 @@ export function viewportSnapshotKey(v: ViewportSnapshot) {
|
|||
return `${v.atBottom ? 1 : 0}:${Math.ceil(v.top / 8) * 8}:${v.viewportHeight}:${Math.ceil(v.scrollHeight / 8) * 8}:${v.pending}`
|
||||
}
|
||||
|
||||
export function getScrollbarSnapshot(s?: ScrollBoxHandle | null): ScrollbarSnapshot {
|
||||
if (!s) {
|
||||
return EMPTY_SCROLLBAR
|
||||
}
|
||||
|
||||
const viewportHeight = Math.max(0, s.getViewportHeight())
|
||||
const scrollHeight = Math.max(viewportHeight, s.getScrollHeight())
|
||||
const maxTop = Math.max(0, scrollHeight - viewportHeight)
|
||||
|
||||
return {
|
||||
scrollHeight,
|
||||
top: Math.max(0, Math.min(maxTop, s.getScrollTop())),
|
||||
viewportHeight
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollbarSnapshotKey(v: ScrollbarSnapshot) {
|
||||
return `${v.top}:${v.viewportHeight}:${v.scrollHeight}`
|
||||
}
|
||||
|
||||
export function useViewportSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>): ViewportSnapshot {
|
||||
const key = useSyncExternalStore(
|
||||
useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
|
||||
|
|
@ -72,3 +104,21 @@ export function useViewportSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>
|
|||
}
|
||||
}, [key])
|
||||
}
|
||||
|
||||
export function useScrollbarSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>): ScrollbarSnapshot {
|
||||
const key = useSyncExternalStore(
|
||||
useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
|
||||
() => scrollbarSnapshotKey(getScrollbarSnapshot(scrollRef.current)),
|
||||
() => scrollbarSnapshotKey(EMPTY_SCROLLBAR)
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
const [top = '0', viewportHeight = '0', scrollHeight = '0'] = key.split(':')
|
||||
|
||||
return {
|
||||
scrollHeight: Number(scrollHeight),
|
||||
top: Number(top),
|
||||
viewportHeight: Number(viewportHeight)
|
||||
}
|
||||
}, [key])
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue