Convert semantic + palette CSS variable values in style.css from hex to bare
'R G B' channel triplets, and emit Tailwind colors as
rgb(var(--token) / <alpha-value>) via createTailwindColors. This makes opacity
modifiers (bg-surface-primary/50, bg-border-medium/60, etc.) resolve correctly
and remain dark-aware, fixing ~26 existing usages that previously fell back to a
hardcoded light hex.
- Wrap direct var(--token) color usages in CSS rules as rgb(var(--token))
(style.css, Dropdown.css, Tooltip.css) and two inline component styles
- applyTheme writes bare triplets to match the new wrapping
- shadcn tokens (HSL) and the JS palette (hex) are unchanged
- Format with prettier (Alert, MCPStatusBadge, ApiKeys, Memory, etc.) after
--no-verify commits skipped the hook
- Localize the 'Or' auth divider (com_auth_or) instead of a bare literal
- Drop dead InvocationModePicker imports in Skill forms; fix VerifyEmail
unused arg + useEffect deps
- Revert out-of-scope color edits in legacy Files/VectorStore views that
carried pre-existing untranslated-string lint debt
- Update Memory tests to assert status-* tokens (text-status-error,
bg-status-error-subtle) instead of the old hardcoded red classes
Drop ~829 lines of dead styles across style.css (2992->2355) and
mobile.css (323->131): unreferenced classes (legacy token utilities,
orphaned animations, form/prose/scrollbar leftovers), commented-out
blocks, and duplicate/orphaned keyframes. Library-injected (hljs, sandpack,
codemirror, markdown language) and dynamically-applied (scroll-animation,
icon sizes) classes were retained.
Map the leftover semantic colors to tokens: skill error/dirty states and the
selected-version/selected-skill highlights move to status-warning/status-success,
the global indicator to status-success, and the markdown message text to
text-text-primary. Drop the redundant dark: overrides on the dynamic checkbox,
which the Checkbox primitive already handles.
What remains is intentional and stays raw: categorical color sets (category
icons, principal avatars, per-tool toggle accents), brand marks, the
WCAG-tuned toast severities, code/diagram surfaces, scrims, and text-white on
submit/destructive action surfaces.
Remove the shadcn/ui color tokens (background, foreground, card, popover,
muted, accent, secondary, destructive, input) and migrate every usage to
LibreChat semantic surface/text/border tokens.
Add surface-inverted/text-inverted for the neutral inverted CTA and
surface-fixed/text-fixed for controls that must not flip with the theme
(favicon chips, QR container, carousel arrows). New tokens are defined once
in style.css (light + dark), createTailwindColors, the theme types,
applyTheme and the default/dark theme objects so they stay overridable at
runtime.
Collapse paired dark: color variants into the dark-aware tokens and tokenize
the remaining raw palette and white/black utilities, mapping status colors to
the status-* tokens and legacy ring-black/ring-white focus rings to
ring-text-primary.
Retain the background, primary and ring tokens, which are still referenced by
the SidePanel/Agents and SidePanel/Builder panels (excluded from this pass).
Tokenize TemplateTable th/td/border classes (surface-primary, border-light,
text-primary/secondary) and FileUpload status colors (text-text-secondary,
text-text-destructive, text-status-success) plus the import button hover.
Map the DangerButton, the Data tab destructive actions (RevokeKeys, ClearChats,
DeleteCache), and the DeleteAccount button from bg-red-*/bg-destructive to the
surface-destructive tokens.
Delete-confirm buttons to surface-destructive tokens (MemoryCardActions,
BookmarkCardActions), drop redundant text-white on submit Buttons (the variant
already sets it), legacy preset button green hover/focus to submit tokens, and
slider hover borders to border-light. Leaves DynamicCheckbox dark overrides for
a separate pass against the Checkbox component.
Migrate the last banners to the Alert component: ResetPassword success,
MessageContent connection error, and MemoryInfo storage-full errors. Tokenize
the Agents ErrorDisplay error state in place (icon badge, headings, message,
retry button) since it's a full error state, not a compact callout. Also
tokenize ResetPassword field-validation errors to text-text-destructive
(fixes the low-contrast dark:text-red-900).
Add a reusable Alert component (@librechat/client) with error/success/warning/
info/neutral variants backed by the status-color tokens, default per-variant
icons, and role=alert. Migrate the duplicated colored-div banners to it:
Auth ErrorMessage, RequestPasswordReset success, and the identical error boxes
in ToolSelectDialog, AssistantToolsDialog, and MCPToolSelectDialog.
Migrate the genuine status badges to the status-* tokens: MCPConfigDialog
connection pills (info/warning/neutral/error/success + dot), MemoryUsageBadge
usage levels, and DialogImage quality badge (also gains dark-mode support it
previously lacked). Removes hardcoded colors and dark: twins.
Add a status-color layer (status-{success|info|warning|error|neutral} plus
-subtle variants) to style.css and the unified createTailwindColors map, with a
blue palette for the info hue. Migrate MCPStatusBadge (badges + dots) and
MCPCardActions to the new tokens, removing all hardcoded status colors and
dark: twins. Status colors are now themeable like the rest of the system.
Migrate the Conversations sidebar section to semantic tokens: focus rings to
ring-text-primary (keeps >=3:1 contrast in both modes; the mid-gray ring would
fail WCAG 1.4.11 on dark), the active-conversation indicator and hover-fade
gradient to surface/text tokens, and the pagination controls. Removes every
dark: color twin; no behavior change.
Both the client SPA and @librechat/client Tailwind configs now consume one
createTailwindColors() map, eliminating config drift. Fixes the package-side
build along the way: shadcn tokens are wrapped in hsl(), the broken opacity
helper is removed, and text-destructive/border-destructive/switch-unchecked
plus the gray/green palettes are included.
* 🛡️ fix: Guard Prompts and Mention popovers against empty-result navigation
* 🛡️ fix: Prevent Tab default and clear stale filter on empty popover close
* ✨ feat: Add Google url_context Param with Native YouTube Video Understanding
Mirror the web_search grounding wiring for a new Google/Gemini `url_context`
model param (resolves to the native `urlContext` tool). When enabled, YouTube
URLs in the latest user message are injected as Gemini video parts (fileData),
since the URL Context tool does not support YouTube.
* 🎞️ fix: Provider-aware YouTube injection limits for url_context
Address Codex review on the YouTube video-understanding path:
- Cap injected YouTube parts per request by provider/model (Vertex: 1; Gemini
Developer API: 10 on 2.5+, 1 on earlier models) so multi-link messages cannot
exceed the provider limit and get rejected.
- Set a video/mp4 mimeType on Vertex YouTube fileData (matching Vertex samples);
the Developer API still omits it.
* 🧩 fix: Round-trip url_context for Google-compatible custom endpoints
Add url_context to openAIBaseSchema so the per-chat value persists for custom
endpoints configured with customParams.defaultParamsEndpoint: 'google', matching
how web_search is already picked there.
* 🚦 fix: Gate url_context tool to Gemini 2.5+ models
Per Google's URL Context supported-models list (2.5+/3.x only), skip the native
urlContext tool on earlier models (debug-log + no-op) instead of sending it and
triggering a provider 400. This also gates the coupled YouTube video-understanding
injection to 2.5+, since it keys off the resolved urlContext tool.
* ✂️ fix: Strip YouTube URLs from urlContext text; keep url_context out of OpenAI schema
- Remove url_context from the shared openAIBaseSchema (revert): it is Google-only
and would otherwise leak as an unsupported param to OpenAI/Azure/OpenRouter
requests. On Google-compatible custom endpoints url_context is enabled via admin
addParams/defaultParams, same as web_search.
- When injecting YouTube video parts, strip the matched YouTube URLs from the prompt
text so the urlContext tool (which reads URLs from text and cannot fetch YouTube)
does not consume its URL budget on them. Non-YouTube URLs are left intact.
* 🎯 fix: Refine url_context model gating and YouTube injection edges
Address Codex round 4:
- Exclude non-text modality variants (image/live/tts) from URL Context support,
mirroring the Google tool-combination modality exclusion.
- Use the resolved run model (model_parameters.model) for YouTube injection limits
instead of the saved base model.
- Strip only the YouTube links actually routed to video (id-aware); keep over-limit
links in the text so the model can still reason about them.
- Keep timestamped YouTube links (?t=/&start=) in the text so the moment cue survives.
- Recognize youtube-nocookie.com/embed links.
* 🎚️ fix: Exclude audio Gemini variants + preserve pre-id YouTube timestamps
Address Codex round 5:
- Add `audio` to the url_context modality exclusion so audio-only Gemini variants
(e.g. gemini-2.5-flash-preview-native-audio-dialog) skip the tool instead of 400ing.
- Detect YouTube timestamps anywhere in the matched URL (incl. before `v=`, e.g.
watch?t=90&v=<id>), so timestamped links are kept in the prompt text as intended.
Otherwise, it's possible for a config to override the `isValidAgentId` check.
Without that check, it's possible to query `getAgentById()` with a blank `agent_id`,
which can result in polluting the `QueryKeys.agent` cache with a full list of agents
(instead of just a single agent result).
* 🐛 fix: Prevent Infinite Render Loop on Code-Execution File Preview
Loading a conversation that contains a large (>1MB) code-execution
office file crashed the whole app with React error #185 ("Maximum
update depth exceeded") on hard refresh.
Root cause (client-only): the terminal-write effect in
useAttachmentPreviewSync writes the resolved preview record back into
messageAttachmentsMap with a fresh object identity on every run, and
`attachment` is in the effect's dependency array. useAttachments
re-derives `attachment` ({...db, ...liveEntry}) with a new identity on
every map write, so once polling resolves (pending -> ready on a loaded
conversation) the effect ping-pongs forever:
setAttachmentsMap -> re-derive -> effect -> setAttachmentsMap.
Only files large/slow enough to defer extraction are persisted at
status: 'pending', which is why small documents never triggered it.
Fix: an idempotency gate that bails before setAttachmentsMap when the
merged attachment already carries the resolved status/text/textFormat/
previewError. The write happens once and then settles.
Tests:
- useAttachmentPreviewSync.loop.spec.tsx wires the real
useAttachments -> hook feedback to reproduce the loop (verified to
throw #185 without the gate, settle with it).
- e2e/specs/mock/attachment-preview-loop.spec.ts loads a conversation
with a pending code-exec attachment whose preview resolves ready and
asserts the app does not crash.
Closes#13916
* 🔧 feat: Make Office Preview Extraction Cap Configurable (default 2MB)
The inline code-execution preview extraction ceiling was a hardcoded 1MB
constant (MAX_TEXT_EXTRACT_BYTES). Office/text artifacts over that skip
the inline preview and resolve to "Preview unavailable" (download-only).
Make it configurable via FILE_PREVIEW_MAX_EXTRACT_BYTES and raise the
default to 2MB so larger documents get an inline preview out of the box.
The rendered HTML remains independently capped at MAX_TEXT_CACHE_BYTES
(512KB), so image-heavy files over that still fall back to the existing
"preview too large" banner rather than rendering unbounded output.
- resolveMaxTextExtractBytes(env) parses the override, falling back to
2MB on missing/non-numeric/non-positive values (warns on invalid).
- Documented in .env.example next to the other file-size limits.
- Unit tests cover default, valid override, fractional flooring, and
invalid fallback.
* 🐛 fix: Guard sub-byte preview cap from flooring to zero
A fractional FILE_PREVIEW_MAX_EXTRACT_BYTES in (0, 1) passed the
positive-number check then floored to 0, making MAX_TEXT_EXTRACT_BYTES
zero and treating every non-empty artifact as oversized. Floor first,
then require the result to be >= 1 byte before accepting it; otherwise
fall back to the 2 MB default. Adds coverage for the sub-byte case.
* ✅ test: Make exported-ceiling assertion env-independent
The "exported ceiling" assertion compared MAX_TEXT_EXTRACT_BYTES to a
literal 2 MB, but that const is initialized from
FILE_PREVIEW_MAX_EXTRACT_BYTES at module load — so the suite would
falsely fail when run with the override set. Assert the export tracks
resolveMaxTextExtractBytes(env) for the current environment instead; the
undefined-case test continues to pin the 2 MB default.
* 🖱️ fix: Summon Quote Popup on Double-Click Word Selection
Chromium commits a double-click word selection on the `dblclick` event, after `mouseup` has already read a still-collapsed range, so the "Add to chat" popup never appeared for double-click selections. Listen for `dblclick` in addition to `mouseup`/`keyup`.
Adds an e2e covering a native double-click word selection (measured-coordinate dblclick exercises the real browser path, unlike the programmatic-Range helper).
* 🎯 test: Target Reply Text Node in Double-Click Quote E2E
Walk to the text node containing the needle (not the first text node in .message-render, which may be a select-none screen-reader/model-label header) and measure the needle's first character, so the native double-click lands on the reply word rather than metadata.
Resolve the new-chat default spec from the most recent conversation setup
(LAST_CONVO_SETUP_0) instead of reconstructing intent from accumulated
cross-endpoint history. Removes hasStoredModelValue, hasStoredPrefixValue,
hasStoredModelSelection, the sticky LAST_SPEC read, the nested
resolveSoftDefault closure, and the duplicated prioritize/modelSelect branches.
Fixes the soft default being dropped on New Chat ("Select a model") when its
preset endpoint sits outside modelSpecs.addedEndpoints alongside a custom
endpoint: a model lingering in LAST_MODEL for that endpoint no longer
suppresses the soft default.
Clear All Chats now also clears LAST_SPEC/LAST_MODEL/LAST_TOOLS so a new chat
afterward cleanly returns to the soft default. Adds the cross-endpoint unit
case, a clearAllConversationStorage test, and a cold-load e2e regression test.
* feat: add useKeyboardShortcuts hook and showShortcutsDialog atom
Implements the core keyboard shortcuts hook with 11 shortcuts:
- General: new chat, focus input, copy last response
- Navigation: toggle sidebar, model selector, search, settings
- Chat: stop generating, scroll to bottom, temporary chat, copy code
Also adds the showShortcutsDialog atom to control dialog visibility.
Closes#3664
* feat: add KeyboardShortcutsDialog component
Renders a modal dialog listing all available keyboard shortcuts
grouped by category (General, Navigation, Chat). Features:
- Platform-aware key labels (⌘ on Mac, Ctrl on others)
- Clean kbd-style key badges with subtle shadows
- Grouped sections with separators
- Sticky footer with shortcut to open the dialog itself
- Single close button, Escape to dismiss
* feat: integrate keyboard shortcuts into Root layout and account menu
- Mount useKeyboardShortcuts and KeyboardShortcutsDialog in Root.tsx
via a KeyboardShortcutsProvider wrapper (only renders post-auth)
- Add 'Keyboard Shortcuts' menu item with Keyboard icon to the
account settings popover for discoverability
* chore: add data-testid to model selector button
Adds data-testid="model-selector-button" to the model selector
trigger for reliable DOM targeting by keyboard shortcuts and tests.
* i18n: add keyboard shortcuts localization keys
Adds 12 new com_shortcut_* translation keys for the keyboard
shortcuts feature: group labels, action labels, and dialog title.
* style: fix keyboard shortcuts dialog dark mode
Replace token-based dark mode styling with explicit white-alpha
values for kbd badges, borders, and separators:
- Kbd: dark:bg-white/[0.06] dark:border-white/[0.08] dark:shadow-none
- Separators: dark:border-white/[0.06]
- Dialog border: dark:border-white/[0.06] dark:shadow-2xl
Ensures the key badges blend naturally into the dark surface
instead of appearing as harsh bright rectangles.
* feat(shortcuts): add definitions for 8 new keyboard shortcuts
Add shortcut definitions and localization keys for:
- Upload file (Cmd/Ctrl+Shift+U)
- Toggle right sidebar (Cmd/Ctrl+Shift+R)
- Regenerate response (Cmd/Ctrl+Shift+E)
- Edit last message (Cmd/Ctrl+Shift+I)
- Scroll to top (Cmd/Ctrl+Shift+↑)
- Archive conversation (Cmd/Ctrl+Shift+A)
- Delete conversation (Cmd/Ctrl+Shift+Backspace)
Addresses #3664
* feat(shortcuts): implement handlers for all new shortcuts
New handlers:
- Upload file: triggers attach-file button click
- Toggle right sidebar: clicks parameters-button
- Regenerate response: clicks regenerate-generation-button
- Edit last message: finds last user-turn and clicks edit button
- Scroll to top: scrolls main[role=main] to top
- Archive conversation: calls archive mutation + navigates to new chat
- Delete conversation: calls delete mutation + navigates to new chat
Improvements:
- Use getMainScrollContainer() helper targeting main[role=main]
instead of fragile class-based selectors
- Use data-testid selectors instead of aria-label substring matching
for stop-generation and model-selector buttons
- Use id-based selectors (button[id^=edit-]) for edit buttons
- Add isEditing guard to skip shortcuts when user is typing in
inputs, textareas, or contentEditable elements
- Refactor handler from if/return chain to switch statement for
cleaner flow control
* fix(shortcuts): increase dialog scroll height for expanded shortcut list
With 20 shortcuts across 3 groups, the previous 480px max was tight.
Increase to 560px / 70vh so all shortcuts are visible without
excessive scrolling.
* refactor(shortcuts): use data-testid selectors for reliable targeting
Add data-testid="nav-settings" to the Settings menu item in
AccountSettings so the open-settings shortcut no longer relies on
fragile text-content matching ('Settings' but not 'Keyboard').
* refactor(shortcuts): two-column layout for shortcuts dialog
Split the shortcuts dialog into a two-column grid layout:
- Left column: General + Navigation groups
- Right column: Chat group (which has the most shortcuts)
Reduces vertical height so the full list is visible without scrolling.
Widen dialog to max-w-4xl (w-11/12) to accommodate both columns.
Simplify Kbd/group styling for cleaner visual density.
* refactor(shortcuts): adjust padding in KeyboardShortcutsDialog content
* feat(shortcuts): customizable keyboard shortcuts with recorder UI
Add per-shortcut overrides stored in localStorage, a recorder component
for capturing new key combos with conflict detection, and a per-row
edit/reset affordance in the shortcuts dialog.
* test(shortcuts): fix specs broken by keyboard shortcut hooks
- ExpandedPanel: add customShortcuts atom to the store mock so
useShortcutDisplay/useShortcutAriaKey can read state
- AttachFileMenu: update queries to the new 'Attach Files' aria-label
- Button (Generations): wrap renders in RecoilRoot now that the
component reads shortcut state
* feat(shortcuts): add panel/submit/bookmark/continue/read-aloud shortcuts
- Wire stop, regenerate, continue, and read-aloud handlers to existing
buttons via data-testid, fixing handlers that previously queried
selectors with no matching DOM nodes.
- Add data-testid='nav-panel-${id}' to expanded sidebar nav buttons so
the panel-opener shortcuts can target them.
- Add new shortcut definitions and handlers: submitMessage,
bookmarkConversation, continueResponse, readAloudLastResponse, and
the open* panel openers (assistants, agents, prompts, memories,
parameters, files, bookmarks, MCP).
- Drop the toggleRightSidebar shortcut — there is no right sidebar to
toggle in this codebase.
- Refresh the KeyboardShortcutsDialog layout and ShortcutRecorder for
the new groups, tighten ShortcutKeyCombo styling, and surface the
shortcuts hint chips in the account menu.
* chore(shortcuts): remove unused translation keys
Drop com_shortcut_dialog_subtitle, com_shortcut_not_set, and
com_shortcut_reset_aria — no remaining references in the codebase.
* fix(shortcuts): resolve keyboard shortcut and footer regressions
- Guard the temporary-chat toggle so the shortcut mirrors the UI, only
toggling when the conversation has no messages and is not submitting.
- Stop Ctrl/Cmd+Enter from double-submitting: the main chat textarea
already submits via its own handler, and submit is blocked from
unrelated inputs while still working in the chat box.
- Ignore repeated keydown events (e.repeat) so held keys no longer
re-run toggles or destructive actions.
- Scope archive/delete shortcuts to the conversation in the active
route using useMatch, preventing mutations of a stale background
conversation on non-chat routes.
- Keep the recorder conflict controls clickable by including the whole
editing row in the outside-click containment check.
- Restore privacy policy and terms of service links on public share
pages via an opt-in Footer prop.
- Expand the sidebar before activating panel shortcuts so they are
visible on mobile, and avoid toggling an already-active panel.
* fix(shortcuts): reject bare non-printable shortcut bindings
A recorded non-printable key (Tab, Enter, Backspace, Delete, arrows,
Space) with no Cmd/Ctrl/Alt was treated as valid, so it could be saved
and then hijack navigation or fire destructive actions since the global
handler preventDefaults it outside text inputs. Require Shift at minimum
for these keys, which keeps Shift+Escape (focusChat) valid while
rejecting bare single-key bindings.
* style: fix import order drift across keyboard shortcut files
* fix(shortcuts): guard actions behind dialog and resolve reset conflicts
- Ignore global shortcut actions while the shortcuts dialog is open
(except the toggle that closes it), so a combo like delete/archive
can no longer fire on the conversation behind the modal.
- When resetting a shortcut to its default, unbind any other action
whose custom binding collides with that restored default, so Reset
after a Replace can't leave two rows sharing one binding with one
action unreachable.
* fix(shortcuts): keep attach menu button accessible name stable
The shortcut pass changed the attach menu button's aria-label from the hardcoded "Attach File Options" to localize('com_sidepanel_attach_files') ("Attach Files"), which changed its accessible name and broke the provider-file e2e specs that locate it by name. Restore the original label and keep only the added aria-keyshortcuts.
* fix(shortcuts): gate temporary chat toggle to chat routes
The Root-level listener runs on non-chat routes (search, settings, panels) where the last loaded conversation may be empty, so Ctrl/Cmd+Shift+T could flip the hidden isTemporary state without the TemporaryChat control being visible. Require an active chat route (routeConvoId) before toggling.
* test(shortcuts): align attach menu spec with button accessible name
The attach menu button's aria-label was restored to "Attach File Options" (matching dev and the provider-file e2e specs), so update the unit test's button queries from /attach files/i to /attach file options/i. All 26 cases pass.
* fix(shortcuts): target conversation bookmark and reveal search panel
- Bookmark: query the unique #bookmark-menu-button so the shortcut
bookmarks the current conversation. The previous
querySelector('[data-testid="bookmark-menu"]') matched the sidebar
tag-filter button first (same testid, earlier in the DOM), toggling
the filter instead of bookmarking.
- Focus search: activate the conversations panel before focusing, since
the search input only mounts there and the sidebar renders just the
active panel. Route through the nav-panel-conversations button (the
listener is outside ActivePanelProvider) and settle before focusing,
so Ctrl/Cmd+/ works from any panel.
* fix(shortcuts): preserve footer links, cross-platform bindings, modal guard
- restore unconditional legal footer links (drop showLegalLinks gate)
- keep untouched platform's default when customizing a binding
- round-trip bindings whose key is the plus character
- suppress global shortcuts while any modal dialog is open
- tag read-aloud test id only on assistant turns
* fix(shortcuts): include non-Radix dialogs in the modal guard
The guard only matched Radix dialogs via data-state="open", missing
Headless UI dialogs (e.g. the redesigned Settings modal) that render
role="dialog" without data-state. Iterate all dialog/alertdialog nodes
and treat one as open unless it is inert or data-state="closed", which
also avoids false positives from always-mounted inert panels.
* fix(shortcuts): gate temporary chat toggle behind TEMPORARY_CHAT permission
* fix(shortcuts): only prevent native key event when shortcut action runs
* fix(shortcuts): rebind temporary chat, open settings without toggling menu, release no-op keys
* fix(shortcuts): confirm conversation delete, use clipboard fallback, add tests
* fix(shortcuts): navigate to new chat after keyboard-confirmed delete
* fix(shortcuts): copy last response via message button, guard unavailable controls
* fix(shortcuts): keep custom Enter-based submit bindings working in the composer
* fix(shortcuts): restrict shift-only bindings to safe keys
* fix(shortcuts): submit custom Enter chords in the composer without inserting a newline
* fix(shortcuts): block global shortcuts while a menu overlay is focused
* fix(shortcuts): rebind archive off the browser-reserved Ctrl+Shift+A
* fix(shortcuts): honor submitMessage overrides in the composer
* chore: Update `@ariakit/react` and `@ariakit/react-core` dependencies to v0.4.29 and v0.4.26 respectively, and add new `@ariakit/components`, `@ariakit/react-components`, `@ariakit/react-store`, and `@ariakit/react-utils` packages to package-lock.json and package.json files.
* fix: restore keyboard navigation for Tools dropdown submenus
Compose the Artifacts and MCP submenu triggers as a `MenuButton` that
receives the parent `MenuItem`'s props/ref directly, instead of nesting a
`MenuItem` inside the submenu's own provider and placing the ref on a
wrapper div. This registers the focusable trigger with the parent menu
store so arrow-key navigation reaches the items, which fully broke under
Ariakit 0.4.29.
* fix: Improve keyboard navigation for TokenUsageIndicator popover
Refactor the TokenUsageIndicator component to enhance keyboard accessibility. The popover now maintains focus on the gauge trigger, ensuring that the Escape key closes the popover without shifting focus to the non-interactive panel. Additionally, the autoFocusOnShow property is set to false to prevent unwanted focus behavior when the popover is displayed.
* fix: Stabilize focus and layout shift in Archived Chats dialog
Anchor dialog focus to the content element so rapid tabbing during the
virtualized table's loading state no longer escapes to the page's top
focus guard, and stabilize the columns memo to keep the focus trap intact.
Reserve a fixed height and stable scrollbar gutter, and drop the redundant
nested scroll wrapper in the shared DataTable to eliminate load-time
layout shift.
* fix: Add stable scrollbar gutter to SharedLinks DataTable
Enhance the layout stability of the SharedLinks component by adding a "scrollbar-gutter-stable" class to the DataTable. This change aims to prevent layout shifts during loading, improving the overall user experience.
* fix: Enhance keyboard accessibility and focus management in TokenUsageIndicator
Refactor the TokenUsageIndicator component to improve keyboard navigation and focus behavior. Introduced a useRef hook for the disclosure button to ensure focus remains on the gauge trigger when the popover is opened. Updated the popover's finalFocus property to return focus to the trigger on close, enhancing the overall user experience for keyboard users.
* 🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup
Add a ChatGPT/Codex-style quote feature: selecting text in any message shows
an 'Add to chat' popup that accumulates removable quote chips above the
composer. On submit, the excerpts are merged into the user message text as
Markdown blockquotes (counted in the user message token count, not a system
message) and persisted on the message so they render on the user bubble and
survive reload.
- packages/api: add getReferencedQuotes + mergeQuotedText helpers (blockquote merge, length/count caps) with unit tests
- BaseClient.sendMessage: temporarily merge req.body.quotes into userMessage.text before buildMessages, restore clean text, persist quotes array
- data-schemas + data-provider: add optional quotes field to message schema/type
- client: pendingQuotesByConvoId atom, QuoteButton selection popup, PendingQuoteChips composer row, MessageQuotes persistent display
- useChatFunctions: drain pending quotes onto the message, carry forward on regenerate
- add localization keys and component/integration tests
* 🧪 test: Add Playwright e2e for chat quote feature
Add e2e/specs/mock/quotes.spec.ts covering select -> 'Add to chat' popup ->
chip -> send -> persistent reference block -> reload, plus multi-select
accumulation and chip removal. Selection is driven programmatically (real DOM
Range + dispatched mouseup) to summon the popup deterministically.
Add data-testid hooks (add-to-chat-button, pending-quote-chips, message-quotes)
to the quote components for stable selectors.
* 🛡️ fix: Address Codex review on quote feature
- Run PII filter + OpenAI moderation over req.body.quotes (P1): quoted excerpts
are merged into the model-facing user message, so they must clear the same
filters; a crafted quotes payload could otherwise bypass them. Adds tests.
- Carry quotes through edit/save-and-submit replays (overrideQuotes in
EditMessage), mirroring overrideManualSkills, so edited turns keep context.
- Hide the quote UI for Assistants endpoints (which bypass BaseClient merge),
so users can't queue quotes the assistant never receives.
- Clear pending quote/skill queues by resolved conversationId in useClearStates,
not the UI index, so queued-but-unsent selections don't linger in Recoil.
- Cap queued quotes client-side at 10 to match the backend QUOTE_MAX_COUNT, so
the composer never shows more quotes than are actually sent.
* 🧵 fix: Durably re-merge quotes + Codex round 2
Address Codex's re-review of the quote feature:
- Durable history re-merge (per maintainer decision): quotes are no longer
merged at request time and stripped; instead each user message's persisted
message.quotes is merged into its formatted content in AgentClient.buildMessages
(new prependQuotes helper) for current AND historical turns. The model
receives the referenced context on every prompt and the token count stays
consistent with what was persisted; stored text stays clean for display.
- Attach normalized quotes to the user message in handleStartMethods (before
getReqData/onStart) so the optimistic bubble, resumable abort metadata, and
saved row all carry them (fixes the abort-metadata gap).
- Skip the quote drain entirely for Assistants endpoints in useChatFunctions,
leaving the pending atom intact (UI is already hidden there).
- Normalize req.body.quotes via getReferencedQuotes before moderation/PII so
only the trimmed/truncated/capped excerpts the model will receive are checked.
- Tests: prependQuotes unit tests; BaseClient quote tests assert early
attachment + clean text; e2e now verifies the model receives the merged
blockquote on the current turn and re-merged from history on a later turn
(new E2E_ASSERT_QUOTE mock marker).
* 🔗 fix: Quote share/memo/abort/PII gaps (Codex round 3)
- Shared links: include quotes in the anonymized projection + SharedMessage
type (+test) so the /share view renders the same reference blocks as the
owner, mirroring manualSkills/alwaysAppliedSkills.
- MessageRender memo: compare quotes length so a server/resume copy whose only
change is the quote list re-renders (the block no longer goes stale/missing).
- Resumable job metadata: include quotes in the userMessage written to
GenerationJobManager so a reload/reconnect mid-stream reconstructs the chips.
- PII + moderation: also scan the merged blockquote+text exactly as the model
receives it, so a secret split across a quote and the typed body (each clean
alone) is caught (+cross-boundary test).
- e2e: make quote-add robust against the auto-scroll-dismisses-selection race
via a retried select+click helper.
* 🛑 fix: Keep quotes on aborted turn's request message (Codex round 4)
abortMiddleware reconstructs finalEvent.requestMessage from jobData.userMessage
but only copied ids + text; include quotes so a stopped quoted turn keeps its
MessageQuotes in the UI and a regenerate-before-reload still sends the
referenced context. Completes the resumable-metadata fix from the prior round.
* 🧮 fix: Quote recount + preliminary abort metadata (Codex round 5)
- Force a canonical token recount for messages carrying quotes in
AgentClient.buildMessages, so a plain text-only Save edit (which recomputes
tokenCount from text alone) can't leave a stale, quote-excluding count that
undercounts context on later turns — recount from the quote-merged copy
self-heals it.
- Seed normalized quotes into the preliminary userMessage metadata
(getPreliminaryUserMessage), so an abort during init/tool-loading (before
onStart) still reconstructs the stopped turn's MessageQuotes.
* ✅ fix: Add getReferencedQuotes to controller test mocks (CI)
request.js's getPreliminaryUserMessage now calls getReferencedQuotes; the
agents controller specs mock @librechat/api wholesale, so the mock must export
it or the call throws and cascades. Added a faithful mock (normalize/cap,
null when empty) to request.resumeMetadata.spec.js and jobReplacement.spec.js.
* 📐 fix: Quotes in context projection + resumable metadata (Codex round 6)
- Context-usage projection (resolveContextProjection): select message.quotes,
prepend them into the projected user text, and recount quoted messages so the
context gauge counts the same prompt the model receives (a text-only Save edit
no longer makes the gauge undercount / over-report remaining budget).
- Resumable job metadata: trackUserMessage (created-event rewrite) and abortJob
(final requestMessage) now carry quotes; SerializableJobData.userMessage and
CreatedEvent.message gained an optional quotes field. With the cross-replica
created-event spread, stopping/reconnecting a quoted turn after the created
event keeps its MessageQuotes.
* 💬 feat: Collapse multi-select quotes into one chip with hover popup
Composer feedback: the quote chip area now shows a single chip — the excerpt
text for one selection, or a collapsed "{n} selections" pill for multiple,
with a hover popup (HoverCard) listing every excerpt and a per-item remove. The
chip is taller (py-1.5/text-sm) to read less skinny. Adds com_ui_quote_selections
and com_ui_remove_all_quotes; updates unit + e2e tests (e2e drives the count via
a data-quote-count hook and exercises the hover popup).
* ♿ fix: Make multi-selection quote popup keyboard accessible
The collapsed "{n} selections" pill used a HoverCard, which Radix only opens on
pointer hover — its interactive content was unreachable by keyboard. Replaced it
with a Popover: the trigger is a real button that opens on click / Enter / Space
(focus moves into the list, each excerpt's × is tab-navigable, Escape closes and
restores focus), with hover-open preserved for mouse via controlled open state +
a close grace period. Hover-initiated opens skip auto-focus so they don't pull
focus off the composer. Adds an e2e asserting keyboard open/close.
* 📐 fix: Clamp the Add-to-chat button within the viewport (Codex round 7)
The floating selection button positioned via translate(-50%,-100%) (bottom-center
anchor) but clamped top/left as if they were its top-left, so a selection near
the viewport top or sides could render the button partly/fully offscreen. Now it
measures the button (ref + useLayoutEffect) and computes an on-screen top-left —
clamping by the full width within side margins and flipping below the selection
when there's no room above — with no transform, and stays hidden until measured
so it never flashes at an unclamped spot.
* ↩️ fix: Restore pending quotes on early-abort draft (Codex round 8)
When a turn is stopped before the created event (e.g. during tool/MCP init), the
final handler restores requestMessage.text to the draft, but the pending-quote
atom was already drained on submit — so a retry sent no quotes. The abort
requestMessage now carries quotes (preliminary metadata + abort fixes), so the
three early-abort/no-response draft-restore paths in useEventHandlers now also
re-queue pendingQuotesByConvoId from requestMessage.quotes.
* ♿ fix: Use Ariakit Popover for quote selections (keyboard focus)
The multi-selection popup used a hand-rolled Radix Popover with Popover.Anchor +
a manual button, so Radix had no trigger to return focus to — Escape dumped
focus to the page top. Refactored to Ariakit (the codebase's popover primitive,
per DropdownPopup/Fork): the `PopoverDisclosure` is the real trigger, so Escape
closes and returns focus to the composer instead of the top of the page. Keyboard
opens (Enter/Space) autofocus into the list and tab through each excerpt's remove;
hover opens for mouse with autofocus suppressed so it never pulls focus off the
composer. e2e asserts the keyboard open/navigate/Escape flow keeps focus on a
real control (never BODY).
* 🔗 feat: Snapshot Files for Shared-Link Attachments
Shared-link viewers could read a shared conversation snapshot but not its
attachments: file preview/download still went through the owner-scoped file
ACL (the /api/files router sits behind requireJwtAuth + owner/agent checks),
so anonymous viewers got 401s and authenticated non-owners got 403s — the
repeated `[fileAccess] denied` warnings seen for the preview poller.
Capture an immutable per-share file snapshot (embedded on the SharedLink
document, referencing the original stored object — no byte copy) at share
create/update, and serve those files through new share-scoped routes
authorized by the existing shared-link view permission (public/ACL) plus
snapshot membership, never the owner's live file ACL.
- data-schemas: fileSnapshots on the share doc; capture in create/update;
read-time rewrite of filepath/preview to /api/share/:id/files/:fileId;
getSharedLinkFile + lazy backfillSharedLinkFiles for legacy links
- api: GET /api/share/:shareId/files/:file_id[/download|/preview]; route
context added to fileAccess denial logs
- packages/api: isFileSnapshotEnabled resolver (env + yaml)
- data-provider: interface.sharedLinks.snapshotFiles (default on) + client
endpoints/services
- client: ShareContext.shareId wired to Image, preview hook, and downloads
- config: SHARED_LINKS_SNAPSHOT_FILES env override (default on)
* 🔒 fix: Address Codex review on shared-link file snapshots
Triage of the Codex review on PR #13740 (2 P1, 7 P2 — all valid):
- P1 (cross-user access): scope the snapshot lookup to the sharing user's own
files so a message referencing another user's file_id can't widen access.
- P1 (stored XSS): the inline share-file route now serves only safe preview
types inline (raster images/pdf); everything else is forced to attachment with
X-Content-Type-Options: nosniff.
- Stream shared downloads by default; redirect to a signed URL only on
?direct=true (blob/XHR callers work without bucket CORS).
- Read preview status live from the file record (always current for deferred
previews) and stop embedding extracted text in the share doc (16MB-limit risk).
- Only lazily backfill when the fileSnapshots field is absent (legacy), not on
every snapshot miss.
- Backfill legacy shares before rewriting message URLs, and gate URL rewriting
to public shares so non-public (ACL) shares keep prior behavior (img/anchor
can't carry the bearer token).
- Frontend: only route a download through the share path when the file was
actually snapshotted (rewritten href / filepath), else fall back.
* 🔑 feat: Authorize shared-link files for non-public shares via cookie
Extends shared-link file access to non-public (ACL) shares (Codex finding 5).
`<img>`/anchor requests can't carry the bearer access token, so non-public
shares previously 401'd on file loads. Add an optional cookie-auth fallback on
the share file routes that resolves the viewer from the `refreshToken` cookie
(or signed `openid_user_id` cookie) — the same mechanism secure image links use
(validateImageRequest) — then let canAccessSharedLink run the viewer's ACL check.
- new middleware optionalShareFileAuth (+ unit spec); applied to the three
share file routes after optionalJwtAuth
- URL rewriting in getSharedMessages is no longer gated to public shares (the
route now authorizes header-less requests), so files work uniformly across
public and non-public shares; revert the now-unused req.sharePublic plumbing
* 🔒 fix: Second Codex pass on shared-link file snapshots
Addresses the follow-up Codex findings on PR #13740:
- Don't snapshot transient text-source files: FileSources.text filepaths are
Multer temp paths the upload route deletes, so they can't be streamed —
removed from the streamable allowlist.
- Unset stale snapshots on a disabled-feature update: updateSharedLink now
$unsets fileSnapshots when snapshotFiles is false, so an opted-out update
can't keep serving file ids the update dropped.
- Load tenant config after share resolution: configMiddleware now runs after
canAccessSharedLink (which enters the share's tenant ALS context), so
per-tenant interface.sharedLinks.snapshotFiles overrides apply to anonymous
public views.
- Return a clean 404 when the snapshotted object is gone: resolveShareFile now
requires the live file record and 404s if it's been deleted/expired, instead
of letting the stream error after headers are sent (ENOENT / 500).
(The re-flagged P1 about private-viewer rewriting was already fixed in the prior
commit's cookie-auth change.)
* 🔒 fix: Third Codex pass on shared-link file snapshots
Addresses the third Codex review pass on PR #13740:
- P1: keep shared previews/files pinned to the snapshotted version. Snapshot the
small previewRevision; resolveShareFile 404s when the live file's revision no
longer matches (file_id reused/overwritten by a later turn), so old links can't
surface post-share content — covers both preview text and streamed bytes.
- Honor the toggle as a kill switch: resolveShareFile 404s when snapshotFiles is
disabled, instead of only skipping backfill, so disabling stops serving
already-snapshotted file URLs.
- Lazy-sweep orphaned 'pending' previews to 'failed' in the share preview route
(mirrors the owner route) so the client poller reaches a terminal state.
- Resolve the cookie-fallback user in runAsSystem so strict tenant isolation
doesn't throw before canAccessSharedLink establishes the share tenant context.
* ✨ feat: Per-link "share files" checkbox for shared links
Add a checkbox to the share-link dialog (checked by default) letting the user
choose whether to include the conversation's files in the shared link, with
copy explaining images/files won't be visible to viewers otherwise. Opting out
skips snapshot creation/serving for that link.
- client: ShareButton renders the checkbox gated on the new
startupConfig.sharedLinksSnapshotFilesEnabled flag; state threads through
SharedLinkButton into the create/update mutations as `snapshotFiles`.
- data-provider: createSharedLink/updateSharedLink send `snapshotFiles` in the
body; TStartupConfig gains `sharedLinksSnapshotFilesEnabled`.
- api: POST/PATCH /api/share compute snapshotFiles as
isFileSnapshotEnabled(req.config) && body.snapshotFiles !== false (admin gate
AND per-link opt-out); config.js exposes the effective enabled flag to clients.
- en locale: com_ui_share_files (+ _description).
* 🐛 fix: Make the "share files" opt-out actually hide files
Unchecking "share files" at creation didn't hide anything: the shared message
JSON still carried each file's original (e.g. static-served) path, and because
opting out only meant "no fileSnapshots field" — indistinguishable from a legacy
link — getSharedMessages would backfill snapshots on first view whenever the
admin feature was on, re-enabling files entirely.
Fix by persisting and honoring the per-link choice:
- Store `snapshotFiles` (boolean) on the SharedLink so opt-out is distinct from a
legacy link; set it on create and update.
- getSharedMessages computes includeFiles = adminEnabled && link not opted out;
when excluded it strips files/attachments from the payload (no original-path
leak) and never backfills the opted-out link.
- Surface the stored choice via getSharedLink so the dialog checkbox reflects an
existing link's actual setting instead of always defaulting to checked.
Note: changing the checkbox on an already-created link still applies only when
the link is refreshed (which regenerates the URL) — a UX follow-up.
* 🔒 fix: Close remaining shared-link file opt-out leaks (Codex)
Follow-up to the per-link opt-out, addressing the third Codex pass:
- Honor the opt-out on the file route too: getSharedLinkFile now returns the
link's `optedOut` choice; resolveShareFile 404s (and never backfills) an
opted-out link, so a direct /files/:id request can't re-create snapshots.
- Make read/serve viewer-independent: the gate no longer uses the viewer's
resolved config (isFileSnapshotEnabled(req.config)) — it uses the link's stored
choice plus a global env-only kill switch (isFileSnapshotKillSwitchActive). A
viewer's own interface.sharedLinks.snapshotFiles can no longer hide a link's
files. Create/update still use the creator's config to set the per-link choice.
- Neutralize render URLs for non-snapshotted files: applyShareFileRoute now
strips filepath/preview for any file/attachment not in the snapshot, so the
owner's original (e.g. static) path can't be loaded through the share.
* 🔒 fix: Harden shared-file version pinning and local path handling (Codex)
- Refuse reused/overwritten file snapshots more broadly: resolveShareFile now
refuses to serve when either previewRevision OR `bytes` changed vs the
snapshot. `bytes` catches non-office reused outputs (e.g. code-exec
same-filename images that lack previewRevision) and is stable across S3 URL
refresh and the pending->ready transition. Same-size content swaps remain a
best-effort gap inherent to the no-byte-copy design.
- Strip cache-busting query strings before local streaming: code-output images
add `?v=...` to filepath; the share route now splits it off so getLocalFileStream
resolves the real filename instead of a literal `*.png?v=...` path.
* 💬 fix: Clarify that file-sharing changes apply on link refresh
For an already-created shared link, changing the "share files" checkbox only
takes effect when the link is refreshed (which regenerates the snapshot). Add a
note under the checkbox, shown only when a link already exists, so the behavior
isn't surprising: "Refresh the link to apply this change — files are snapshotted
when the link is refreshed."
* ✨ feat: Add `defaultPinnedTools` interface config for default tool & MCP pinning
Adds an `interface.defaultPinnedTools` string array letting admins pin tools and the MCP servers dropdown to the prompt bar by default for all users.
- Tool keys (artifacts, execute_code, web_search, file_search, skills) pin their badge via `useToolToggle`.
- The keyword `'mcp'` or a configured MCP server name pins the MCP dropdown via `useMCPSelect`.
- Only seeds initial state; a user's stored pin preference always wins. When unset, tools start unpinned and the MCP dropdown keeps its legacy default (pinned).
Unifies the approaches from #11646 (pinnedTools) and #9251 (defaultPinMcp) into one config key.
* 🐛 fix: Apply defaultPinnedTools pin once startupConfig resolves
On a cold load, useToolToggle can mount before useGetStartupConfig() resolves, so defaultPinned starts false and useLocalStorageAlt eagerly persists it; its init effect never re-runs for the later config-driven default. Fresh users would then miss the admin-configured default pin whenever startup config was not already cached.
Capture whether a pin preference existed before mount (pre-seed) and, once startupConfig arrives, apply the real default for users with no prior preference. Runs once and never overrides an existing stored pin, so the conservative behavior for existing users is preserved.
* 🐛 fix: Preserve pin clicks made before startupConfig resolves
The cold-load default-seeding effect captured the stored-pin state only at mount, so a pin toggled before startupConfig resolved was treated as no-preference and overwritten when the admin default applied.
Track explicit pin toggles via a ref (set through the returned setter) and skip the default application when the user has interacted in-session — in addition to the existing stored-preference guard.
* fix: hide artifacts toggle when capability is disabled
The artifacts badge ignored the agent capabilities config, so a pinned
toggle stayed visible after the artifacts capability was turned off.
Gate the component on artifactsEnabled via useAgentCapabilities, matching
how Skills, FileSearch and CodeInterpreter already handle their capability.
* style: fix import order in Artifacts.tsx
* style: Sort mutation type imports
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Adds a "Provider API keys" entry under Settings → Data controls → API keys
that lists every endpoint requiring a user-provided credential and lets users
set or rotate its key via SetKeyDialog. This is always reachable, so keys can
be managed even when `interface.modelSelect` is hidden by `modelSpecs`.
The endpoint list is filtered the same way the mention popover and model
selector menu are:
- No modelSpecs → every user-provided endpoint.
- modelSpecs configured → limited to spec endpoints ∪ `modelSpecs.addedEndpoints`.
- agents reachable (with access) → expanded to the agents `allowedProviders`
(all providers when unrestricted).
Reworks #13303 onto the registry-driven Settings dialog (#13722); the prior
standalone tab and the `APIKeys` directory are superseded (the latter also
collided with the agent `ApiKeys` feature from #13819).
* ✨ feat: Add scroll-to-bottom terminus node to MessageNav
Append the chat's bottom (#messages-end) as a terminal rib in the message
minimap so it is reachable by click, drag-scrub, and the down chevron like
any message. Rendered as a distinct centered dot rather than a line rib, and
gated on the #messages-end sentinel actually existing.
Also clamp each rib's snap target to the container's max scroll so the down
chevron no longer stays stuck enabled at the bottom (the terminus can never
scroll its top to the container top).
* 🐛 fix: Scope MessageNav terminus to its own scroll container
The terminus rib stored the shared constant id 'messages-end', which is
rendered once per MessagesView. With multiple navs mounted, the global
document.getElementById lookups resolved the first chat's sentinel, breaking
the per-instance isolation guaranteed by the existing multi-instance tests.
Resolve the terminus via the nav's own scrollableRef container
(querySelector), leaving the globally-unique message ids on the fast
getElementById path. Adds a multi-instance test covering the terminus.
* 🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration
Adds a user-configurable `promptCacheTtl` parameter (dropdown: 5m | 1h)
alongside the existing `promptCache` toggle for Anthropic, Bedrock, and
OpenRouter endpoints. Default is undefined so the agents SDK applies its
own default (1h), letting users opt down to the legacy 5m TTL.
- data-provider: schema, parameterSettings dropdown, types, bedrock picks
- data-schemas: convo/preset types + mongoose defaults
- api: thread promptCacheTtl into anthropic + openai(OpenRouter) llmConfig
- i18n: en translation keys for label/description/default placeholder
- tests: anthropic llm.spec coverage for set + unset cases
* 🔧 fix: Tie Bedrock promptCacheTtl to promptCache + thread OpenRouter TTL params (Codex review)
- bedrock.ts: clear promptCacheTtl whenever promptCache is off/unsupported,
so an unsupported 1h is never sent on a non-caching Bedrock request
- openai/llm.ts: resolve promptCacheTtl through the same defaultParams/
addParams/dropParams machinery as promptCache (via promptCacheTtlValue)
so OpenRouter custom endpoints can configure/override/drop it
- tests: bedrock TTL-tied-to-promptCache cases; OpenRouter TTL default/add/drop
* 🎨 style: Sort imports in openai/llm.spec.ts (CI sort-imports)
* ✅ test: Prove OpenRouter TTL-only selection honors promptCache default (Codex review)
OPENROUTER_DEFAULT_PARAMS injects promptCache:true into defaultParams, so a
TTL-only dropdown selection (promptCacheTtl set, promptCache switch untouched)
still resolves caching on and forwards the TTL. Add regression tests via the
real getOpenAIConfig entry point: TTL-only -> promptCache+TTL both set;
explicit promptCache:false -> both dropped.
* 🔖 chore: Bump librechat-data-provider to 0.8.506
* 🔧 fix: Drop Anthropic promptCacheTtl when promptCache is dropped (Codex review)
dropParams: ['promptCache'] deleted requestOptions.promptCache but left
promptCacheTtl behind, so the admin opt-out path could still carry a TTL
on a request with caching disabled. Clear the TTL alongside promptCache.