mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
perf(tui): stabilize long-session scrolling
This commit is contained in:
parent
59b56d445c
commit
db4e4acca0
10 changed files with 195 additions and 105 deletions
62
ui-tui/src/lib/inputMetrics.ts
Normal file
62
ui-tui/src/lib/inputMetrics.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { stringWidth } from '@hermes/ink'
|
||||
|
||||
let _seg: Intl.Segmenter | null = null
|
||||
const seg = () => (_seg ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' }))
|
||||
|
||||
/**
|
||||
* Mirrors the char-wrap behavior used by the composer TextInput.
|
||||
* Returns the zero-based visual line and column of the cursor cell.
|
||||
*/
|
||||
export function cursorLayout(value: string, cursor: number, cols: number) {
|
||||
const pos = Math.max(0, Math.min(cursor, value.length))
|
||||
const w = Math.max(1, cols)
|
||||
|
||||
let col = 0,
|
||||
line = 0
|
||||
|
||||
for (const { segment, index } of seg().segment(value)) {
|
||||
if (index >= pos) {
|
||||
break
|
||||
}
|
||||
|
||||
if (segment === '\n') {
|
||||
line++
|
||||
col = 0
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const sw = stringWidth(segment)
|
||||
|
||||
if (!sw) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (col + sw > w) {
|
||||
line++
|
||||
col = 0
|
||||
}
|
||||
|
||||
col += sw
|
||||
}
|
||||
|
||||
// trailing cursor-cell overflows to the next row at the wrap column
|
||||
if (col >= w) {
|
||||
line++
|
||||
col = 0
|
||||
}
|
||||
|
||||
return { column: col, line }
|
||||
}
|
||||
|
||||
export function inputVisualHeight(value: string, columns: number) {
|
||||
return cursorLayout(value, value.length, columns).line + 1
|
||||
}
|
||||
|
||||
export function stableComposerColumns(totalCols: number, promptWidth: number) {
|
||||
// totalCols is the terminal width. Reserve:
|
||||
// - outer composer paddingX={1}: 2 columns
|
||||
// - transcript scrollbar gutter + marginLeft: 2 columns
|
||||
// - prompt prefix width
|
||||
return Math.max(20, totalCols - promptWidth - 4)
|
||||
}
|
||||
59
ui-tui/src/lib/viewportStore.ts
Normal file
59
ui-tui/src/lib/viewportStore.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { RefObject } from 'react'
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
|
||||
import type { ScrollBoxHandle } from '@hermes/ink'
|
||||
|
||||
export interface ViewportSnapshot {
|
||||
atBottom: boolean
|
||||
bottom: number
|
||||
pending: number
|
||||
scrollHeight: number
|
||||
top: number
|
||||
viewportHeight: number
|
||||
}
|
||||
|
||||
const EMPTY: ViewportSnapshot = {
|
||||
atBottom: true,
|
||||
bottom: 0,
|
||||
pending: 0,
|
||||
scrollHeight: 0,
|
||||
top: 0,
|
||||
viewportHeight: 0
|
||||
}
|
||||
|
||||
export function getViewportSnapshot(s?: ScrollBoxHandle | null): ViewportSnapshot {
|
||||
if (!s) {
|
||||
return EMPTY
|
||||
}
|
||||
|
||||
const pending = s.getPendingDelta()
|
||||
const top = Math.max(0, s.getScrollTop() + pending)
|
||||
const viewportHeight = Math.max(0, s.getViewportHeight())
|
||||
const scrollHeight = Math.max(viewportHeight, s.getScrollHeight())
|
||||
const bottom = top + viewportHeight
|
||||
|
||||
return {
|
||||
atBottom: s.isSticky() || bottom >= scrollHeight - 2,
|
||||
bottom,
|
||||
pending,
|
||||
scrollHeight,
|
||||
top,
|
||||
viewportHeight
|
||||
}
|
||||
}
|
||||
|
||||
export function viewportSnapshotKey(v: ViewportSnapshot) {
|
||||
return `${v.atBottom ? 1 : 0}:${v.top}:${v.viewportHeight}:${v.scrollHeight}:${v.pending}`
|
||||
}
|
||||
|
||||
export function useViewportSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>): ViewportSnapshot {
|
||||
const key = useSyncExternalStore(
|
||||
useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
|
||||
() => viewportSnapshotKey(getViewportSnapshot(scrollRef.current)),
|
||||
() => viewportSnapshotKey(EMPTY)
|
||||
)
|
||||
|
||||
void key
|
||||
|
||||
return getViewportSnapshot(scrollRef.current)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue