refactor(ui-tui): clean touched resize and sticky prompt paths

Trim comment noise, remove redundant typing, normalize sticky prompt viewport args to top→bottom order, and reuse one sticky viewport helper instead of duplicating the math.
This commit is contained in:
Brooklyn Nicholson 2026-04-23 14:37:00 -05:00
parent 9a885fba31
commit 9bf6e1cd6e
5 changed files with 23 additions and 32 deletions

View File

@ -463,11 +463,8 @@ export default class Ink {
this.resetFramesForAltScreen()
this.needsEraseBeforePaint = true
// Post-resize drift healer: 160ms after the last resize, force one full
// reconcile so Yoga/React catch up to the final viewport and any stale
// terminal cells from host-side reflow get repainted away. Ink upstream
// and ConPTY/xterm reports point to this as a general resize/reflow
// desync class, not an xterm.js-only quirk.
// One last repaint after the resize burst settles closes any host-side
// reflow drift the normal diff path can't see.
this.resizeSettleTimer = setTimeout(() => {
this.resizeSettleTimer = null

View File

@ -13,7 +13,7 @@ describe('stickyPromptFromViewport', () => {
const offsets = [0, 2, 10, 12, 20]
expect(stickyPromptFromViewport(messages, offsets, 16, 8, false)).toBe('')
expect(stickyPromptFromViewport(messages, offsets, 8, 16, false)).toBe('')
})
it('shows the latest user message above the viewport when no user message is visible', () => {
@ -26,6 +26,6 @@ describe('stickyPromptFromViewport', () => {
const offsets = [0, 2, 10, 12, 20]
expect(stickyPromptFromViewport(messages, offsets, 20, 16, false)).toBe('current prompt')
expect(stickyPromptFromViewport(messages, offsets, 16, 20, false)).toBe('current prompt')
})
})

View File

@ -22,7 +22,7 @@ import type { Msg, PanelSection, SlashCatalog } from '../types.js'
import { createGatewayEventHandler } from './createGatewayEventHandler.js'
import { createSlashHandler } from './createSlashHandler.js'
import { type AppLayoutProgressProps, type GatewayRpc, type TranscriptRow } from './interfaces.js'
import { type GatewayRpc, type TranscriptRow } from './interfaces.js'
import { $overlayState, patchOverlayState } from './overlayStore.js'
import { turnController } from './turnController.js'
import { $turnState, patchTurnState } from './turnStore.js'
@ -672,16 +672,11 @@ export function useMainApp(gw: GatewayClient) {
return top + vp >= total - 3
})()
const liveProgress = useMemo<AppLayoutProgressProps>(
() => ({ ...turn, showProgressArea, showStreamingArea: Boolean(turn.streaming) }),
[turn, showProgressArea]
)
const liveProgress = useMemo(() => ({ ...turn, showProgressArea, showStreamingArea: Boolean(turn.streaming) }), [turn, showProgressArea])
const frozenProgressRef = useRef(liveProgress)
// When the live tail is offscreen, freeze its snapshot so scroll work doesn't
// keep rebuilding the streaming/thinking subtree the user can't see. Thaw as
// soon as the viewport comes back near the bottom or the turn finishes.
// Freeze the offscreen live tail so scroll doesn't rebuild unseen streaming UI.
if (liveTailVisible || !ui.busy) {
frozenProgressRef.current = liveProgress
}

View File

@ -249,28 +249,15 @@ export function StickyPromptTracker({ messages, offsets, scrollRef, onChange }:
useSyncExternalStore(
useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
() => {
const s = scrollRef.current
if (!s) {
return NaN
}
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
const { atBottom, top } = getStickyViewport(scrollRef.current)
return atBottom ? -1 - top : top
},
() => NaN
)
const s = scrollRef.current
const top = Math.max(0, (s?.getScrollTop() ?? 0) + (s?.getPendingDelta() ?? 0))
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 + vp, top, atBottom)
const { atBottom, bottom, top } = getStickyViewport(scrollRef.current)
const text = stickyPromptFromViewport(messages, offsets, top, bottom, atBottom)
useEffect(() => onChange(text), [onChange, text])
@ -395,3 +382,15 @@ interface TranscriptScrollbarProps {
scrollRef: RefObject<ScrollBoxHandle | null>
t: Theme
}
function getStickyViewport(s?: ScrollBoxHandle | null) {
const top = Math.max(0, (s?.getScrollTop() ?? 0) + (s?.getPendingDelta() ?? 0))
const vp = Math.max(0, s?.getViewportHeight() ?? 0)
const total = Math.max(vp, s?.getScrollHeight() ?? vp)
return {
atBottom: (s?.isSticky() ?? true) || top + vp >= total - 2,
bottom: top + vp,
top
}
}

View File

@ -18,8 +18,8 @@ const upperBound = (offsets: ArrayLike<number>, target: number) => {
export const stickyPromptFromViewport = (
messages: readonly Msg[],
offsets: ArrayLike<number>,
bottom: number,
top: number,
bottom: number,
sticky: boolean
) => {
if (sticky || !messages.length) {