mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 15:58:48 +00:00
* feat(ui): add message navigation strip and redesign scroll-to-bottom button
Add a floating vertical navigation strip on the right edge of the chat
area that lets users jump between messages quickly. Each message gets an
indicator line (wider for assistant, narrower for user) with HoverCard
previews showing truncated message text. IntersectionObserver tracks
which messages are currently visible and highlights their indicators.
Redesign the scroll-to-bottom button: solid backgrounds instead of
semi-transparent, clean enter/exit animations without twist/rotate,
no hover float animation, positioned at the right edge of the chat
form instead of center.
* fix(ui): prevent message nav layout shift on scroll
Use a fixed-height container for each indicator so the nav strip
maintains consistent dimensions when indicators transition between
active and inactive states.
* fix(ui): debounce message nav refresh and persist visibility state
Debounce entry refresh (200ms) to avoid thrashing from rapid DOM
mutations during code block rendering. Persist the visible message
set across IntersectionObserver reconnections to prevent momentary
empty state that disabled navigation buttons.
* fix(ui): prevent nav buttons from disabling during fast scroll
- Fall back to last known active index when IntersectionObserver
reports no visible messages during rapid scrolling
- Lower intersection threshold from 10% to 1% for long messages
- Fix preview text to skip the message header (Prompt N: username)
* fix(ui): scroll to message start when using nav arrow buttons
Arrow buttons now use block: 'start' to always scroll to the top of
the target message. Indicator dots keep block: 'nearest' for minimal
repositioning on direct clicks.
* fix(ui): account for header offset when scrolling to messages
Use manual scrollTo with a 56px offset to prevent the fixed header
from covering the top of the target message when using arrow buttons.
* fix(ui): improve message nav scrolling and visual subtlety
- Up button scrolls to current message top first before jumping to
previous, preventing skipped messages on long content
- Down button consistently scrolls to the start of the next message
- Nav strip is faded (opacity 30%) by default, fully visible on hover
- Background, buttons, and indicators all appear on hover of the
nav area using group hover coordination
* fix(ui): use native scroll-margin-top for reliable message navigation
Replace manual scrollTo calculations with scrollIntoView + CSS
scroll-margin-top on .message-render elements. The browser handles
scroll offset natively, eliminating positioning errors during smooth
scroll animations.
* fix(ui): use firstActiveIndex for both nav directions
Use firstActiveIndex (topmost visible message) for both up and down
navigation. Down now advances one message at a time from what the user
is currently reading instead of jumping past all visible messages.
Remove unused lastActiveIndex.
* fix(ui): address PR review feedback
- Scope getMessageEntries query to scroll container instead of document
- Include preview text in entries equality check to catch content
updates during streaming/edits
- Move scroll button transition to base state so release animates
smoothly instead of snapping back
* fix(ui): make message nav scroll precise and chevrons reliable
- Bump .message-render scroll-margin-top from 1rem to 4rem so messages
land below the 52px absolute gradient header instead of behind it.
- Drive chevron jumps from live scrollTop + offsetTop comparison rather
than the IntersectionObserver-derived firstActiveIndex, which lagged
behind rapid clicks and treated any 1px-visible message as "current".
- Track canGoUp / canGoDown from the same scroll-position comparison so
the disabled state matches what the buttons will actually do.
- Auto-center the indicator column on the visible message range and
smooth-scroll it via rAF so 500+ indicators stay at 60fps.
- Pull entry data from useGetMessagesByConvoId (with a DOM fallback) so
previews are state-backed instead of scraped from rendered markup.
- Memoize MessageIndicator and filter MutationObserver to .message-render
add/remove only.
- Add 5 i18n keys (com_ui_message_nav*) for nav and indicator labels.
* perf(ui): skip off-screen message layout and fix resulting scroll drift
Large conversations used to freeze the main thread during sidebar
toggles because every animated frame had to relayout every message.
With ~3000 message elements on this branch: avg frame 650ms,
max 1701ms (~1.5fps) during the 300ms transition. Adding
`content-visibility: auto` with `contain-intrinsic-size: auto 200px`
on .message-render lets the browser skip layout/paint for messages
outside the viewport, dropping avg frame to 33ms and max to 74ms
(~30fps, feels responsive).
content-visibility comes with a trade-off though: off-screen messages
use the 200px intrinsic-size estimate until they're measured. That
broke indicator-click scrolling on long conversations, landing 1-2
messages off the target because scrollIntoView computed its target
scrollTop once with stale estimates, and intermediate messages
shrunk/grew as they rendered during the smooth scroll.
Replaced scrollIntoView with a manual rAF scroll that re-reads the
target's getBoundingClientRect every frame and eases toward the
*current* target. Verified drift=0 across fake-0, fake-50, fake-250,
fake-450 (messages near the bottom naturally land higher than
scroll-margin when the container is already at max scroll — expected).
Also two small MessageNav.tsx hot-path cleanups:
- Use col.children[i] instead of col.querySelector by data-msg-id for
the indicator-column centering lookup (entries map 1:1 to column
children since HoverCardTrigger asChild forwards to the button).
- Compare visibility set contents before setActiveIds, so an
IntersectionObserver flush with unchanged membership doesn't force
a re-render and 500x memo comparisons.
* revert(ui): drop content-visibility on .message-render
Didn't deliver the expected sidebar-toggle perf win in real-world
usage, and its intrinsic-size estimation introduced the exact kind of
scroll drift we then had to work around. The rAF scroll in MessageNav
is orthogonal to this and stays — it works fine with or without
content-visibility.
* fix(ui): address PR review — a11y, tests, and MessageNav correctness
- ScrollToBottom aria-label now runs through useLocalize instead of being
hardcoded English. Added com_ui_scroll_to_bottom translation key.
- MessageNav nav expands on keyboard focus-within, not just pointer hover.
- Indicator buttons expose aria-current="true" for the active message and
get a visible focus-visible ring. Chevron buttons get the same ring so
keyboard users can see focus.
- Cancel in-flight rAF scrolls when a new navigation starts, so clicking
a second indicator mid-animation doesn't race the first loop on
container.scrollTop.
- Invalidate the cached offsetsTop/offsetsBottom arrays via a
ResizeObserver on the scroll content. Previously heights that changed
after mount (code blocks rendering, images loading) left canGoUp /
canGoDown and the indicator-column centering reading stale positions.
- Observe IntersectionObserver entries incrementally. The observer is
now created once per scroll container and entries add/remove on
change instead of the whole observer being torn down and rebuilt for
every new message.
- memo() the default export so parent re-renders don't cascade through
MessageNav when entries/activeIds haven't changed.
- Add 18-test suite covering rendering threshold, user/assistant
indicator styling, preview sourcing (React Query vs DOM fallback vs
truncation), accessibility (aria-label, aria-current, chevron
disabled state), click-driven rAF scroll + cancellation, and observer
lifecycle (observe on mount, incremental sync, unobserve on removal,
disconnect on unmount).
* fix(ui): catch in-place message id mutations and react to layout shifts
Follow-ups from deep review:
- MutationObserver on .message-render now also watches the id attribute.
During the SSE lifecycle a single DOM node's id cycles through three
values (client UUID -> createdHandler id -> server id, see the comment
in MultiMessage.tsx), which meant the previous childList-only observer
never refreshed entries after a streaming response. Nav clicks on the
most recent message were silently failing because getElementById
returned null for the stale id.
- ResizeObserver now calls scheduleTick() instead of only flipping a
flag. The flag was only consumed inside the scroll handler's tick, so
heights that changed while the user wasn't scrolling (assistant message
streaming in, code blocks highlighting) left offsetsTop/offsetsBottom
stale and canGoUp / canGoDown wrong. Both handlers now route through
scheduleTick so a resize and a scroll share the same rAF slot.
- Unify scroll and resize callbacks on scheduleTick. Removes a duplicate
rAF path and makes the effect cleaner.
- Single-pass build of newIds during incremental IO sync (previously
entries.map().new Set() did two passes for no reason).
- CSSTransition timeouts drop from 550/700 to 300/250 to match the new
scroll-to-bottom animations. Old values left the button in the DOM
for up to 450ms after the exit animation finished.
- ScrollToBottom.tsx imports reordered to longest-first per project
convention.
- style.css: collapse split `border: 1px solid` + `border-color` into
one shorthand; dark variant still overrides border-color cleanly.
- Tests: add SSE-lifecycle test that mutates a .message-render id in
place and asserts the nav now shows an indicator for the new id and
none for the old one. HoverCard mock no longer spreads unknown props
to the DOM div (drops a React warning).
* fix(ui): address deep-review follow-ups on MessageNav
- Move activeScrollToken from module scope to a per-instance useRef
(scrollTokenRef). When LibreChat eventually mounts more than one
MessageNav side-by-side (multi-panel / added-convo view) a click in
one panel will no longer cancel an in-flight smooth scroll in another.
scrollToMessageStart is now an instance useCallback and the button
click path goes through an onSelect prop on MessageIndicator, keeping
the memoized indicator stable.
- messagesById goes through a ref (messagesByIdRef) so refreshEntries is
no longer recreated on every streaming token. Previously messagesById
landed in both the useMemo and the refreshEntries dep array, so each
streaming response rebuilt the MutationObserver effect dozens of times
per second. A separate small effect still calls refreshEntries when
messagesById changes, so previews stay fresh.
- Extract USER_TURN_SELECTOR constant and tighten the text-preview type
narrowing so we no longer need the `as { value?: string }` cast (TS
narrows string | TextData correctly through the `typeof object` +
property access guard).
- Cache the computed scroll margin (4rem = 64px) in scrollMarginRef so
the nav callbacks don't call getComputedStyle on every click.
- Tests: add a two-instance isolation test that verifies scroll tokens
don't cross between mounted MessageNavs. Drop the unused `import React
from 'react'` pattern in favor of local type aliases.
- client/package.json: bump @babel/preset-typescript to ^7.28.5. The old
^7.22.15 constraint was resolving to 7.23.3 via hoisting, which can't
parse modern `import type` syntax on a clean install and was breaking
the test suite.
* fix(ui): address re-review — clean lockfile + ScrollToBottom ref target
- package-lock.json: the preset-typescript bump last commit pulled in
transitive Babel packages resolved through a local internal registry
(npm.internal.berry13.com). Rewrote those 31 entries back to the
public npmjs.org registry so CI and contributors can install cleanly.
Integrity hashes unchanged — content-addressed.
- ScrollToBottom now forwards its ref to the wrapping <div> instead of
the inner <button>. CSSTransition's nodeRef + unmountOnExit can now
add transition classes to the actual root element, so the layout
wrapper is what mounts/unmounts, not just the button. Updated
scrollToBottomRef type in MessagesView to HTMLDivElement.
- jumpToPrevious / jumpToNext skip the document.getElementById fallback
lookup when scrollMarginRef is already populated, which is the normal
case after the first scroll-tick effect run.
* fix(ui): preserve IntersectionObserver across in-place id mutations
The IO sync effect was observing new ids before unobserving old ones.
During the SSE lifecycle of a fresh chat, a single .message-render node
cycles through three ids (client UUID -> handler id -> server id). When
the id mutated on the same element, the effect would call observe(el)
then unobserve(el) on that element in the same pass — leaving it
permanently unobserved. The active-message highlight never updated for
the new id until a hard refresh rebuilt everything from scratch.
Switched to element-identity tracking. Build an element -> newId map
from entries, then for each currently observed [oldId, el]:
- if the element no longer appears in entries, unobserve and drop it
- if the element appears under a new id, migrate observed and
visibleSet keys in place — the IntersectionObserver keeps watching
the same DOM node uninterrupted
Genuinely new elements get observed afterward as before. Rename doesn't
fire an IO callback, so flush activeIds manually when at least one
migration happened. Existing convos already had this working because
their ids never mutate after load — only fresh chats hit the SSE id
cycle, which matches the reproduction.
* fix(ui): keep message nav current and pinned at bottom
|
||
|---|---|---|
| .. | ||
| public | ||
| scripts | ||
| src | ||
| test | ||
| babel.config.cjs | ||
| check_updates.sh | ||
| index.html | ||
| jest.config.cjs | ||
| nginx.conf | ||
| package.json | ||
| postcss.config.cjs | ||
| tailwind.config.cjs | ||
| tsconfig.json | ||
| vite.config.ts | ||