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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user