From a489ee1a7c032c0d81f7faaf42a7a14e6fea28eb Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Tue, 5 May 2026 10:47:32 -0700 Subject: [PATCH] fix(canvas/chat): instant-scroll to bottom on first mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: "right now when chat box opens it opens in the middle, but it should be at the end of conversation." Root cause: ChatTab.tsx:548 fires `bottomRef.scrollIntoView({ behavior: "smooth" })` on every messages-update. On initial mount with N messages already loaded, the smooth-scroll triggers a ~300ms animation that any concurrent React re-render (agent push landing, theme toggle, sidepanel resize) interrupts mid-flight, leaving the user stuck somewhere in the middle of the conversation. Fix: track first-mount via hasInitialScrollRef. Use behavior:"instant" for the initial jump (deterministic, no animation interruption), then smooth for subsequent appends (the new-message-landing visual stays). Refs flipped on first messages.length > 0 transition, so: - Initial open of chat tab: instant jump to bottom ✓ - New agent message arrives: smooth scroll into view ✓ - Workspace switch (ChatTab remounts): fresh hasInitialScrollRef, gets instant again ✓ - loadOlder prepend: anchor-restore path unchanged, still pins user's reading position ✓ Test plan: - pnpm test --run ChatTab.lazyHistory.test.tsx → 8 pass (existing lazy-history tests untouched) - npx tsc --noEmit clean - Manual on hongming.moleculesai.app: open a busy chat (mac laptop, ~50 messages), confirm view lands at the latest bubble, not mid- scroll. Switch to another workspace + back → instant again. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/tabs/ChatTab.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/canvas/src/components/tabs/ChatTab.tsx b/canvas/src/components/tabs/ChatTab.tsx index 7da17b72..2d6ae908 100644 --- a/canvas/src/components/tabs/ChatTab.tsx +++ b/canvas/src/components/tabs/ChatTab.tsx @@ -286,6 +286,14 @@ function MyChatPanel({ workspaceId, data }: Props) { const [error, setError] = useState(null); const [confirmRestart, setConfirmRestart] = useState(false); const bottomRef = useRef(null); + // First-mount scroll-to-bottom needs `behavior: "instant"` — long + // conversations smooth-animate for ~300ms which any concurrent + // re-render can interrupt, leaving the user stuck mid-conversation + // when the chat tab opens. Subsequent appends (new agent messages) + // keep `smooth` for the visual "landing" feel. Flipped the first + // time messages.length goes positive, so a workspace switch (which + // remounts ChatTab) gets a fresh instant jump too. + const hasInitialScrollRef = useRef(false); // Lazy-load older history on scroll-up. // - containerRef = the scrollable messages viewport // - topRef = sentinel above the messages list; IO observes it @@ -545,6 +553,15 @@ function MyChatPanel({ workspaceId, data }: Props) { scrollAnchorRef.current = null; return; } + // Instant on first arrival of messages — smooth-scroll on a long + // conversation gets interrupted by concurrent renders and leaves + // the user stuck in the middle. After the first jump, subsequent + // appends animate as before. + if (!hasInitialScrollRef.current && messages.length > 0) { + hasInitialScrollRef.current = true; + bottomRef.current?.scrollIntoView({ behavior: "instant" as ScrollBehavior }); + return; + } bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);