Skip to content

[pull] main from danny-avila:main#118

Merged
pull[bot] merged 12 commits intoinnFactory:mainfrom
danny-avila:main
May 7, 2026
Merged

[pull] main from danny-avila:main#118
pull[bot] merged 12 commits intoinnFactory:mainfrom
danny-avila:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 7, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

danny-avila and others added 12 commits May 6, 2026 10:28
* fix: honor Anthropic Vertex config

* chore: format Anthropic Vertex config fix
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: add gpt-5.5 token definitions

* fix: align gpt-5.5 context limit
* fix: Preserve unicode filenames

* fix: Cap unicode filenames by bytes

* fix: Preserve clean artifact directories

* fix: Disambiguate normalized artifact names
)

* 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
* feat: add signed CloudFront downloads

* fix: preserve local IdP avatar paths

* fix: address signed download review findings

* fix: harden CloudFront cookie scope validation

* fix: preserve URL save API compatibility

* fix: store CDN SSO avatars under shared prefix

* fix: Harden CloudFront tenant file access

* fix: Preserve CloudFront download compatibility

* fix: Address CloudFront review follow-ups

* fix: Preserve file URL fallback user paths

* fix: Address download review hardening

* fix: Use file owner for S3 RAG cleanup

* fix: Address final download review nits

* fix: Clear stale avatar CloudFront cookies

* fix: Align download filename helpers with dev

* fix: Address final CloudFront review follow-ups

* fix: Stream S3 URL uploads

* fix: Set S3 stream upload length

* fix: Preserve download metadata filepath

* fix: Avoid remote content length for stream uploads

* fix: Use bounded multipart URL uploads

* fix: Harden S3 filename boundaries
* fix: harden agent file access

* style: format agent file query

* fix: prune agent file refs on alternate writes

* test: fix agent pruning specs
…12983)

* 🌐 fix: Percent-encode X-File-Metadata header for Unicode filenames

After #12977 preserved Unicode in filenames, the download route
crashes with ERR_INVALID_CHAR because JSON.stringify(file) now
contains non-ASCII characters that Node.js rejects in HTTP headers
per RFC 7230.

Wrap the header value in encodeURIComponent on the server and
decodeURIComponent on the client before JSON.parse.

* fix: Update file route tests after dev merge

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: Align auto-refill next date

* style: Fix auto-refill lint formatting

* refactor: Share auto-refill eligibility date

* refactor: Consolidate refill interval units

* fix: Guard malformed refill interval units

* fix: Preserve refill unit fallback label
* fix: Improve subagent dialog prompt rendering

* fix: Preserve cancelled subagent traces

* chore: Reuse generic prompt toggle labels

* fix: Scope new-chat subagent cleanup exemption

* fix: Use valid subagent prompt min-height

* fix: Flatten subagent dialog conditionals

* fix: Place subagent prompt in dialog scroll
* feat(files): add optional region-aware storage keys

* test(files): fix region storage CI fixtures

* feat(files): finalize inline CloudFront asset namespaces

* fix(files): allow wildcard region CloudFront cookies

* fix(files): preserve legacy storage key compatibility

* fix(files): align CloudFront clear cookie cleanup

* fix(files): clear legacy CloudFront cookie scopes

* chore(files): clean up storage review nits

* fix(files): keep inline namespaces CloudFront-only
@pull pull Bot locked and limited conversation to collaborators May 7, 2026
@pull pull Bot added the ⤵️ pull label May 7, 2026
@pull pull Bot merged commit 1bc2692 into innFactory:main May 7, 2026
1 check passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants