From 30ae414911ff804e018ec1fd7bb9d77ae3a39332 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 22 Jul 2026 22:13:29 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8C=20fix:=20Pin=20Scroll-to-Bottom=20?= =?UTF-8?q?Rib=20in=20Message=20Nav=20(#14397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📌 fix: Pin Scroll-to-Bottom Rib in Message Nav Render the terminus rib outside the scrolling rail, between the column and the down chevron, so the scroll-to-bottom affordance stays in view no matter how far the rail has scrolled. - scrubTo enumerates ribs from the nav so drag-to-bottom still lands on the terminus - the pinned rib drives the shared preview itself on hover and focus, since it is no longer covered by the column's pointer magnification * 🖱️ fix: Keep Drag-Scrub Startable From the Pinned Terminus Pointer-down on the pinned rib no longer reaches the column's handler now that it renders outside the scrollport, so wire the same drag-start to the wrapper. Dragging up from the bottom dot scrubs the thread again. --- .../components/Chat/Messages/MessageNav.tsx | 86 ++++++++++--- .../Messages/__tests__/MessageNav.spec.tsx | 115 ++++++++++++++++++ 2 files changed, 187 insertions(+), 14 deletions(-) diff --git a/client/src/components/Chat/Messages/MessageNav.tsx b/client/src/components/Chat/Messages/MessageNav.tsx index afb3a9aad9..99d252c5c2 100644 --- a/client/src/components/Chat/Messages/MessageNav.tsx +++ b/client/src/components/Chat/Messages/MessageNav.tsx @@ -313,6 +313,16 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { + const last = entries[entries.length - 1]; + if (last?.isEnd === true) { + return { messageEntries: entries.slice(0, -1), endEntry: last }; + } + return { messageEntries: entries, endEntry: null }; + }, [entries]); + const getCurrentVisibleId = useCallback((): string | null => { const container = scrollableRef.current; if (!container) { @@ -518,10 +528,11 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { const col = columnRef.current; - if (!col) { + const nav = navRef.current; + if (!col || !nav) { return; } - const ribs = col.querySelectorAll('[data-msg-id]'); + const ribs = nav.querySelectorAll('[data-msg-id]'); const count = ribs.length; if (count === 0) { return; @@ -715,6 +726,38 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject { + const rect = el.getBoundingClientRect(); + const left = columnRef.current?.getBoundingClientRect().left ?? rect.left; + focusTooltip(MESSAGES_END_ID, rect.top + rect.height / 2, window.innerWidth - left + 8); + }, + [focusTooltip], + ); + + const handleEndPointerEnter = useCallback( + (e: React.PointerEvent) => showEndTip(e.currentTarget), + [showEndTip], + ); + + const handleEndFocus = useCallback( + (e: React.FocusEvent) => showEndTip(e.currentTarget), + [showEndTip], + ); + + const handleEndBlur = useCallback( + (e: React.FocusEvent) => { + const next = e.relatedTarget as Node | null; + if (next && e.currentTarget.contains(next)) { + return; + } + clearTooltip(); + }, + [clearTooltip], + ); + const applyMagnify = useCallback(() => { magRafRef.current = null; const col = columnRef.current; @@ -1209,9 +1252,7 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject document.removeEventListener('keydown', onKeyDown); }, [focusNav]); - const hasEnd = entries.length > 0 && entries[entries.length - 1].isEnd === true; - const messageCount = hasEnd ? entries.length - 1 : entries.length; - if (messageCount < 3) { + if (messageEntries.length < 3) { return null; } @@ -1252,15 +1293,11 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject - {entries.map((entry) => { - const label = entry.isEnd - ? localize('com_ui_scroll_to_bottom') - : localize( - entry.isUser - ? 'com_ui_message_nav_go_to_user' - : 'com_ui_message_nav_go_to_assistant', - { 0: entry.preview.slice(0, 30) }, - ); + {messageEntries.map((entry) => { + const label = localize( + entry.isUser ? 'com_ui_message_nav_go_to_user' : 'com_ui_message_nav_go_to_assistant', + { 0: entry.preview.slice(0, 30) }, + ); const isHighlighted = hoveredId != null ? hoveredId === entry.id : visibleIds.has(entry.id); return ( @@ -1276,6 +1313,27 @@ function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject + {endEntry && ( +
+ +
+ )} +