From 3c4eac806191244e7ecea67161ce59b12b7d3aea Mon Sep 17 00:00:00 2001 From: Amos Date: Sat, 22 Aug 2026 06:56:39 +0300 Subject: [PATCH] Fix bar-click flicker: suppress collapse while the reveal scroll is in flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking the collapsed bar set expanded=true and started a smooth scrollToBottom — but the scroll position was still legitimately inside the collapse zone (that's why the bar was showing), so the hysteresis listener re-collapsed the dock on the very next scroll event, then re-opened it when the scroll arrived: open → snap shut → reopen. The machine had no notion of intent. Now the bar click sets an expandIntent flag that suppresses the collapse branch; it clears when the scrollToBottom promise settles (completion or interruption) or after a 2s safety net, so a user yanking the scroll away mid-reveal collapses the dock again instead of wedging it open. Verified live: click = exactly one transition (bar→open at ~26ms, stays open, lands at d=1); collapse still healthy afterward; mid-reveal interrupt re-collapses within ~300ms. --- components/chat.tsx | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/components/chat.tsx b/components/chat.tsx index 7cd4300..aeb0bf3 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -235,6 +235,26 @@ function DockOverlay({ const expandedHRef = useRef(0); const collapsedHRef = useRef(42); + // Explicit expand (bar click): the smooth scroll to the bottom takes + // hundreds of ms, during which the position is still legitimately inside + // the collapse zone — the machine would instantly re-collapse the dock + // (open → snap shut → reopen at the bottom, the flicker). Suppress the + // collapse branch until the reveal scroll settles or is interrupted. + const expandIntentRef = useRef(false); + const intentTimerRef = useRef>(undefined); + const expandAndReveal = () => { + expandIntentRef.current = true; + setExpanded(true); + clearTimeout(intentTimerRef.current); + intentTimerRef.current = setTimeout(() => { + expandIntentRef.current = false; + }, 2000); + void Promise.resolve(scrollToBottom()).finally(() => { + clearTimeout(intentTimerRef.current); + expandIntentRef.current = false; + }); + }; + useEffect(() => { const el = scrollRef.current; if (!el) return; @@ -243,7 +263,7 @@ function DockOverlay({ const d = el.scrollHeight - el.clientHeight - el.scrollTop; const delta = Math.max(expandedHRef.current - collapsedHRef.current, 240); if (expandedRef.current) { - if (d > delta + 64) setExpanded(false); + if (d > delta + 64 && !expandIntentRef.current) setExpanded(false); } else if (d < 16) { setExpanded(true); void scrollToBottom(); @@ -310,10 +330,7 @@ function DockOverlay({ toolCallId={pending.toolCallId} input={pending.input as never} expanded={expanded} - onExpand={() => { - setExpanded(true); - void scrollToBottom(); - }} + onExpand={expandAndReveal} onSubmit={onSubmitAnswer} /> ) : (