mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(tui): recompute virtual tail after width resize
Avoid preserving a frozen virtual transcript range when wrapped rows shrink enough that the old tail window no longer covers the viewport.
This commit is contained in:
parent
521c870a05
commit
2a75bec607
2 changed files with 68 additions and 5 deletions
|
|
@ -8,6 +8,7 @@ import { useVirtualHistory, virtualHistorySnapshotKey } from '../hooks/useVirtua
|
|||
|
||||
interface Item {
|
||||
height: number
|
||||
heightAfterResize?: number
|
||||
key: string
|
||||
}
|
||||
|
||||
|
|
@ -49,12 +50,17 @@ const viewportIsMounted = (items: readonly Item[], virtualHistory: ReturnType<ty
|
|||
return top >= span.top && bottom <= span.bottom
|
||||
}
|
||||
|
||||
const itemHeightForColumns = (item: Item | undefined, columns: number) =>
|
||||
columns >= 80 ? (item?.heightAfterResize ?? item?.height ?? 1) : (item?.height ?? 1)
|
||||
|
||||
function Harness({
|
||||
columns = 80,
|
||||
expose,
|
||||
height = 10,
|
||||
items,
|
||||
maxMounted = 16
|
||||
}: {
|
||||
columns?: number
|
||||
expose: React.MutableRefObject<Exposed | null>
|
||||
height?: number
|
||||
items: readonly Item[]
|
||||
|
|
@ -62,9 +68,9 @@ function Harness({
|
|||
}) {
|
||||
const scrollRef = useRef<ScrollBoxHandle | null>(null)
|
||||
|
||||
const virtualHistory = useVirtualHistory(scrollRef, items, 80, {
|
||||
const virtualHistory = useVirtualHistory(scrollRef, items, columns, {
|
||||
coldStartCount: 16,
|
||||
estimateHeight: index => items[index]?.height ?? 1,
|
||||
estimateHeight: index => itemHeightForColumns(items[index], columns),
|
||||
maxMounted,
|
||||
overscan: 2
|
||||
})
|
||||
|
|
@ -85,7 +91,11 @@ function Harness({
|
|||
.map(item =>
|
||||
React.createElement(
|
||||
Box,
|
||||
{ height: item.height, key: item.key, ref: virtualHistory.measureRef(item.key) },
|
||||
{
|
||||
height: itemHeightForColumns(item, columns),
|
||||
key: item.key,
|
||||
ref: virtualHistory.measureRef(item.key)
|
||||
},
|
||||
React.createElement(Text, null, item.key)
|
||||
)
|
||||
),
|
||||
|
|
@ -139,6 +149,40 @@ describe('useVirtualHistory offset cache reuse', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('recomputes tail coverage when wrapped rows shrink after a width resize', async () => {
|
||||
const items = Array.from({ length: 100 }, (_, index) => ({
|
||||
height: 4,
|
||||
heightAfterResize: 1,
|
||||
key: `item-${index}`
|
||||
}))
|
||||
|
||||
const expose = { current: null as Exposed | null }
|
||||
const streams = makeStreams()
|
||||
|
||||
const instance = renderSync(
|
||||
React.createElement(Harness, { columns: 40, expose, height: 10, items, maxMounted: 80 }),
|
||||
{
|
||||
patchConsole: false,
|
||||
stderr: streams.stderr as NodeJS.WriteStream,
|
||||
stdin: streams.stdin as NodeJS.ReadStream,
|
||||
stdout: streams.stdout as NodeJS.WriteStream
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
await delay(20)
|
||||
instance.rerender(React.createElement(Harness, { columns: 80, expose, height: 10, items, maxMounted: 80 }))
|
||||
await delay(80)
|
||||
|
||||
const resizedItems = items.map(item => ({ height: item.heightAfterResize!, key: item.key }))
|
||||
|
||||
expect(viewportIsMounted(resizedItems, expose.current!.virtualHistory, expose.current!.scroll!)).toBe(true)
|
||||
} finally {
|
||||
instance.unmount()
|
||||
instance.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('recomputes offsets after a mounted row height changes', async () => {
|
||||
const tall = [
|
||||
{ height: 6, key: 'a' },
|
||||
|
|
|
|||
|
|
@ -248,8 +248,26 @@ export function useVirtualHistory(
|
|||
// During a freeze, drop the frozen range if items shrank past its start
|
||||
// (/clear, compaction) — clamping would collapse to an empty mount and
|
||||
// flash blank. Fall through to the normal path in that case.
|
||||
const frozenRange =
|
||||
freezeRenders.current > 0 && prevRange.current && prevRange.current[0] < n ? prevRange.current : null
|
||||
const frozenRangeCandidate =
|
||||
freezeRenders.current > 0 && prevRange.current && prevRange.current[0] < n
|
||||
? ([prevRange.current[0], Math.min(prevRange.current[1], n)] as const)
|
||||
: null
|
||||
|
||||
// Width grows can shrink wrapped rows enough that the old tail window no
|
||||
// longer covers the viewport. In that case freezing preserves stale spacers
|
||||
// and visually cuts off the last message, so recompute immediately.
|
||||
const frozenRange = (() => {
|
||||
if (!frozenRangeCandidate || vp <= 0) {
|
||||
return frozenRangeCandidate
|
||||
}
|
||||
|
||||
const visibleTop = sticky && !recentManual ? Math.max(0, total - vp) : target
|
||||
const visibleBottom = visibleTop + vp
|
||||
const rangeTop = offsets[frozenRangeCandidate[0]] ?? 0
|
||||
const rangeBottom = offsets[frozenRangeCandidate[1]] ?? total
|
||||
|
||||
return rangeTop <= visibleTop && rangeBottom >= visibleBottom ? frozenRangeCandidate : null
|
||||
})()
|
||||
|
||||
let start = 0
|
||||
let end = n
|
||||
|
|
@ -464,6 +482,7 @@ export function useVirtualHistory(
|
|||
|
||||
if (skipMeasurement.current) {
|
||||
skipMeasurement.current = false
|
||||
bumpMeasuredHeightVersion(n => n + 1)
|
||||
} else {
|
||||
for (let i = effStart; i < effEnd; i++) {
|
||||
const k = items[i]?.key
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue