mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-13 03:52:00 +00:00
fix(tui): refresh scroll height at cached bottom
This commit is contained in:
parent
5b24c0fa85
commit
2c14d3b9b0
2 changed files with 63 additions and 3 deletions
|
|
@ -3,9 +3,12 @@ import { describe, expect, it, vi } from 'vitest'
|
||||||
import { scrollWithSelectionBy } from '../app/scroll.js'
|
import { scrollWithSelectionBy } from '../app/scroll.js'
|
||||||
|
|
||||||
function makeScroll(overrides: Partial<Record<string, unknown>> = {}) {
|
function makeScroll(overrides: Partial<Record<string, unknown>> = {}) {
|
||||||
|
const getScrollHeight = (overrides.getScrollHeight as (() => number) | undefined) ?? vi.fn(() => 100)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
getFreshScrollHeight: vi.fn(() => getScrollHeight()),
|
||||||
getPendingDelta: vi.fn(() => 0),
|
getPendingDelta: vi.fn(() => 0),
|
||||||
getScrollHeight: vi.fn(() => 100),
|
getScrollHeight,
|
||||||
getScrollTop: vi.fn(() => 10),
|
getScrollTop: vi.fn(() => 10),
|
||||||
getViewportHeight: vi.fn(() => 20),
|
getViewportHeight: vi.fn(() => 20),
|
||||||
getViewportTop: vi.fn(() => 0),
|
getViewportTop: vi.fn(() => 0),
|
||||||
|
|
@ -34,6 +37,47 @@ describe('scrollWithSelectionBy', () => {
|
||||||
expect(s.scrollBy).toHaveBeenCalledWith(1)
|
expect(s.scrollBy).toHaveBeenCalledWith(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses fresh scroll height when cached height would swallow a down-scroll at a fake bottom', () => {
|
||||||
|
const s = makeScroll({
|
||||||
|
getFreshScrollHeight: vi.fn(() => 34),
|
||||||
|
getScrollHeight: vi.fn(() => 30),
|
||||||
|
getScrollTop: vi.fn(() => 10),
|
||||||
|
getViewportHeight: vi.fn(() => 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selection = {
|
||||||
|
captureScrolledRows: vi.fn(),
|
||||||
|
getState: vi.fn(() => null),
|
||||||
|
shiftAnchor: vi.fn(),
|
||||||
|
shiftSelection: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollWithSelectionBy(10, { scrollRef: { current: s as never }, selection })
|
||||||
|
|
||||||
|
expect(s.scrollBy).toHaveBeenCalledWith(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses fresh height when pending down-scroll reaches the cached fake bottom', () => {
|
||||||
|
const s = makeScroll({
|
||||||
|
getFreshScrollHeight: vi.fn(() => 38),
|
||||||
|
getPendingDelta: vi.fn(() => 2),
|
||||||
|
getScrollHeight: vi.fn(() => 32),
|
||||||
|
getScrollTop: vi.fn(() => 10),
|
||||||
|
getViewportHeight: vi.fn(() => 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selection = {
|
||||||
|
captureScrolledRows: vi.fn(),
|
||||||
|
getState: vi.fn(() => null),
|
||||||
|
shiftAnchor: vi.fn(),
|
||||||
|
shiftSelection: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollWithSelectionBy(10, { scrollRef: { current: s as never }, selection })
|
||||||
|
|
||||||
|
expect(s.scrollBy).toHaveBeenCalledWith(6)
|
||||||
|
})
|
||||||
|
|
||||||
it('does nothing at the edge instead of queueing dead pending deltas', () => {
|
it('does nothing at the edge instead of queueing dead pending deltas', () => {
|
||||||
const s = makeScroll({
|
const s = makeScroll({
|
||||||
getScrollHeight: vi.fn(() => 30),
|
getScrollHeight: vi.fn(() => 30),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,23 @@ export interface ScrollWithSelectionOptions {
|
||||||
readonly selection: SelectionApi
|
readonly selection: SelectionApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollBoundsForDelta(s: ScrollBoxHandle, cur: number, delta: number) {
|
||||||
|
const viewport = Math.max(0, s.getViewportHeight())
|
||||||
|
const cachedHeight = Math.max(viewport, s.getScrollHeight())
|
||||||
|
let max = Math.max(0, cachedHeight - viewport)
|
||||||
|
|
||||||
|
// getScrollHeight() is render-time cached. After the streaming tail is
|
||||||
|
// committed into virtual history, the Yoga height can be fresher than the
|
||||||
|
// cached value; if we clamp only against the cached fake bottom, wheel-down
|
||||||
|
// becomes a no-op and no render is scheduled to reveal the real tail.
|
||||||
|
if (delta > 0 && cur + delta >= max - 1) {
|
||||||
|
const freshHeight = Math.max(viewport, s.getFreshScrollHeight())
|
||||||
|
max = Math.max(0, freshHeight - viewport)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { max, viewport }
|
||||||
|
}
|
||||||
|
|
||||||
export function scrollWithSelectionBy(delta: number, { scrollRef, selection }: ScrollWithSelectionOptions): void {
|
export function scrollWithSelectionBy(delta: number, { scrollRef, selection }: ScrollWithSelectionOptions): void {
|
||||||
const s = scrollRef.current
|
const s = scrollRef.current
|
||||||
|
|
||||||
|
|
@ -21,8 +38,7 @@ export function scrollWithSelectionBy(delta: number, { scrollRef, selection }: S
|
||||||
}
|
}
|
||||||
|
|
||||||
const cur = s.getScrollTop() + s.getPendingDelta()
|
const cur = s.getScrollTop() + s.getPendingDelta()
|
||||||
const viewport = Math.max(0, s.getViewportHeight())
|
const { max, viewport } = scrollBoundsForDelta(s, cur, delta)
|
||||||
const max = Math.max(0, s.getScrollHeight() - viewport)
|
|
||||||
const actual = Math.max(0, Math.min(max, cur + delta)) - cur
|
const actual = Math.max(0, Math.min(max, cur + delta)) - cur
|
||||||
|
|
||||||
if (actual === 0) {
|
if (actual === 0) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue