mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(tui): stabilize sticky prompt tracking
Keep the latest prompt sticky while the viewport is in live assistant output beyond history, and clear stale sticky state at the real bottom using fresh scroll height.
This commit is contained in:
parent
afb20a1d67
commit
ce2cc7302e
5 changed files with 62 additions and 7 deletions
|
|
@ -28,4 +28,31 @@ describe('stickyPromptFromViewport', () => {
|
|||
|
||||
expect(stickyPromptFromViewport(messages, offsets, 16, 20, false)).toBe('current prompt')
|
||||
})
|
||||
|
||||
it('shows the last prompt once the viewport starts after the history tail', () => {
|
||||
const messages = [
|
||||
{ role: 'user' as const, text: 'current prompt' },
|
||||
{ role: 'assistant' as const, text: 'completed answer' }
|
||||
]
|
||||
|
||||
expect(stickyPromptFromViewport(messages, [0, 2, 5], 8, 14, false)).toBe('current prompt')
|
||||
})
|
||||
|
||||
it('shows a prompt as soon as its full row is above the viewport', () => {
|
||||
const messages = [
|
||||
{ role: 'user' as const, text: 'current prompt' },
|
||||
{ role: 'assistant' as const, text: 'current answer' }
|
||||
]
|
||||
|
||||
expect(stickyPromptFromViewport(messages, [0, 2, 10], 2, 8, false)).toBe('current prompt')
|
||||
})
|
||||
|
||||
it('hides the sticky prompt at the bottom', () => {
|
||||
const messages = [
|
||||
{ role: 'user' as const, text: 'current prompt' },
|
||||
{ role: 'assistant' as const, text: 'current answer' }
|
||||
]
|
||||
|
||||
expect(stickyPromptFromViewport(messages, [0, 2, 10], 8, 10, true)).toBe('')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -35,4 +35,20 @@ describe('viewportStore', () => {
|
|||
})
|
||||
expect(viewportSnapshotKey(snap)).toBe('0:16:5:40:3')
|
||||
})
|
||||
|
||||
it('uses fresh scroll height to clear stale non-bottom state', () => {
|
||||
const handle = {
|
||||
getFreshScrollHeight: () => 20,
|
||||
getPendingDelta: () => 0,
|
||||
getScrollHeight: () => 40,
|
||||
getScrollTop: () => 15,
|
||||
getViewportHeight: () => 5,
|
||||
isSticky: () => false
|
||||
}
|
||||
|
||||
const snap = getViewportSnapshot(handle as any)
|
||||
|
||||
expect(snap.atBottom).toBe(true)
|
||||
expect(snap.scrollHeight).toBe(20)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,21 +26,25 @@ export const stickyPromptFromViewport = (
|
|||
return ''
|
||||
}
|
||||
|
||||
const first = Math.max(0, Math.min(messages.length - 1, upperBound(offsets, top) - 1))
|
||||
const last = Math.max(first, Math.min(messages.length - 1, upperBound(offsets, bottom) - 1))
|
||||
const first = Math.max(0, upperBound(offsets, top) - 1)
|
||||
const last = Math.max(first, upperBound(offsets, bottom) - 1)
|
||||
const visibleStart = Math.min(messages.length, first)
|
||||
const visibleEnd = Math.min(messages.length - 1, last)
|
||||
|
||||
for (let i = first; i <= last; i++) {
|
||||
for (let i = visibleStart; i <= visibleEnd; i++) {
|
||||
if (messages[i]?.role === 'user') {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = first - 1; i >= 0; i--) {
|
||||
for (let i = Math.min(messages.length - 1, visibleStart - 1); i >= 0; i--) {
|
||||
if (messages[i]?.role !== 'user') {
|
||||
continue
|
||||
}
|
||||
|
||||
return (offsets[i] ?? 0) + 1 < top ? userDisplay(messages[i]!.text.trim()).replace(/\s+/g, ' ').trim() : ''
|
||||
return (offsets[i + 1] ?? (offsets[i] ?? 0) + 1) <= top
|
||||
? userDisplay(messages[i]!.text.trim()).replace(/\s+/g, ' ').trim()
|
||||
: ''
|
||||
}
|
||||
|
||||
return ''
|
||||
|
|
|
|||
|
|
@ -28,11 +28,18 @@ export function getViewportSnapshot(s?: ScrollBoxHandle | null): ViewportSnapsho
|
|||
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 cachedScrollHeight = Math.max(viewportHeight, s.getScrollHeight())
|
||||
let scrollHeight = cachedScrollHeight
|
||||
const bottom = top + viewportHeight
|
||||
let atBottom = s.isSticky() || bottom >= scrollHeight - 2
|
||||
|
||||
if (!atBottom) {
|
||||
scrollHeight = Math.max(viewportHeight, s.getFreshScrollHeight?.() ?? cachedScrollHeight)
|
||||
atBottom = s.isSticky() || bottom >= scrollHeight - 2
|
||||
}
|
||||
|
||||
return {
|
||||
atBottom: s.isSticky() || bottom >= scrollHeight - 2,
|
||||
atBottom,
|
||||
bottom,
|
||||
pending,
|
||||
scrollHeight,
|
||||
|
|
|
|||
1
ui-tui/src/types/hermes-ink.d.ts
vendored
1
ui-tui/src/types/hermes-ink.d.ts
vendored
|
|
@ -83,6 +83,7 @@ declare module '@hermes/ink' {
|
|||
readonly getScrollTop: () => number
|
||||
readonly getPendingDelta: () => number
|
||||
readonly getScrollHeight: () => number
|
||||
readonly getFreshScrollHeight: () => number
|
||||
readonly getViewportHeight: () => number
|
||||
readonly getViewportTop: () => number
|
||||
readonly getLastManualScrollAt: () => number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue