fix(ui-tui): clear sticky prompt when follow snaps to bottom

Renderer-driven follow-to-bottom was restoring the viewport to the tail without notifying ScrollBox subscribers, so StickyPromptTracker could stay stale-visible. Notify on render-time scroll/sticky changes and treat near-bottom as bottom for prompt hiding.
This commit is contained in:
Brooklyn Nicholson 2026-04-23 14:19:32 -05:00
parent c8ff70fe03
commit aa47812edf
4 changed files with 15 additions and 2 deletions

View file

@ -257,6 +257,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
if (el) {
el.scrollTop ??= 0
el.notifyScrollChange = notify
}
}}
style={{

View file

@ -72,6 +72,7 @@ export type DOMElement = {
scrollViewportHeight?: number
scrollViewportTop?: number
stickyScroll?: boolean
notifyScrollChange?: () => void
// Set by ScrollBox.scrollToElement; render-node-to-output reads
// el.yogaNode.getComputedTop() (FRESH — same Yoga pass as scrollHeight)
// and sets scrollTop = top + offset, then clears this. Unlike an

View file

@ -761,6 +761,7 @@ function renderNodeToOutput(
// active text selection by the same delta (native terminal behavior:
// view keeps scrolling, highlight walks up with the text).
const scrollTopBeforeFollow = node.scrollTop ?? 0
const stickyBeforeFollow = node.stickyScroll
const sticky = node.stickyScroll ?? Boolean(node.attributes['stickyScroll'])
@ -863,6 +864,10 @@ function renderNodeToOutput(
scrollDrainNode = node
}
if ((node.scrollTop ?? 0) !== scrollTopBeforeFollow || node.stickyScroll !== stickyBeforeFollow) {
node.notifyScrollChange?.()
}
scrollTop = clamped
if (content && contentYoga) {

View file

@ -256,15 +256,21 @@ export function StickyPromptTracker({ messages, offsets, scrollRef, onChange }:
}
const top = Math.max(0, s.getScrollTop() + s.getPendingDelta())
const vp = Math.max(0, s.getViewportHeight())
const total = Math.max(vp, s.getScrollHeight())
const atBottom = s.isSticky() || top + vp >= total - 2
return s.isSticky() ? -1 - top : top
return atBottom ? -1 - top : top
},
() => NaN
)
const s = scrollRef.current
const top = Math.max(0, (s?.getScrollTop() ?? 0) + (s?.getPendingDelta() ?? 0))
const text = stickyPromptFromViewport(messages, offsets, top, s?.isSticky() ?? true)
const vp = Math.max(0, s?.getViewportHeight() ?? 0)
const total = Math.max(vp, s?.getScrollHeight() ?? vp)
const atBottom = (s?.isSticky() ?? true) || top + vp >= total - 2
const text = stickyPromptFromViewport(messages, offsets, top, atBottom)
useEffect(() => onChange(text), [onChange, text])