From 511b8e2325c621a30bc9a75f00762d820282e72f Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 23 May 2026 17:48:28 -0500 Subject: [PATCH] fix(tui): re-check sticky inside resize debounce + document remount Addresses Copilot review on PR #31077: - onResize now re-checks isSticky() inside the 100ms timer so manual scrolls during the debounce window don't get snapped back to tail. - Comment on the virtualRows cols-keying calls out the deliberate trade-off: per-row local state (e.g. systemOpen) resets on resize so yoga can remeasure off live geometry. The hook's scale-by-ratio path is too approximate for mixed markdown widths. --- ui-tui/src/app/useMainApp.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index af12f50fab3..6f693eb7f3b 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -233,6 +233,12 @@ export function useMainApp(gw: GatewayClient) { return next }, []) + // Wrapped row heights are width-dependent. Cached layout outlives a resize + // and lands sticky-scroll at the stale max, cutting off the tail. The + // hook's "scale heights by oldCols/newCols" path is too approximate for + // mixed markdown — we deliberately remount every row so yoga re-measures + // off live geometry. Cost: per-row local state (e.g. systemOpen toggles) + // resets on resize; small UX hit for a hard correctness win. const virtualRows = useMemo( () => historyItems.map((msg, index) => ({ index, key: `${messageId(msg)}:c${cols}`, msg })), [cols, historyItems, messageId] @@ -424,18 +430,18 @@ export function useMainApp(gw: GatewayClient) { let timer: ReturnType | undefined - // Resize reflows wrapped lines; if the user was pinned to the tail we need - // to re-snap once React has remeasured. virtualRows is keyed on cols so - // every column change forces a fresh measurement pass before this fires. + // Resize reflows wrapped lines; if the user is still pinned to the tail + // we need to re-snap once React has remeasured. virtualRows is keyed on + // cols so every column change forces a fresh measurement pass before + // this timer fires. Re-check isSticky() inside the timeout — a manual + // scroll during the 100ms window otherwise yanks the user back to tail. const onResize = () => { - const wasSticky = scrollRef.current?.isSticky() ?? false - clearTimeout(timer) timer = setTimeout(() => { timer = undefined - if (wasSticky) { - scrollRef.current?.scrollToBottom() + if (scrollRef.current?.isSticky()) { + scrollRef.current.scrollToBottom() } void rpc('terminal.resize', { cols: stdout.columns ?? 80, session_id: ui.sid })