Skip to content

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

Merged
pull[bot] merged 4 commits into
innFactory:mainfrom
LibreChat-AI:main
Sep 12, 2026
Merged

pull[bot] merged 4 commits into
innFactory:mainfrom
LibreChat-AI:main

Conversation

@pull

@pull pull Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

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 4 commits September 11, 2026 16:47
* fix: inspect stored file locators per message

* fix: declare traversal diagnostics for isolated declaration builds

* refactor: inject locator traversal reporting from application callers

* fix: propagate traversal diagnostics across protected entry points

* fix: complete middleware reporters and hydration message counts
* 🕵️ style: Mark Temporary Chat with the Incognito Icon

The dashed speech bubble (`MessageCircleDashed`) read as a generic chat glyph
rather than a retention cue, and its dashes break up at 16px. Every temporary
chat surface now uses lucide's `hat-glasses`, the hat-and-glasses incognito
mark: the header toggle, the read-only indicator chip once a conversation has
started, the overflow menu row, and the landing empty state.

`hat-glasses` landed in lucide 0.528.0, so `lucide` and `lucide-react` move
from `^0.525.0` to `^0.528.0` in `client` and `packages/client` (dependencies
and peers). That is the smallest bump that carries the icon; lucide redraws
existing icons between minors and the whole UI draws from it, so jumping to the
current 0.577 would bring unrelated visual churn.

`useChatBadges` imported the old icon without using it; the import is dropped.

* 🧹 style: Keep the Model Disclaimer on the Landing Page Only

The footer disclaimer rendered under the composer in every conversation, where
it repeats itself on each turn and costs a line of the thread. It now renders
only on the landing page, where it is first seen.

`Footer` is untouched, as are its other hosts: the auth screens and the public
shared-link view still render it. The component is a zero-height `relative`
wrapper around an `absolute bottom-0` bar, so dropping it from the conversation
branch shifts no layout.

* 📱 fix: Run the Mobile Composer to the Screen Edges

Below `sm` the composer is full-bleed — no horizontal padding, squared off at
the bottom, flush with the viewport floor — but it was inset on two sides there:

- `.scrollbar-gutter-spacer` reserved the message column's scrollbar band (8px)
  on the composer's trailing edge. That lines the composer up with the thread on
  desktop, but on mobile it only cut a dead strip off its right side, so the
  padding now applies from `sm` up. Its two users, the composer band and the
  scroll-to-bottom control, move together, so the control still stacks over the
  send button.
- The composer surface carried `pb-4` under its action row below `sm`
  (`sm:pb-0` above), leaving 16px of empty surface between the buttons and the
  bottom of the screen.

Desktop metrics are unchanged: the band still reserves the gutter, the composer
still clears the floor by `sm:mb-10`, and the surface still resolves to `pb-0`.

* 🔅 style: Mute the Landing Page Disclaimer

The footer sat in `text-primary`, the same weight as the greeting above it, so
the least important line on the landing page read as one of the most prominent —
hardest to ignore in the light theme, where it lands at 16.1:1 on white.

It now takes `text-muted`, the quietest text token that still clears AA for
12px copy on `bg-presentation`: 5.11:1 on white and 7.93:1 on the dark canvas.
The contrast modes collapse every text token to pure black or white, so
`high-contrast-light` and `high-contrast-dark` keep the disclaimer at 21:1.

The links move with it instead of keeping the brighter `text-secondary`: the
underline carries the affordance, and a link that outshines its own sentence
puts the emphasis back where this change takes it from.

The text stays at the bottom of the page. The landing hierarchy is the composer
and its title first, the sidebar second, and moving the disclaimer under the
text field would place it above both.

* 💠 style: Draw the Empty Prompts Panel Like Its Neighbours

The prompts side panel hand-rolled its own empty card — the same circular icon,
title and caption as the bookmarks, memories and schedules panels, but written
out again and a shade off: `border-border-medium` where the shared card uses
`border-border-light`.

It now renders the `EmptyState` primitive the other panels use, which is where
that appearance is owned. The strings, the icon and the list's own `my-2` are
unchanged, so the panel reads the same apart from the border it was never meant
to differ on.

* 📐 fix: Close the Gap Under the Composer in a Conversation

From `sm` up the composer band left 40px under itself, the height of the
disclaimer that used to sit there. A started conversation no longer carries one,
so the composer floated a line above the viewport floor with nothing in the gap.

The clearance now follows what is underneath. `ChatView` owns that fact — it is
the same `isLandingPage` it gates the footer on — and passes it to `ChatForm`:

- landing, centred composer: `sm:mb-28`, unchanged
- landing, composer at the bottom: `sm:mb-10`, the clearance the disclaimer
  needs, unchanged
- started conversation: `sm:mb-4`, enough to show the surface's own shadow

Below `sm` the composer still runs to the viewport floor in every state.

`ChatForm` cannot answer this for itself: deriving it from
`conversation?.messages?.length === 0` reads false while `messages` is
undefined, which silently drops the un-centred landing page to the conversation
clearance and overlaps the disclaimer.

`SubagentThreadPanel` states that its own bottom padding matches the main
composer's so the two surfaces end on one line when the panel is open beside a
thread, so it moves with it.

* 🎞️ fix: Slide the Composer Between Welcome Screen and Conversation

With "Center Chat Input on Welcome Screen" off, the welcome composer sits at the
bottom with the disclaimer's clearance under it and a conversation's sits 24px
lower. React keeps the same form node across that navigation, so the band had
everything it needed to travel and jumped instead: `margin-bottom`, the property
that carries it, was not in the transition list — only `max-width` was, from the
chat-width preference.

`margin-bottom` now transitions with it, 300ms on Tailwind's default easing, in
both directions and from the centred welcome composer as well. Reduced motion
gets the new position outright (`motion-reduce:transition-none`): this is a
slide across the page, not decoration.

* ↔️ fix: Hold the Composer Still Across the Welcome Screen

Opening a conversation from the welcome screen slid the composer 4px to the
left. A conversation's composer band reserves the band the message column holds
back for its scrollbar (`.scrollbar-gutter-spacer`, 8px here), which is what
lines the composer up with the messages; the welcome screen reserved nothing, so
its composer was centred on the full region and half the gutter off from where
it was about to land. Now that `margin-bottom` animates, the sideways step ran
alongside the downward one.

The gutter is reserved once per state, wherever the centring happens: the
conversation keeps it on the band around the composer, and the welcome screen
takes it on the column that centres greeting and composer together, so both move
as one and the composer lands where the message column will put it.

Measured across the navigation — 1280 and 1024 wide, centred and bottom
composer, sidebar open and collapsed: the surface keeps its exact left edge and
width. Below `sm` the reservation does not apply and the composer stays
full-bleed on both sides of the navigation.

* 📏 fix: Measure the Scrollbar Gutter Before the Welcome Screen Reserves It

The welcome screen holds back the band the message column reserves for its
scrollbar, so the composer lands where the message column will put it. On a
fresh load no message column has mounted, so `useScrollbarGutter` has published
nothing and the spacer fell back to `--scrollbar-size`: a fixed 8px, correct
only where the app's own `::-webkit-scrollbar` width applies. On an
overlay-scrollbar platform the real band is zero, so the welcome screen started
4px off-centre and the composer stepped sideways as soon as the first
conversation measured the band for real — the jump this change set out to
remove, inverted.

The gutter is now seeded from a detached probe built like the column itself
(`overflow-y: auto` with `scrollbar-gutter: stable`), which answers the same
question for the platform in front of the user: the scrollbar width, and zero
where scrollbars overlay. A mounted column measures itself and is
authoritative, so the seed only fills the gap before the first one exists and
never overwrites a published measurement.

Reported by Codex on PR #15838:
#15838 (comment)

* 🧪 test: Cover the Composer and Welcome Screen Behaviours

Nine browser scenarios for what this change promises, in the mock harness
against the in-process fake model, tagged so each one is addressable:

- `welcome-screen-disclaimer-only`, `welcome-disclaimer-readable-while-muted`:
  the disclaimer stays on the welcome screen, leaves the conversation, and
  clears AA contrast against the canvas it is painted on — the dark project
  measures the dark palette, the light one the light palette.
- `composer-holds-position-into-conversation`,
  `composer-holds-position-with-overlay-scrollbars`: the composer keeps its left
  edge and width across the navigation and lines up with the message column,
  both with the app's own scrollbars and on a platform whose scrollbars reserve
  nothing.
- `composer-slides-down-into-conversation`,
  `composer-settles-instantly-under-reduced-motion`: the clearance change runs as
  a `margin-bottom` transition on the composer's own form, and runs no
  transition at all under `prefers-reduced-motion`.
- `mobile-composer-reaches-screen-edges`: below `sm` the surface spans the
  viewport and its action row is the last thing in it.
- `temporary-chat-marked-with-incognito-icon`: turning the mode on marks the
  toggle and the welcome screen with the outline incognito mark.
- `empty-prompts-panel-matches-other-panels`: the empty prompts card and the
  empty bookmarks card are drawn identically.

Each scenario skips the projects whose viewport it does not describe, rather
than asserting a desktop layout under mobile emulation.

* 🩹 fix: Seed the Gutter Before Paint and Keep Configured Footers

Three findings from the review round on eec4b37, one root cause each.

The gutter seed measured in a passive effect, so the welcome screen's first
frame still reserved the `--scrollbar-size` token and recentred by 4px once the
real band was known — the load-time half of the shift this reservation removes.
It measures in a layout effect now, before the browser paints.

Scoping the disclaimer to the welcome screen also unmounted the only
authenticated-chat renderer of `customFooter`, and of a deployment's privacy
policy and terms links. `Footer` takes `configuredOnly`: the generic disclaimer
is dropped, operator content is kept, and with nothing configured it renders
nothing rather than an empty bar. The conversation renders it again under that
scope.

The mobile composer spec gated itself on viewport width, and the repository's
mock config has one Desktop Chrome project, so CI skipped it and reported
nothing. It declares the viewport it describes with `test.use`.

Two scenarios cover the first two:

- `welcome-composer-paints-in-its-final-position`: samples the composer's x from
  the document's first frame on a platform whose scrollbars reserve nothing, and
  fails on any later correction.
- `configured-footer-stays-in-a-conversation`: serves a configured
  `customFooter` through the startup-config route and finds it on both the
  welcome screen and inside a conversation.

Findings:
#15838 (comment)
#15838 (comment)
#15838 (comment)

* 🧹 test: Drop the First-Paint Sampler, Keep the Layout Effect

The sampler recorded the composer's x from the document's first frame to catch a
late recentring on a platform whose scrollbars reserve nothing. It cannot fail:
with the seed measured in a passive effect it still recorded a single position
in all three projects, because React flushes that effect before the browser
paints the commit it belongs to. A test that passes with and without the
behaviour it describes pins nothing.

The measurement stays in a layout effect: a value the layout reads belongs
before paint by construction, whatever a given runtime's flush order happens to
be. The reservation itself is covered by
`composer-holds-position-with-overlay-scrollbars`, which does fail without it —
4px, the half-band Codex reported.

* 🧷 fix: Clear the Configured Footer and Remeasure the Band on Contrast

Two findings from the round on d942b7c, one invariant each.

Keeping a configured footer in conversations left the composer reserving 16px
above an absolutely positioned bar about 32px tall, so the bar painted over the
composer's action row and would have taken its clicks. The clearance follows
what actually renders beneath the composer now, not which page it is: `ChatView`
reads `useConfiguredFooter` once and both decisions — whether to render the
footer, and how much room to leave for it — come from that same answer.
`ChatForm` takes `footerBelow` and keeps the welcome screen's rhythm for the
welcome screen.

The band is not a constant of the platform either: the contrast modes widen the
app's own scrollbar to 0.75rem, and a reader can turn contrast on — or have the
OS turn it on — while the welcome screen is the only thing mounted, with no
message column to republish. The seed remeasures when `ThemeProvider` flips a
class on the document element, which is the thing that changed the band.

Two scenarios, both failing before the fixes:

- `configured-footer-clears-the-composer`: with `customFooter` configured, the
  bar sits at or below the composer's bottom edge and the composer still takes a
  click at its own bottom row.
- `composer-holds-position-across-a-contrast-switch`: contrast turned on while
  the welcome screen is up, then a conversation opened; the composer keeps its
  left edge and width.

Findings:
#15838 (comment)
#15838 (comment)

* 🪡 fix: Align the Subagent Panel With the Conversation's Footer

The panel's bottom band states that it matches the main composer's clearance so
the two surfaces end on one line beside each other. That clearance became
footer-dependent in the previous commit, and the panel kept a fixed `pb-4`, so a
deployment with a configured footer ended them 24px apart.

The panel reads the same answer the composer reads — `useConfiguredFooter` — and
clears the bar when there is one. Three consumers now share one source for the
question "does a footer render beneath this surface": `ChatView`, `ChatForm`
through `footerBelow`, and this panel.

The panel's own alignment has no browser scenario: reaching this surface needs
an agent with subagents, a dispatched task and an open control footer, which is
a fixture of its own. The shared cause is covered by
`configured-footer-clears-the-composer` on the main composer.

Reported by Codex on PR #15838:
#15838 (comment)

* 🧵 fix: Hold the Footer Clearance Through a Cold Load and a Stale Band

Three findings from the round on 55106cd.

Whether a conversation carries a footer is an answer from the startup config,
and on a cold `/c/<id>` load it arrives after the composer is painted.
`useConfiguredFooter` now reports `{ present, resolved }`, and the two readers
use them differently on purpose: nothing renders until the answer arrives,
because there is nothing to render, but the clearance is held from the first
frame, because guessing "no footer" moves the composer twice.

The measured scrollbar band outlives the chat screen on purpose, and the screens
it outlives can change it — the auth layout carries a theme selector, and a
contrast switch there widens the app's own scrollbar while no chat column is
watching. The seed measures on every mount now instead of trusting a value it
did not publish.

`@librechat/client`'s peer range kept its previous line as well as the new one
(`^0.525.0 || ^0.528.0`): only `/client` consumes `HatGlasses`, and the shared
package still builds against either, so raising the published minimum would have
rejected downstream installs for an icon it does not use. Its devDependency
stays on 0.528, which is what it is tested against.

Two scenarios, both failing before the fixes:

- `configured-footer-clearance-survives-a-cold-load`: the startup config is
  served 700ms late with a custom footer, and the composer's bottom is sampled
  from the document's first frame; any correction fails it.
- `welcome-screen-remeasures-a-stale-gutter`: a 40px reservation is left behind
  before the app boots, as another screen would, and the welcome screen has to
  replace it rather than inherit it.

Findings:
#15838 (comment)
#15838 (comment)
#15838 (comment)

* 🧠 fix: Remember the Footer Answer Instead of Guessing It

Reserving the footer's band while the startup config was in flight fixed the
cold load of a deployment that configures a footer and broke the cold load of
one that does not: the composer took `sm:mb-10`, then dropped 24px when the
answer said there was nothing to clear. Both defaults are wrong for one of the
two deployments, because the layout was being derived from an answer that does
not exist yet.

`useConfiguredFooter` remembers the answer instead. A deployment's footer
configuration is a deployment-lifetime fact, so the last answer it gave is the
right thing to lay out against while `/api/config` is in flight; it is recorded
once the query resolves. Every load after the first is exact in either kind of
deployment, and a first-ever visit falls back to LibreChat's default — no
configured footer — and settles once if the deployment disagrees.

`ChatView` and the subagent panel drop the `!resolved` term: `present` already
carries the remembered answer, so the value is the same before and after the
config answers.

Scenarios, both sampling the composer's bottom from the document's first frame
with the config served 700ms late:

- `configured-footer-clearance-survives-a-cold-load` now primes the answer with a
  first visit and asserts the load after it, which is the load a returning user
  gets.
- `default-clearance-survives-a-cold-load` covers the other branch: a deployment
  that configures nothing must not move either.

Reported by Codex on PR #15838:
#15838 (comment)

* 🛟 fix: Treat a Failed Startup Config as No Answer

React Query reports a request that exhausted its retries as fetched, with no
data, so reading `isFetched` turned a failed `/api/config` into a confirmed
"nothing configured": a deployment remembered as having a footer lost the
clearance that footer needs, and the memory was overwritten with the guess, so
the composer moved again on the next load that succeeded.

`useConfiguredFooter` reads `isSuccess`. A failure now answers nothing: the
remembered deployment state stands, and nothing is recorded until a response
actually arrives.

`config-failure-keeps-the-remembered-footer` covers it: a first visit records a
configured footer, a second visit is served 500s until the retries run out, and
the composer's clearance has to be the one it had.

Reported by Codex on PR #15838:
#15838 (comment)

* 🧽 test: Drop the Config-Failure Scenario, Keep the Success Check

The scenario recorded a configured footer, served `/api/config` 500s until the
retries ran out, and then asserted the next successful load lays out once. It
cannot fail: without a startup config the app renders no chat at all, so
`useConfiguredFooter` is never mounted on the failing visit and nothing is
recorded either way. The negative control — reading `isFetched` again — passes
it in all three projects.

The reported corruption therefore has no reachable path: the hook only observes
a failed query while the chat is mounted, which requires a config that answered
once, and React Query keeps that answer as cached data through a later failure.

`isSuccess` stays. "A failure answers nothing" is what the code should say, and
saying it costs nothing; the test that pretended to prove it does not stay.

Reported by Codex on PR #15838:
#15838 (comment)

* 🧱 refactor: Own the Footer Memory in Jotai and Take the Landing Preference In

Two repository-policy findings from the round on 5d535a6.

The remembered footer answer was a raw `localStorage` read and write, which is
the persistence path the client state rules replaced: it is feature-owned state
that the chat surface both writes and reads, so it is a Jotai storage atom now
(`configuredFooterAtom`, beside the feature that owns it). `getOnInit` is what
makes it usable — the value has to be there on the first render, or it is the
guess again — and only a successful config response writes to it.

`ChatForm` also still subscribed to `centerFormOnLanding` directly, an
app-level persisted preference the chat feature only consumes. `ChatView`
already reads it for `Landing`, and now passes it in beside `isLandingPage` and
`footerBelow`, so the composer's clearance is computed entirely from what the
host handed it.

Findings:
#15838 (comment)
#15838 (comment)

* 🔍 fix: Give the Context Popover Its Own Foreground

The popover is portaled to the body with a surface colour and no text colour, so anything inheriting \ there took the user-agent default black: the insights toggle rendered at 1.3:1 on the dark canvas and was invisible until hovered. The panel now names \ beside its surface, as the shared hover-card surface already does.

* 🧮 fix: Show the Cached Share Beside Its Tokens

The cached and cache-write rows were built without a max, and the row renders its share only when one is given, so both printed a bare token count while every row around them carried a percentage. Both now key the window, and the cached test pins the share.

The insights toggle also names its resting colour here: the ghost variant defines only a hover colour, and every other glyph in the panel names a role.
* fix: keep quote selection updates outside React renders

* fix: stop animating hidden code execution spinners

* test: guard idle transcripts against continuous animations

* test: Assert accepted paste behavior independently of placeholder resize
@pull pull Bot locked and limited conversation to collaborators Sep 12, 2026
@pull pull Bot added the ⤵️ pull label Sep 12, 2026
@pull
pull Bot merged commit 1ef4aa9 into innFactory:main Sep 12, 2026
11 of 12 checks 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.

2 participants