Repository navigation
Sync with upstream LibreChat main (326 commits since 2026-09-28) - #2
Conversation
…ce-code-body Role (LibreChat-AI#16334) (cherry picked from commit ecce519) Original-PR: LibreChat-AI#16334
* 🫗 fix: Bound MCP App Responses and In-Flight Work * 🫗 fix: Stop Oversized App SSE Reconnect Loops * 🫗 fix: Validate and Inject MCP App Operation Budgets * 🫗 fix: Keep Safe MCP App SSE Streams Open * 🫗 test: Admit Large Apps Under Configured Byte Caps * 🫗 fix: Bound SSE-Typed App Error Responses * 🫗 fix: Share App Admission With Initial Document Reads * 🫗 fix: Apply App Byte Limits During Tool Discovery * 🫗 fix: Honor Configured App SSE Line Budgets --------- Co-authored-by: Lia <lia@librechat.ai> (cherry picked from commit fe2b547) Original-PR: LibreChat-AI#16300
…#16333) (cherry picked from commit f1bc07b) Original-PR: LibreChat-AI#16333
…eChat-AI#16335) * refactor: normalize stream events behind a ChatTransport type Add the ChatTransport interface and a ChatEvent union discriminated by type, covering every frame the client handles today: the created, sync and final control frames, the step and side-channel { event, data } frames, legacy content and text frames, attachments, errors, open and abort. Types only; nothing consumes them yet. * fix: type the stream frames the server actually sends Assistants final frames carry a partial requestMessage, aborts can send title: null, Assistants content frames omit userMessageId, and the error event body can be a bare string or { error, generationProtocolVersion }. Widen the frame types to admit each. * fix: admit the remaining step, attachment and error payloads on_agent_update also carries a { runId, message } status line when sequential outputs are hidden, on_run_step_completed can arrive without a result, a published run artifact is a plain TFile, and sendError marks its body with error: true. The frame types now admit each. (cherry picked from commit 2f8a207) Original-PR: LibreChat-AI#16335
* refactor: drive useSSE through the ChatTransport Move the sse.js connection, frame parsing, the named attachment and error events, the 401 refresh-and-retry and the cancel-on-close signal into client/src/hooks/SSE/transport. normalizeFrame tags each message frame by the keys the hooks already checked, in the same order, and createSSETransport emits the resulting ChatEvent union. useSSE now switches on event.type and calls the same handlers with the same arguments; its signature is unchanged. A frame that is not JSON is now logged and skipped. Previously the parse error escaped the listener and dropped every frame behind it in the same chunk. createdHandler never read its first argument, so it is typed as the created frame it actually receives instead of TResData. * fix: keep the stream closed when a 401 refresh settles after abort A refresh that resolved after the caller aborted called sse.stream() again, reopening the request with the unmounted consumer's handlers; a refresh that failed after abort dispatched the error path. Both now return once the signal is aborted. * fix: refresh the token at most once per stream A retried connection that drew another 401 refreshed and reconnected again with no bound, so a server rejecting the refreshed token looped. The second 401 now reaches the error path. * refactor: consume the widened content frame type The content handler never read userMessageId, which Assistants content frames do not send, so it now takes ChatContentFrame. * refactor: bridge the wider step and attachment frames to their handlers The handlers accept narrower payload types than the wire frames now admit. useSSE narrows at the call, where these frames already reached the handlers unchanged before the transport existed. * fix: cancel a turn aborted while its 401 refresh is pending sse.js marks the connection closed on the 401, so an abort during the refresh skipped the cancel path and nothing reset the submitting state. The stream now counts as open until the refresh settles. (cherry picked from commit a3541da) Original-PR: LibreChat-AI#16336
(cherry picked from commit 84180a8) Original-PR: LibreChat-AI#16259
* feat: Radius and Font Scales Follow the Theme Map Tailwind's radius scale (sm through 3xl, plus bare `rounded`) and the sans and mono families to theme-owned appearance properties in the app's `@theme inline` block, so a theme definition reshapes the existing plain utilities instead of only the `theme-*` roles. Defaults reproduce the values those utilities resolved to before, and the JS config entries and the bare `--radius` they replace are removed. * fix: Keep the Pixel Offsets in the Small Radius Defaults The old `rounded-sm` and `rounded-md` were `calc(var(--radius) - 4px)` and `calc(var(--radius) - 2px)`. Rem-only defaults matched them only at a 16px root, so keep the calc form and match at every root font size. * test: Resolve Custom Properties Before Comparing the Code Font `code` and `.font-mono` now both reach the mono stack through custom properties, so compare the resolved stacks rather than the raw declarations. * fix: Leave Bare rounded on Its Fixed Radius Bare `rounded` was a fixed 0.25rem, which the `sm` default only equals at a 16px root now that it keeps its px offset, so it no longer follows the `sm` step. * fix: Accept the calc() Radius Defaults the Registry Exports * test: Cover the Theme Radius and Font Scales in the Browser * fix: Require Units on Both Operands of a calc() Radius (cherry picked from commit 50ff198) Original-PR: LibreChat-AI#16340
…t Lists (LibreChat-AI#16244) * style: Quieten the shared primitives and the dark borders The primitives carried the weight the screens then inherited: boxed empty states, outlined table shells, raised composer pills. Softening them here means a screen gets the quieter treatment by composing what it already composes rather than by overriding it. Dark moves its border roles one step off the page instead of two, so an edge separates a surface from the one behind it without reading as a seam. The values live in style.css for the pre-hydration paint and in dark.ts for the runtime theme, so both change together. Checkbox gains CheckboxGlyph, the presentational half of the control, for rows that are already a button and must not nest a second one inside it. * feat(client): Panel header and scroll fade for the side panels Every management panel was building its own title row, so the title, the search field and the create button sat at a different size and spacing in each one. PanelHeader is that row, and the panels adopt it next. PanelContent gains a fade at its foot, fed by useScrollFade, which watches the viewport and its content and reports whether anything sits below the fold. A list that is exactly as tall as its box shows no fade, and the gradient is dropped under prefers-reduced-motion. The sidebar keeps a right edge in every theme now that it shares a surface with the content beside it, rather than only in high contrast. * style: Apply the quieter layout to the panels and management pages The main content now sits on the same surface as the sidebar, so the app reads as one plane instead of a page floating on a backdrop. With the fill gone the containers had nothing left to justify them: cards, outlines and double borders come off the MCP, Skills, Bookmarks, Memory, Prompts and Scheduled Chats lists, and spacing and weight carry the hierarchy instead. The management screens adopt PanelHeader, so title, search and create sit at one size and one rhythm across all of them, and Schedules gains the search its siblings already had. Secondary row actions appear on hover and focus rather than resting in the row. Two rows also stop nesting interactive controls: the skills row splits into sibling buttons under a focus-within ring, and the memory and prompt toggles render CheckboxGlyph inside their button rather than a second control. * fix(client): Keep the schedules quota and skeleton honest under filtering The footer counted the filtered list while the create limit is enforced against all schedules, so filtering made a full account look under quota. The skeleton reserved a switch, a menu and three text rows the flattened row no longer has, so the panel visibly collapsed on load; it now mirrors the healthy row. The skill file expander announced "Show files" even while expanded, so its label is now a neutral toggle. * fix(client): Restore schedule row actions and settle the panels' design debt A schedule row's actions sat in the hover-revealing slot other lists use, but this panel's rows end at the sidebar's resize handle and a slot collapsed to zero width put the switch's clickable point outside the panel, under the handle, where no hover could ever reveal it; the slot now keeps its width and the menu trigger fades in on row hover alone. The design-rule violations this branch had suppressed are paid instead: the tables drop restatements the Table primitives own and take a compact header size, FilterInput gains the dialog surface instead of a caller-side color, the projects entries move to the section-action variant, the MCP cancel cross fades via a wrapper the caller owns, and the panel group's backdrop moves to its host. The counts this branch had raised come back down with the entries pruned. * fix(client): Preserve Shared Row Actions and Verify Panel States * fix(client): Match Schedule Skeleton Action Height * fix(client): Preserve Panel Overflow and Schedule History Actions * refactor(client): Derive Row Action Classes From the Reveal Variant The reveal recipe was spelled out both in the row-action-reveal Button variant and in rowActionClasses. The helper now starts from the variant and only adds the open and always-visible states, and the MCP card spec asserts classes the real recipe renders instead of a mocked string. * fix(client): Announce Panel Filter Results and Usage Changes The schedule filter now reports its match count through a polite live region, as the prompt filter does. The memory usage badge keeps a status region beside its toggle so a changed usage is announced while focus is elsewhere, and the search count reads as a floor while more pages remain. * fix(client): Keep Row Focus Visible and Warn on Balance-Skipped Runs The row action slot clipped its overflow even while focus was inside it, cutting off the focused action's ring. It now clips only while collapsed. Skill rows drew one focus ring around the whole row, so moving between the open and disclosure buttons changed nothing on screen. Each button now draws its own ring. A run skipped for balance keeps its schedule enabled only until the skip count disables it, so the row now shows it as a warning instead of reading as healthy. * test(e2e): Match the always-applied skill row's full button name * refactor(client): Render MCP Row Actions Through the Button Primitive The MCP row actions passed a computed class string to TooltipAnchor, which the design rules cannot check, and the file carried a new static-classes suppression for it. They now render the shared Button with the row-action variants, like the bookmark and memory rows, and the suppression is gone. * fix(client): Paint Field Notches With the Page Surface and Respect IME Escape The marketplace, skills and prompts pages moved to the primary-alt surface, but the floating labels on their fields still painted the presentation surface, leaving a patch across each field's border in both themes. The labels now paint the surface they sit on. The create-skill name label is a plain label like the edit form's, since the Label primitive owns its color and a notch has to paint its own backdrop. Escape in the sidebar search cleared the query even while an IME was composing, where Escape only cancels the composition. * fix(client): Keep the Chat Column Within the Viewport Beside the Sidebar Moving the surface off ResizablePanelGroup put a new flex item around it with the default auto minimum width, so a reply with a wide table grew the chat column past the viewport and cut its right edge off. The host now allows itself to shrink, as the group did. Also covers the sidebar search's IME Escape guard with a unit test, since the search field is not mounted without a search backend. (cherry picked from commit 7d4a3f0) Original-PR: LibreChat-AI#16244
* feat: Shadow Scale Follows the Theme Map Tailwind's shadow scale (xs through 2xl, plus bare `shadow`) to theme-owned appearance properties in the app's `@theme inline` block, with Tailwind's own values as defaults. `--theme-elevation-surface` stays the separate role behind `shadow-theme-surface`. * fix: Validate Theme Shadows and Keep Disabled Ones Composable A theme shadow must now parse as a box-shadow list or none, so a malformed value is rejected instead of voiding every shadow-* call site. none resolves to a transparent layer, since Tailwind lists --tw-shadow after the ring layers where a bare none invalidates the declaration. * feat: Theme the Smallest Shadow Step Too * test: Cover the Theme Shadow Scale in the Browser * fix: Let the Browser Reject Unknown Shadow Colors * fix: Keep var() and Other Length Units Valid in Theme Shadows * fix: Keep Defaults for Appearance Keys Present but Undefined * fix: Classify Shadow Tokens Instead of Deferring var() Values to the Browser * fix: Reject Empty Layers in a Theme Shadow List * fix: Require Concrete Theme Shadows So the Browser Can Validate Them * fix: Reject env() and attr() in Theme Shadows Like var() Both are substitution functions, so the browser only checks a value that holds one at computed-value time. A missing environment variable without a fallback then invalidated the composed box-shadow, ring layers included, instead of rejecting the theme and keeping the default. * fix: Keep Loading Released Themes With a Variable-Backed Surface Elevation The released validator accepted var() in elevationSurface, and a theme that fails validation is dropped as a whole. Only the new shadow scale steps need to be concrete, so elevationSurface keeps its original check. (cherry picked from commit 89f4a62) Original-PR: LibreChat-AI#16341
* feat: ClickHouse Theme Shape Parity The ClickHouse reference theme now sets the radius scale from Click UI's `border.radii`, the mono family to Inconsolata over a real fallback stack, and its shadow scale and surface elevation from Click UI's `shadow.1` (0.15 alpha in light, 0.6 in dark), so selecting it changes shape and not only colour. The divergence spec asserts the new tokens differ from LibreChat's, and the theme README documents the appearance scales. * test: Compare the ClickHouse Small Radius by Rendering, Not by String * test: Scenarios for the ClickHouse Theme Shape (cherry picked from commit 631fa64) Original-PR: LibreChat-AI#16342
(cherry picked from commit 09c12b9) Original-PR: LibreChat-AI#16361
(cherry picked from commit c4fbffb) Original-PR: LibreChat-AI#16361
(cherry picked from commit b9b82d7) Original-PR: LibreChat-AI#16361
…-AI#16373) A definition naming an appearance token this build does not know now resolves with that token dropped and reported, instead of being rejected whole. Unknown colours, invalid values for known tokens, and CSS or markup carried by an unknown token still reject the definition. (cherry picked from commit 0ea3e27) Original-PR: LibreChat-AI#16373
…I#16377) * fix(client): Give form-control outlines their own 3:1 border role border-medium and border-light were the only visible edge on the shared form controls, and neither clears the WCAG 1.4.11 3:1 non-text floor in any palette. Raising them would also darken every separator and card edge, which the quieter dark borders deliberately keep one step off the page. border-control is a new theme role for the outline of a field, select trigger, dropdown, combobox trigger and OTP slot. Every bundled palette gives it a value that clears 3:1 on each canvas a form can sit on, the package ships the stock light and dark defaults so the outline holds before and without a theme definition, and a custom theme that paints its own medium border but not the new role keeps the outline it drew. semanticTokens.spec.ts holds the 3:1 floor for the role in all six modes. * fix(client): Keep a legacy control outline only when it clears 3:1 A theme that paints its own borders but predates border-control took its medium border as the control outline, and a quiet one left every field without a visible edge. The fallback now keeps that border only where it clears 3:1 on the theme's own canvases and otherwise uses the theme's secondary text, in both resolveTheme and the legacy applyTheme adapter. The palette scenarios spell out their tags so the runner can find them. * fix(client): Map legacy light and medium borders onto the control outline Controls drew border-light (fields, dropdowns, comboboxes) or border-medium (select, OTP) before border-control existed, so a stored theme that painted either, or the canvases they sit on, takes the first of its light border, its medium border, the bundled role or its secondary text that clears 3:1 on its own canvases. A theme that names none of them keeps the bundled role. * fix(client): Never settle for a control outline that fails when a better one exists Secondary and primary text join the candidates for a legacy theme's control outline, and when none clears 3:1 on the theme's canvases the one that comes closest is used rather than the secondary text by default. (cherry picked from commit 85910e9) Original-PR: LibreChat-AI#16377
…reChat-AI#16378) * refactor: drive the resumable stream through the ChatTransport Add reconnectToStream to ChatTransport and the sse.js transport: a GET attach with request headers, a token refresh on every 401, a cancel the caller did not issue reported as a dropped connection (status 0), and a closed flag for the foreground check. useResumableSSE now consumes normalized ChatEvents instead of opening sse.js and parsing frames itself; resume, steer and queue bookkeeping are unchanged. * fix: bound the resume 401 refresh and keep plain-text server errors Refresh the token once per attachment and report a second 401, so the hook's reconnect budget bounds a stream that keeps answering 401. A server-written error event that is not JSON keeps its raw text instead of reading as a dropped connection. (cherry picked from commit df01dba) Original-PR: LibreChat-AI#16378
* feat: bundle Inconsolata for the ClickHouse theme The ClickHouse theme names Inconsolata as its mono family, but the app did not ship the face, so code fell back to the system monospace. Self-host the latin 400 and 700 woff2 faces from Fontsource 5.3.0 with their OFL licence, declared with font-display: swap in a stylesheet of theme-opt-in faces. The browser fetches a declared face only when text renders in it, so the default theme never downloads them. * fix: ship the Inconsolata licence with the production build Production builds disable publicDir, so the woff2 faces reached dist through the $fonts alias while the OFL text next to them in public/fonts did not. The build now copies the licence texts from public/fonts into assets/fonts, beside the faces they cover. (cherry picked from commit 57da77c) Original-PR: LibreChat-AI#16376
…ibreChat-AI#16379) * feat: close the data-level Click UI gaps in the ClickHouse theme Set the theme values Click UI defines that the ClickHouse theme still left at LibreChat defaults: - controlHeight 2rem (genericMenu.panel.size.height), spaceCompact 0.5rem (button.basic.space.gap), spaceNormal 1rem (button.basic.space.x) and motionFast 100ms (transition.default). - Light ring-primary to outline.default #437eef, matching the dark mode ring, which already used outline.default. - Light surface-overlay to dialog.color.opaqueBackground #151515. - Dark surface-inverted to Click UI's primary fill #faff69 with its hover and #1f1f1c label, which is also its checked checkbox and switch colour. * fix: keep the shared spacing roles at LibreChat's values in the ClickHouse theme spaceNormal and spaceCompact are not control-only: spaceNormal pads the user message and queued-steer bubbles, and spaceCompact is the composer send button's inner padding. Raising them to Click UI's button padding and gap widened the bubbles and shrank the send icon's box. Keep both at the defaults; 0.75rem is already Click UI's field padding. (cherry picked from commit 6d54d07) Original-PR: LibreChat-AI#16379
…6374) * feat: map message content parts to a UI parts union Adds a UIMessage-shaped view of TMessage modelled on the AI SDK (ai@7.0.114): content parts map one-to-one onto text, reasoning, tool-<name>, file and data-* parts, holes map to step-start, and the reverse restores the stored content so a view round-trips. * fix: carry text, files and metadata through the reverse message mapping An empty content array with text now views as contentless, an edited text part updates the stored text, attachment file parts reconcile message files, and metadata fields restore a message rebuilt without a stored base. * fix: read durable tool markers and apply view edits in the parts mapping Tool parts fall back from an empty id to the host step id, surface approval pauses and background-cancel or validation failures, and treat an empty legacy function output as submitted. The reverse mapping writes file and attachment edits over the stored records, keeps an explicit null parent, carries non-derived message text and absent text fields, persists hand-built tool failures, and collects text, content and attachments in one pass. Web search sources include link references and take message-wide ids. UIMessage drops the system role it could not round-trip. Adds mock-harness scenarios that view persisted replies, reasoning and tool turns and map them back. * fix: view persisted reasoning without its think tags Persisted reasoning is stored inside <think> tags that every renderer strips; the reasoning part now exposes the text alone and keeps the exact wrapper so the reverse mapping restores it. The tool-turn scenario skips mobile, where the composer MCP picker is not offered. * fix: give view attachments a stable identity and read legacy tool outcomes Attachment file parts carry the stored path they were viewed with, so an edited url still matches its file. Reverse slots advance only for content parts, audio takes its format from an edited media type, a null model survives a base-less rebuild, legacy retrieval outputs count as submitted, and error-prefixed outputs map to output-error through isToolErrorOutput, the predicate the tool output renderers use. * fix: keep attachments, empty content and hand-built tool results through a rebuild A base-less rebuild restores the stored attachments and an explicit empty content array from metadata. Hand-built tool parts record a completed status for output-available and keep an array output as a code interpreter call. Files the view never showed keep their stored positions while visible files fill the others in view order. * feat: let the caller supply tool outcome rules to the parts mapping Some tool outcomes are decided by rules the client owns, such as memory failure prose and background status attachments. toUIMessage, toUIParts and toUIPart take a resolveToolFailure option instead of copying those rules. Text deltas keep their ownership metadata, an empty hand-built array output is marked completed, and fromUIMessage documents that a stored message round-trips only with its base. * fix: map untyped lane placeholders to step-start A dual-conversation turn seeds type '' placeholder slots, which fell through the part switch and crashed toUIMessage. Any slot without a known type now maps to step-start, and the reverse leaves a hole, the same thing persistence compaction does. * fix: identify view attachments by position and keep content added to plain messages Attachment parts carry their index in message.files beside the path, so two files sharing a path stay distinct, and a hand-built part claims the first unclaimed match. Content parts added to a view of a plain message are written as content. Tool arguments that parse to a JSON primitive stay their string, inside the declared input type. * fix: derive fallback tool ids from the streamed index and drop stale annotations A tool call with neither a provider id nor a step id takes its fallback id from the streamed index the settled message keeps, so its identity survives compaction. Annotations on object-form text are dropped once the view's text no longer matches the value they index into. * fix: map tool call slots without a call to step-start and keep text delta object shape Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017snqTCuA2LHsoKuC335HFG * fix: report background and caller-resolved tool failures by their reason Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017snqTCuA2LHsoKuC335HFG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 6ac941e) Original-PR: LibreChat-AI#16374
…aring (LibreChat-AI#16245) * feat(api): Filter the conversation list by date, endpoint, files and sharing The list query takes five new facets. Dates and endpoints narrow it directly; hasFiles matches conversations that carry at least one file; sharedOnly resolves the user's live share links and matches their conversation ids, since a share expires and a denormalized flag on the conversation would outlive it. parseConversationListFilters owns what a valid facet is, so the route keeps the call and nothing else. A malformed date fails the request rather than being dropped: a filter that is quietly ignored answers with the conversations the user asked not to see. The endpoint list is bounded at 50 names of 128 characters to keep an unbounded $in out of the query. Two indexes back the new paths: user, archived, endpoint, updatedAt on conversations, and user, conversationId on shares. Cursor pagination is unchanged, so a filtered list pages the same way an unfiltered one does. * fix(api): Read the attachment facet from messages and tighten filter parsing The hasFiles facet matched only the conversation's own files array, which the standard send flow never writes: uploads ride on user messages, so an ordinary chat with an attachment was invisible to the filter. The facet now OR-matches message-derived conversation IDs alongside the conversation-level array. Dates are validated as ISO 8601 before construction, so 2026-02-30 and a bare year no longer roll over into an unintended cutoff, and a mistyped hasFiles or sharedOnly value fails with 400 instead of reading as absent. The endpoint count and name-length limits moved to conversationList in configSchema and are injected into the parser, so a deployment serving more endpoints can raise them. The shared filter resolves its IDs with distinct and reuses the set to mark isShared, dropping the second SharedLink query on every filtered page. * test(api): Exercise the real list-filter parser through the convos route The route mock re-implemented the parser loosely: it ignored the configured endpoint limits and dropped a mistyped flag, so a route that stopped forwarding conversationList or stopped answering 400 still passed. The mock now wraps the real parser, and the facet tests cover a mistyped flag and an endpoint list over the configured limit. * test(e2e): Acceptance scenarios for the conversation list facets Each facet is exercised against the running API with its own endpoint name, so the rows a test seeds are the only rows the filter can return: date cutoffs on updated and created time, an OR-matched endpoint list, an attachment that only exists on a message, shares that are live, expiring or lapsed, malformed facets answering 400, and a list without facets returning what it did before. * fix(api): Read the list filter limits from the base config conversationList never reached the resolved app config: AppService builds it from an explicit field list and the new knob was not on it, so a configured limit was silently ignored and the defaults always applied. It is now carried onto AppConfig. The list route also stops resolving the caller's merged config for it. The limits are deployment-level, and configMiddleware put principal and override reads in front of every cache-cold sidebar request, including ones with no facets; the base config is in memory. * fix(api): Read a list cutoff without an offset as UTC A timestamp without Z or an offset was handed to new Date(), which reads it in the server's local zone, so the same request selected different conversations on hosts in different zones. It is now UTC, matching how a date-only cutoff is read. * test(api): Share the base-config mock across the convos route specs * perf(api): Start the attachment and share lookups together With hasFiles and sharedOnly both on, the message-attachment lookup finished before the share lookup began, adding the two latencies on every page. Both are independent user-scoped reads, so they now run in parallel. * refactor(api): Resolve the list filter limits in one place The 50 and 128 defaults lived in both the parser and the schema's comments. They are now field defaults on conversationListConfigSchema, AppService resolves them (an invalid block keeps the defaults), and the parser takes the resolved values. The base-config read moves out of the CJS route into resolveConversationListFilters, which receives getAppConfig from the route. * fix(api): Keep the list config read inside the route's error handling * perf(api): Read the list limits only when a request names endpoints The base config sits behind the config cache, which can be Redis, so every sidebar request paid that round trip and could fail on it even though the limits bound only the endpoint facet. A request without endpoints now reads no config. * fix(api): Accept list cutoffs with microsecond or finer fractions The cutoff pattern allowed at most three fractional digits, so a valid ISO 8601 timestamp from a serializer that emits microseconds (Python, most databases) answered 400. Any fraction is accepted now and Date keeps the milliseconds. * fix(data-schemas): Count files attached to message content parts in the file facet A file attached through a mid-run steer is stored on the steer content part rather than on the message, so hasFiles=true left that conversation out. * fix(data-schemas): Match every content-part file shape in the file facet Replay reads a file on a content part as files[], file.file_id, image_file.file_id or a bare file_id; the facet matched only files[]. * fix(data-schemas): Count tool and assistant attachments in the file facet A file a tool produces is persisted on message.attachments, the one place on a message the facet did not read, so a chat whose only file was generated output was left out of hasFiles=true. * fix(data-schemas): Count download-only attachments in the file facet A code or tool result can persist an attachment with a filepath and no file_id; the client renders any attachment with a filepath as a file, so the facet now uses the same rule. (cherry picked from commit d1cf27e) Original-PR: LibreChat-AI#16245
…I#16400) * fix: Quiet Form-Control Outlines in the Stock Palettes The stock light and dark palettes gave border-control a 3:1 value, which made every field, textarea and dropdown outline far louder than the cards and separators around them. Both now use their border-light value, while the high contrast and ClickHouse palettes keep their 3:1 outlines. A legacy theme without border-control now keeps its own light border on controls instead of being pushed to a contrast-ranked candidate, which often landed on a text color. The contrast helper that ranking needed is removed. The empty tools and skills cards step up from border-light to border-medium so their dashed edge reads against the panel. * fix: Keep a Legacy Medium Border on Controls A theme that predates border-control and painted only its medium border drew that edge on selects and OTP slots, so it now falls back to the medium border after the light one instead of the bundled role. Adds scenarios for the stock outlines matching the card border, the legacy medium-border theme, and the empty tools card edge. * test: Spell Out the Stock Control Outline Scenario Tags (cherry picked from commit df59d61) Original-PR: LibreChat-AI#16400
) * feat: expose the messages key and the submitted response on the chat contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: keep the submitted response in the chat host instead of reading the submission atom The contract's initialResponse is now the response ask submitted, held beside the host's own state and exposed only while the turn is in flight, so a settled turn and a run restored after a reload report none, and the submission atom gains no reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: take the submission ask builds as a value and document the empty messages key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: scope the submitted response to its chat and promise seeded positions, not identity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * docs: note that a new chat's submitted response ends at its saved id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: end the submitted response with its turn and note that the stream can continue the last seeded part Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * refactor: drop the submitted response from the chat contract A run can be restored, replaced or torn down outside ask, so a copy of the submitted response kept in the host describes the wrong run on those paths. The contract keeps messagesKey only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * test: cover the messages key across chat switches in the mock e2e harness Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 77a1cfa) Original-PR: LibreChat-AI#16395
(cherry picked from commit f510bd7) Original-PR: LibreChat-AI#16361
…t-AI#16375) * feat: expose the messages key and the submitted response on the chat contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: keep the submitted response in the chat host instead of reading the submission atom The contract's initialResponse is now the response ask submitted, held beside the host's own state and exposed only while the turn is in flight, so a settled turn and a run restored after a reload report none, and the submission atom gains no reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: take the submission ask builds as a value and document the empty messages key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: scope the submitted response to its chat and promise seeded positions, not identity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * docs: note that a new chat's submitted response ends at its saved id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: end the submitted response with its turn and note that the stream can continue the last seeded part Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * refactor: drop the submitted response from the chat contract A run can be restored, replaced or torn down outside ask, so a copy of the submitted response kept in the host describes the wrong run on those paths. The contract keeps messagesKey only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * feat: add an AI SDK-shaped useChat facade over the chat contract useChat reads ChatContext and presents it as @ai-sdk/react@4.0.117 does: UI-shaped messages, a submitted/streaming/ready/error status derived from the in-flight flag and the latest message, and sendMessage, regenerate, stop and setMessages forwarding to the contract. It holds no state and changes no existing consumer. * feat: apply the client tool outcome rules in the useChat facade The parts mapping reads only the markers stored on a tool call; the facade supplies getToolMeta as its resolveToolFailure, so memory failure prose and background task status attachments reach the UI parts view the same way they reach the tool cards. * fix: make the useChat facade reactive, placeholder-aware and cached per message messages re-reads getMessages whenever the message query cache is written, through useSyncExternalStore, so setMessages and other cache writes show without an unrelated context update. Views are cached per message reference, so a streamed chunk remaps only the messages it replaced. status ignores placeholder parts through isEmptyContentPart, the error reads an Assistants error part's text, and a message inserted through setMessages joins the active conversation under the message before it. * fix: remap streamed responses and scope stepless tool calls in the facade The content handler reuses a response object across frames and replaces its content, so views are cached per message and per snapshot of the fields a view reads rather than by the object alone. The tool outcome resolver passes getToolMeta the step ids a repeated provider id already owns, so a call without a step is not failed by another step's attachment. * fix: follow in-place stream frames and tolerate empty tool call slots in the facade The content handler replaces a response's content on the same object, so structural sharing keeps the cached message array and the external store saw no change. The snapshot is now keyed by the message cache write count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: catch unobserved cache writes, keep inserts in the active chat and report the error part Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: key the facade snapshot on the active query's own update count Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: use the filters form of findAll for the facade's messages scan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * perf: look up the facade's holder queries once per cached array Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: read the chat id, first delta and active branch from the chat contract The facade subscribes to the one message query the contract names instead of matching cached arrays, reports that query's conversation as the chat id, counts only parts the submitted response was not seeded with as streamed output, and parents an appended message to the active branch. A memory call's failure prose is its error text now that the resolver's reason becomes the part's errorText. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: keep inserted messages in the active chat, unassigned in a new one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: keep stored messages in their conversation and count generated files as streamed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: read seeded parts by position and report failed user tails as errors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * fix: count a continued last seeded part as streamed and remap views on metadata changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw * refactor: read streaming status from content alone now the contract has no submitted response Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 0967e16) Original-PR: LibreChat-AI#16375
…t-AI#16246) * feat(client): Rebuild the chat list filter as a properties menu The old menu listed every option at once. It now opens on three rows that state where the list stands, Show, Sort and Filter, each with its current value and a submenu holding the choices. Reading the list's state no longer means reading the whole menu. Filter holds the five facets and a search field that looks inside them, so typing openai finds the endpoint rather than the category that contains it, and typing a category name offers everything under it. Reset sits in the header next to the title, and the active facet count rides on the trigger. The facets are one derived atom, so the query key and the request are built from the same description of the filter. Date cutoffs snap to local midnight: taken from the current instant they would differ on every render and refetch the list each time. The panels also fade their content at the foot when a list runs past the fold, and the chat list's loading label shimmers the way every other in-flight label in the app does. * feat(client): Show a project's chats under that project only A chat assigned to a project was listed twice in one sidebar: once under the project and once in Chats. The chats list now asks for the chats that belong to no project, which the list query and its cache helpers already knew how to answer. Search and the archived view keep asking for everything. Projects is not rendered while a search is on, and an archived chat has no project row to appear under, so excluding them from either would leave a chat with no way back to it. * fix(client): Keep facet-filtered caches honest and the facets session-scoped Cache writers judged list membership only by project, archive, tags and sort, so an SSE update or a fork could insert a chat the server's filter had excluded. The endpoint and date facets are decided from the row itself, and the attachment and sharing facets mark their variants for a refetch, since only the server can place a row in them. The date cutoffs now derive from a day anchor a mounted consumer advances at local midnight, so a tab left open stops serving yesterday's today. Facets reset at the authentication boundary with the rest of the session state, the bookmark query stops firing for roles without BOOKMARKS:USE, an empty Chats section under existing projects says so instead of claiming the account is empty, and the design-rule violations this branch had suppressed in Conversations and ChatFilterMenu are fixed and their entries pruned. * test(client): Mock the projects query the chats section now reads * test(e2e): Acceptance scenarios for the chat list properties menu * fix(client): Call the account empty only once its projects have loaded An empty Chats list read as "No chats yet" whenever the projects query had not succeeded, so a slow or failed projects request claimed an account was empty when its chats may all live under projects. The account-level wording now needs a loaded, empty project list; otherwise the section says only that nothing sits outside a project. * test(e2e): Scenario for the Filter submenu under a role without bookmarks * fix(client): Drop a cached chat from an endpoint filter it no longer matches The endpoint facet only guarded insertion, so a chat already in an endpoint-filtered list stayed there after switching provider until the next refetch. List membership now includes the endpoint for existing rows as well; a row with no endpoint stays until the server decides. * fix(client): List every chat when the projects fail to load Scoping Chats to unassigned chats hid every project chat behind the Projects section, which shows nothing when its request fails. A failed project list without cached data now leaves Chats unscoped, so those chats stay reachable, and an empty unscoped list reads as an empty account. * test(e2e): Scenario for project chats when the projects fail to load * fix(client): Count the bookmark filter once in the menu trigger The trigger added one per selected bookmark while the Filter row counts the whole bookmark group as one, so a screen reader heard two different totals for the same state. Bookmarks now count once, like the endpoint facet. * fix(client): Stop the endpoint filter at the configured limit The Endpoint facet let a user pick more endpoints than conversationList.maxEndpointFilters allows, and the list request past it answered 400, leaving Chats in its error state. The post-login startup config now publishes the limit, resolved by the same helper the list parser uses, and the facet disables further endpoints once the selection reaches it, with a note saying why. * test(e2e): Scenario for the endpoint filter limit * fix(client): Hold the endpoint filter until its limit is known The sidebar renders before the post-login startup config resolves, and until then the Endpoint facet had no limit to enforce, so a selection made in that window could pass conversationList.maxEndpointFilters. Endpoints are now offered disabled until that config has loaded. * fix(client): Keep a chosen bookmark removable and a failed config unlocked A selected bookmark whose last chat dropped it fell out of both bookmark lists, so only Reset could clear it; a selected tag now stays listed. The Endpoint facet waits only while the startup config is loading, so a config that failed no longer locks every endpoint without saying why. * test(e2e): Scenario for a chosen bookmark whose count drops to zero * fix(client): Keep a chosen endpoint removable when the deployment stops listing it An endpoint that left the endpoints config, or a config that failed to load, dropped out of both endpoint lists while it stayed selected, so it kept narrowing the chats and could not be turned off. Both lists now come from one helper that keeps the selection listed. * docs(client): Punctuate the list membership comments without dashes * fix: Publish the chat list filter limits the list route enforces The startup config published maxEndpointFilters from the caller's merged config while the list route reads the deployment's base config, so a principal override could let the sidebar allow a selection the route refused, or disable one it accepted; and the endpoint name limit was not published at all, so a long custom endpoint name could be selected and turn the list into a 400. Both limits now come from one loader the list route also uses and are published together as conversationListLimits; the Endpoint facet leaves out names past the length limit. * fix(client): Keep a deleted bookmark removable from the filter A selected bookmark that was deleted no longer came back from the tags query, so neither bookmark list could show it and only Reset cleared it. Both lists now come from one helper that keeps every selected bookmark. * fix(client): Offer a retry when a project's chats fail to load Chats lists only chats outside any project, so a project whose own chat request failed hid its chats behind the empty state. The project now says its chats could not load and offers a retry. * test(e2e): Scenario for a project whose chats fail to load (cherry picked from commit 1160c80) Original-PR: LibreChat-AI#16246
…4473) * 💬 refactor: ChatForm UI redesign with unified palette Replace the scattered composer controls (badge row, tools dropdown, attach menu, floating steer chips) with one system: a searchable palette with favourites, a composer bar holding active tools plus effort and speech controls, a tray staging files and manual context, and a queue rail that buffers messages while a reply is running. Collapses the 94 development and review-fix commits of PR LibreChat-AI#14473 into one commit so the branch could be rebased onto dev once; the upstream merge of this pull request is a squash merge either way. The pre-rebase history is kept locally at tag prerebase-backup (afde3e4). * fix: finish the dev rebase of the composer redesign The rebase onto dev left four defects the merge itself produced, plus one review finding the branch had never answered: - ChatForm's `min-w-0` JSX comment lost its closing brace, so the file did not parse at all. - `useSteering` carried both dev's local draft consumer and the host-injected one under the same name; the host's `consumeDraft` owns the storage key autosave actually parked on, so the local copy is gone. - `useUploadOptions` kept dev's `awaitingAgentProvider` gate after the merge replaced the block that derived `isSavedAgent`; the predicate is back, from the same `isEphemeralAgentId` the shared allowance helper uses. - `MessagesView` still reserved a band for the in-flight steer overlay and read `steerOverlayHeightFamily`, which nothing writes now that pending steers render inside the streaming reply. The reservation is gone and `ScrollButton`'s lift is optional; composer panels keep standing the button down through `composerOverlayCountFamily`. - The aggregate MCP chip keyed on `mcp:pinned`, which a server literally named `pinned` also produces (`mcp:${serverName}`), so both entries shared one React key and one packing slot. It uses a key outside that namespace. Also drops the locale keys whose only consumers the redesign deleted, ports dev's extracted-text delivery path into `useAttachExisting` (every attach-existing surface routes through it now), repoints the mock e2e built-in helpers at the palette, and repairs test fixtures the merge left stale (a query mock that blanked agent metadata, a Jotai store mismatch, an ambiguous tray query, an ephemeral-agent fixture standing in for a saved one). * test: cover the composer redesign's acceptance scenarios end to end Twelve mock-harness scenarios, one `@scenario:` tagged test each, over the behaviours this change is accountable for: the single searchable palette (open, search, favourite across a reload, keyboard-only operation), the composer bar and tray (active tool chip, a staged file riding with the message, a per-turn thinking level that applies to the next turn only), the queue rail (Enter buffers during a run and drains after it, keyboard reorder changes the send order, the draft survives run completion), and the composer's portaled surfaces (Stop reaches the run from inside a portal, a pending tool approval in the bar resumes the run). Two contract scenarios are deliberately absent, with reasons: split-pane Stop isolation is not runtime-reachable because `ChatRoute` mounts one pane, and code-approval mode mid-run needs an attached stateful code environment the mock lab does not configure, so dev's `ChatForm.approvalMode` unit spec stays its coverage. * fix: make the composer's acceptance run and the whole client suite green The first verification run of the twelve scenarios passed six and failed six, and the full client jest suite (which CI runs sharded, unlike the diff-scoped local precheck) failed fourteen tests. One of each group was a real defect; the rest were queries and fixtures still aimed at surfaces the redesign replaced. Defects: - `useQueueDrain` held its send lock after a revealed queued turn settled, so the next server-owned row could never be selected. The reveal path now owns its own lock and releases it when the revealed turn clears. - `ChatForm` recomputed the during-run slot without `consumeComposer` in its dependency list, and still carried a landing flag the host prop replaced. Tests and lab: - The five `ChatForm.attachments` focus cases now drive the palette dialog and the tray's quote chips instead of the deleted attach menu and quote popup, with the same strict focus assertions; the harness gains the `main` landmark the palette portals into, and the touch fixture a complete `matchMedia`. - `Palette.spec` no longer closes a hoisted `jest.mock` factory over a later `const`; `useSteering.spec` follows dev's toast wording. - The six failing scenarios were repointed at what this lab actually renders: uploads go through the palette's local-computer row, the reasoning control needs `interface.parameters`, the queued-row contract is the row and its drain rather than a control the submit consumes, the palette's keyboard path uses a built-in the lab grants, the slow-reply wait uses the fixture's chunk marker, and the approval scenario selects its agent through the builder flow the passing approval specs use. * fix: close the eight findings Codex raised on the rebased head Codex reviewed the rebased heads and reported eight issues; all eight were real. Two of them concern code this session added. Behaviour: - A custom or OpenRouter endpoint that declares neither `paramDefinitions` nor a `reasoningFormat` no longer gets a reasoning control inferred from the generic settings list. Capability now has to be declared (a reasoning-key parameter definition, a non-disabled `reasoningFormat`, or a known reasoning provider as the default params endpoint); a `disabled` format always resolves nothing, and every consumer — the composer control, the pre-send validation and the server — asks the same resolver, so a gateway backed by a non-reasoning model can no longer offer a level the turn would fail on. - Restoring a queued attachment keeps its `llmDeliveryPath`, so an extracted document is routed as its text rather than as raw provider content. - The placeholder-to-durable reasoning migration now requires this pane's own live submission, so opening another conversation from a new chat no longer carries the pending level into it. The created/sync case it was written for still carries. - The queue rail treats an acknowledged server-owned row as an ordering boundary: local rows still reorder among themselves, a server-owned row cannot move, and a move that would cross one is refused out loud — the queued-turn API has enqueue, list and cancel only, so a client-side reorder across that boundary could not be made authoritative. - The palette resolves an agent's skill scope with `resolveAgentSkillsScope` instead of treating an empty list as an open catalog, and the attach target resolves `useResponsesApi` with `resolveUseResponsesApi`, matching what the run will actually do. Policy: - The reasoning-override behaviour moved out of the CJS middleware into `packages/api/src/modelSpecs/reasoningOverride.ts`, which returns a discriminated result; the middleware is wiring again, and the behavioural cases moved with it. - `showComposerTips` and `showFilesDialog` are Jotai atoms now, the tips preference keeping its storage key and reaching the composer as a prop. * fix: make the redesigned composer reachable from the mock e2e helpers CI's Playwright shard failed four `file-provisioning` tests on this branch because the shared upload helpers still clicked `#attach-file-menu-button` and `#attach-file-button`, entry points the redesign removed — attaching now goes through the palette, which also owns the file input. Both helpers drive the palette now, `uploadViaLegacyOption` mapping its two destination names onto the palette rows that route to the code environment and the vector store, and the specs that drove the old surfaces directly (`code-upload`, `thread-renderers`, `unified-upload`) follow. Also closes the last client/server disagreement the acceptance run exposed: a custom endpoint that declares a reasoning key in `paramDefinitions` had that declaration merged into the provider defaults and then discarded by model-name inference, so the composer offered a level for `mock-model-a` that the server rejected with "Invalid reasoning override". An explicitly declared key now selects the merged setting before inference, while an undeclared generic custom or OpenRouter target still resolves nothing. The twelfth acceptance scenario is rescoped: a paused approval keeps the composer usable and resumes to completion, but the composer-hosted approval panel and chip are not asserted, because the pending action is keyed by the stream's conversation id while the composer stays bound to the new-chat sentinel — in code byte-identical to dev, and mirroring the write onto the sentinel did not surface the panel. Tracked at berry-13#39. * test: finish the acceptance run and keep the client typecheck green - The thinking scenario read `reasoningOverride` from a `userMessage` wrapper the chat request does not have; the trace shows the request carrying `{ key: 'effort', value: 'max' }` at the root, so the assertion reads it there. Source behaviour was already correct. - The approval scenario timed out on mobile because the Agent Builder drawer stayed open after selecting the agent and swallowed the pointer events meant for the composer; it now closes the sidebar and waits for the drawer to go inert before sending. - Two specs typed a named custom endpoint's label as an `EModelEndpoint`, which CI's client typecheck rejects (the label is exactly what the case is about). All twelve acceptance scenarios now pass in desktop light, desktop dark and mobile emulation. * fix: close the nine findings Codex raised on the verified head Two were defects in code this session added while closing earlier findings: - A reasoning key declared in `paramDefinitions` but absent from the generic settings list was accepted as capability and then never inserted, so the resolver exposed and validated the generic `reasoning_effort` instead. A declared key is now appended to the effective settings and resolved before any fallback, for `effort`, `thinkingLevel` and `thinkingBudget` alike. - The queue's reorder guard validated from the destination row's own index, so `target === index` rejected every pointer drag and only keyboard reordering worked. It validates the dragged row's identity now; the server-owned boundary, its announcement and the disabled grips are unchanged. The rest: - Stop no longer falls through to the document-wide control once a focused pane has been resolved: focus in an idle pane while another generates used to abort the other pane's run. - `PendingSteers` is mounted once in `MessagesView`, above the renderer switch, so a sent or failed steer keeps its in-flight, retry and send-as-new surface with `FLAT_THREAD` off, where the recursive renderer had none and the composer no longer carries a fallback. - The file-list policy moved out of the CJS route into `packages/api/src/files/list.ts`, and its cap is `interface`-level config (`fileListLimit`, default 100) instead of a literal in the route. - The steer-arm confirmation window is `interface.steerArmConfirmationTimeoutMs` with its current 10s as the default, travelling the same path as the other client-consumed timings. - The pause predicates judge the active branch's tail rather than the last assistant in array order, so an inactive sibling branch can no longer enable or disable the inline escalation control. * fix: move persisted reasoning validation into api package * fix: restore composer entry points and defaults * style: normalize reasoning helper formatting * test: open mobile sidebar before account menu * test: use settled sidebar layout for mobile account menu * test: handle empty mobile sidebar in account scenario * test: target account menu in mobile drawer * fix: keep composer surfaces and mobile drawer reachable * fix: let thread approvals receive pointer events * fix: stabilize composer steering and shortcuts * fix: commit queue drags and centralize file modal * fix: return a canceled steer's text to the composer Canceling a pending steer dropped the words the user had written: the bubble disappeared and nothing carried the text back, so a reclaim that the server confirmed still lost the message. Hand the reclaimed text to `pendingComposerTextByConvoId`, which the composer already drains on its first render, and append rather than overwrite when the composer holds a draft the user typed while the cancel request was in flight. * fix: close the six findings Codex raised on the pre-rebase head Pending steers keyed off the Recoil conversation id, which lags the route during warm-cache navigation, so the surface could sit at the destination thread's tail while its Cancel and Escalate acted on the source run. It now keys off the rendered tree, as the mount window already does. Arming a steer removes the control that was activated, dropping keyboard focus on the document body. `useSteerEscalate` already exposes `onArmed` for exactly that; the bubble now passes one that moves focus to the row's Cancel action. Opening the file manager from the account menu left its dialog with no opener: the modal rejects an active element inside a menu, and nothing supplied a fallback, so closing it returned focus to the body. The opener is published alongside the open flag and cleared on close, which keeps the shortcut path's composer-focus behaviour intact. A steer the server rejected was swept into the queue with no marking, and the run-end drain then sent those words as a fresh turn without the user touching Retry or Send as new. Converted failures now carry `needsExplicitSend`, which the drain skips the same way it skips a row the rail is mid-edit on. The queued-turn reconciliation window was a hard-coded 60s, unreachable by an operator behind a slow proxy or a delayed read replica. It ships as `interface.queuedTurnReconciliationTimeoutMs` with that value as default, beside the steer arm timeout it belongs with. Reasoning-override shape validation moved to `parseReasoningOverrideRequest` in packages/api, leaving the middleware to map a rejection onto its own error response. * fix: keep the composer's reasoning level in step with the parameters panel `stableConversation` memoized the conversation without any reasoning field in its key, so editing the level in the Parameters panel produced a new conversation object that every listed dependency ignored. The composer went on showing the old level while the next ordinary send already used the new one. * test: stop the sidebar helper waiting for a drawer desktop never renders `ensureSidebarOnScreen` measured `#mobile-drawer` whenever the sidebar had no Pinned region. Desktop renders no drawer, and `boundingBox()` on a locator that matches nothing waits for it, so every scenario that reached that branch at a desktop viewport timed out instead of continuing. Check for the drawer's absence rather than measuring it. * test: open the mobile drawer from its inert state, not its geometry A closed drawer stays mounted and keeps its layout box, so measuring the Pinned region reported it on screen while every control inside was still untappable. `inert` is the state the component actually sets when it closes, so the helper reads that and clicks the header opener before anything else measures. * fix: return a canceled steer whole, through one recovery boundary A steer that stops being a steer was handed back three different ways, and each one lost something. The immediate cancel wrote `steer.text` into the composer handoff atom, dropping the attachments, quoted excerpts, manual skill picks and reasoning override that `settleReclaimed` then deleted with the pending object. The cancel that won its race against its own POST cleared the pending marker and restored nothing at all, so those words disappeared. A definitively rejected steer could only be resent unchanged, because the row offering to edit it was gone. Give the recovery one boundary. `useSteerRehome` hands the whole steer to the composer through the guarded restore the queue rail already uses, and converts it into an ordinary queued follow-up when the composer refuses: a draft in the box, another chat on screen, a paused question, or a composer that unmounted while the reclaim was in flight. The chip goes either way, since a pending row claiming the server still holds the words is a lie once they live somewhere else. The composer is a sibling of the message list rather than its ancestor, so the thread had no way to reach that restore. The pane's host now holds the reference and both sides read it there, which also means a reclaim resolving after the composer is gone refuses instead of writing into a dead form. Because the restore refuses rather than overwrites, a draft typed while a cancel is in flight is no longer glued to the returned steer; the steer becomes its own queued message and the draft is left as typed. * test: drive the surfaces these scenarios actually reach Four scenarios were failing against surfaces the user never touches. The approval scenario clicked Approve on the thread's copy of the pending card. The composer's review panel floats over the tail of the thread and owns the batch submit, so that copy sits behind it and the click retried until the test timed out. Drive the panel instead; the thread card stays asserted as the record of the request. The palette scenario counted rows the moment the dialog painted, then expected clearing the filter to restore that number. The catalog arrives in waves: the MCP server list is released by the app-wide warmup timer and the skills catalog only starts fetching once the dialog's open effect has run, so the count kept climbing under the test. Wait for the last wave and for the count to hold still, then snapshot. The sidebar helper opened the mobile drawer twice through two signals that disagree. It waited for `inert` to clear, which happens on a state commit several frames ahead of the slide, then fell through to a bounding box still reading off-screen and clicked the opener again, into a machine already opening. One open, through `inert`, then wait for the drawer to settle in the viewport. Splitting the cancel scenario follows the recovery change: an empty composer takes the steer back, and a staged draft keeps the box while the steer becomes a queued message of its own. * fix: hold a server-rejected steer for an explicit send Editing a definitively failed steer falls back to the queue when the composer will not take it. Queued like any other follow-up, the run-end drain would send it the moment the reply finished, which turns the server's refusal into an automatic new turn carrying the payload it refused. Mark that fallback the way the rest of the rejected-steer path is marked, so the row waits for the user to send it. * test: restore two assertions this branch broke while rewriting specs Both specs were updated for the redesigned surfaces and both lost a step they needed, and both fail deterministically in CI as a result. thread-fold pressed Enter mid-run and waited for a queued row, under a comment claiming Enter queues by default. It does not: the default during-run action is steer, so Enter produced a pending steer and the queued row the next line waits for could never appear. Ctrl/Cmd+Enter is what takes the other action, which is the one this test needs. mcp-oauth-readiness asserted on the server card's Cancel button straight after clicking Connect. Connect opens the OAuth dialog over the panel, and it is modal, so while it is open the card underneath is marked aria-hidden and Cancel is not in the accessibility tree at all, which is why the failure reads "element(s) not found" rather than "not visible". The rewrite dropped the dismissal the previous flow performed; restore it as a helper both call sites share. * test: reach the account menu without the pinned section The two account-menu tests borrowed `ensureSidebarOnScreen`, which also settles the Pinned section's placement for the pinned specs. They never touch that region, and on a run where an earlier test left a pinned conversation the section exists but has not been laid out yet, so both tests spent fifteen seconds polling it and failed on the wait rather than on anything they assert. Give them a helper that does only what they need: open the mobile drawer from `inert`, the one signal that distinguishes a closed drawer from an open one while it stays mounted, then wait for the slide to reach the viewport. Desktop renders no drawer and needs nothing. * test: open the drawer by what it shows, not one early read The helper decided once, from a single read of `inert` taken right after navigation. On a fresh mobile load the drawer can still be mounting, so that read reports a state the app has not settled into, the helper returns believing the drawer is open, and the account menu inside it is never shown. Retry against the thing the caller actually needs, the account menu being visible, re-checking before each attempt. While the drawer is open its control is labelled "Close sidebar", so the retry cannot toggle it shut. * fix: keep the composer observing MCP authorization where its tools apply The old badge row mounted its provider with observeToolAuthorization set whenever the composer showed ephemeral tools, so on a plain endpoint the chat kept MCP connection status and discovered tools in the query cache. The redesigned composer mounts the same provider without the prop, so it defaults to off and nothing on the chat side loads that state any more. The Agent Builder's tool library reads that cache. With it empty, every server reads as Unknown with no tools, and clicking one opens its detail dialog offering to connect instead of adding it to the agent. Pass the same gate the badge row used: showTools is the old showEphemeralBadges expression. * test: let the palette close before reopening it, and name what is asserted enableBuiltinTool pressed Escape and returned while the palette was still animating out. A caller that reopened it straight away resumed the same popover, scroll offset included, so the Attach rows at the top sat outside the virtualized window and upload helpers timed out looking for them. Wait for the dialog to be gone before returning. The staged-file assertions matched the bare file name, which the palette now also lists under recent uploads while it closes; scope them to the tray they are about. The OAuth dialog dismissal waited for a title built from the server's display name, but the dialog is titled with the server name. * feat: make the queued-send lock expiry configurable The per-pane lock that stops the run-end drain and an explicit Send now from submitting the same queued message twice gives up a broken claim after a fixed 60 seconds. Until then every drain and every Send now for that pane is refused, and a deployment had no way to tune that window. Expose it as interface.queuedSendLockTimeoutMs beside the queued-turn reconciliation timeout, defaulting to the same 60 seconds so nothing changes unless an operator sets it. The interface loader copies each field explicitly, so the value is added there too; without that line a configured value would never reach the client. * fix: decline interrupt-and-steer while reasoning is staged With a reasoning choice staged, the Interrupt & steer row is disabled, but its keyboard chord still reached interruptSteer, which fell back to interruptAndSend: it aborted the run and threw away the partial answer the action promises to keep. A staged reasoning choice belongs to a new generation that a steer into the live one cannot carry, so decline the chord exactly as the row does, the same way the function already declines while paused on an approval. The words stay in the composer, where Enter queues them as a turn of their own. * fix: wake the queue drain when a late steer retry is queued Retry marks a failed steer sending, and a sending steer is kept out of the run-end sweep so its own POST settles it. When the run finishes while that POST is in flight, the drain spends its one-shot signal on a queue the steer is not in yet; the late ack or NO_ACTIVE_RUN then moves it into the queue, where it sat until the user sent it by hand or another run ended. Every other late conversion re-posts the last completed run end through the composer's rewakeDrain. The retry lives in the thread, so the composer now publishes that function through the same host it already uses to hand a reclaimed steer back to the draft, and the retry calls it after converting. It stays a no-op unless the run completed cleanly and no newer signal is armed, or when no composer is mounted. * fix: honour thinking-budget ranges wider than 200,000 The request-scoped reasoning override capped thinkingBudget at 200,000 in the shared schema. An operator's paramDefinitions may advertise a wider range, and the composer renders it, but every value above the cap was dropped by the control's parse and rejected by the server's before the range-aware check could run. That check already exists: resolveReasoningOverride validates the value against the endpoint's resolved range. Drop the fixed ceiling so the resolved range is the bound, keeping the integer and lower-bound rules. The new case covers both directions on a custom endpoint whose range goes to 500,000. * refactor: persist the preliminary reasoning override through the shared helper The agents controller validated the request's reasoning override with the schema itself before writing the preliminary user message. That is behavior in the CJS layer, and it duplicated resolvePersistedReasoningOverride, which @librechat/api already exports and BaseClient already uses for the same field. Call the helper instead. A preliminary message is always a fresh turn, so it is never an edit or a compaction. The two controller specs that replace @librechat/api with a hand-built mock now take the real helper. * fix: clear the file manager's state at the session boundary The file manager's open flag and its opener live in Jotai's default store, which outlives the authenticated route. A file manager left open at logout reopened for the next account, still holding a ref to the previous session's unmounted opener. Reset both in useClearStates, next to the other composer state this branch already clears there. * fix: decline interrupt-and-steer on the first turn while reasoning is staged * test: read the phone layout from the viewport and keep the drawer open * fix: name both reasons a queued message cannot be edited yet * test: reuse a published file through the composer palette * style: move the composer onto the design-system rules * style: move the file row's new layout styles into classes * fix: keep the composer's reused thumbnails, focus ring and disabled dimming intact * fix: bring a steer back when its deferred cancel does not remove it * fix: treat a run as over only when every pane holding it is idle * refactor: pass enter-to-send into the composer from its host * feat: make the palette's recent-file count configurable * fix: let a file already in the composer be reattached at the limit * test: hide the chip before replaying a deferred cancel * refactor: restore the resume request through one packages/api facade * fix: keep a conversation's staged reasoning across a visit to another model * fix: keep a connected MCP server's variables configurable from the palette * fix: resolve every pane-scoped shortcut through the pane holding focus * refactor: keep every reasoning-override decision out of the /api wiring * perf: skip paging the skill catalog for an agent that can show no skill * test: type the reasoning snapshot the override helper records * test: pin the applied budget and the untouched parameters in the override specs * style: give the stop button the send button's shared submit recipe * fix: name only the newline key when no chord sends * fix: name the stop key only once the stop control can act * fix: clear every staged reasoning selection at sign-out * fix: stop advertising send-now while staged reasoning forces a queue * fix: resolve the reasoning target of an ephemeral agent in the composer and the request * test: type the staged reasoning the sign-out spec seeds * fix: check the composer will take a queued row before discarding its parked copy * style: carry the composer's classes onto Tailwind v4 * style: sort the composer's classes in Tailwind v4 order * fix: return a refused queued row to the durable queue instead of memory * i18n: localize the whole attachment-limit message * refactor: validate the reasoning override inside the packages/api facade * style: give the composer's submit slot its own IconButton variant * test: pin the submit slot to IconButton's submit variant * test: stub the seen-state hook the rebased MessagesView now calls * fix: turn file search on for an embedded file attached through the shared hook * chore: record the composer's design-rule counts against the rebased base * fix: keep the composer's container query inert under Tailwind v4 * test: point the v4 focus scenarios at the redesigned composer's controls * fix: keep a re-queued row on its own lineage after the run ends * test: assert a re-queued row carries its lineage * refactor: pass the dictation preferences into the composer from its host * test: read the usage atom from the store the ask hook renders under * fix: read a pending steer's pause from its own pane's branch * a11y: keep focus on the quote chips until the last one is removed * fix: keep staged reasoning for the next message across a manual compaction * test: expect focus on the remaining quote chip after removing one of two * style: sort the imports the canary rebase merged * refactor: remove the side-panel files table the account menu replaced * fix: settle a retried steer's late ack against a racing cancel A cancel clicked while the retry POST was in flight left the optimistic marker unread: the ack re-minted a live chip and injected words the user had asked back, and a failed retry after that cancel lost them entirely, because the failure path maps chips the optimistic cancel had removed. The ack path now defers to the marker the way the composer's does, the failure path re-adds the chip before settling it, and the queued row a late ack produces binds its recovery identity only under protocol v2, the same default the composer's degradation path trusts. * fix: strip a replayed reasoning override that no longer validates on resume A paused turn's reasoning override is replayed onto its resume request and then validated as if it were fresh client input, so an operator changing the endpoint's reasoning config between pause and approve made the checkpoint permanently unresumable with a 400. The replay now marks the request, and a replayed override that fails validation is stripped so the resume proceeds on defaults; a fresh override that fails is still rejected. * refactor: derive the enforced spec fields inside the modelSpecs facade The set of keys an enforced model spec locks is the input contract of the reasoning-override refusal rule, and deriving it in the route middleware left the policy untestable from packages/api and free to diverge from the facade that owns the rest of the preset application. applyModelSpecPreset now returns the enforced fields beside the private ones it applies, and the middleware consumes them. * fix: restore the pre-override parameters when a resume degrades Stripping a replayed reasoning override that no longer validates removed only the metadata field, while the resume had already replayed the paused turn's model parameters with the override applied into the endpoint option, so the provider still received what validation had refused. The strip now inverts the application from the replayed base: the recorded pre-override value returns and the thinking flag the override forced on comes off. * fix: resolve the effective responses route for upload targets The attach target returned the conversation or agent's saved useResponsesApi flag, so an Azure chat auto-routed to the Responses API by responsesApiRouting preflighted its uploads against the chat completions route and lost provider-document uploads. The resolver now applies resolveEffectiveUseResponsesApi with the endpoint, model, web-search state and routing policy, the same resolution the during-run upload target makes. * fix: name the interrupt chord in the hint only while it still interrupts The during-run hint advertised Alt+Enter Interrupt & send even when the chord had been claimed: a submitMessage rebound to Alt+Enter, a chord yielded to a global shortcut, or shortcuts disabled altogether each make the keydown resolver return something else. The hint now reads the same resolver verdict the during-run send button does and omits the chord it no longer owns. * test: assert the unseen count the scenario adds rather than a zero baseline The tab title counts every unseen conversation, and a shared verification run leaves legitimately-unseen conversations from earlier scenarios behind, so the three title-count scenarios failed on other scenarios' leftovers instead of the behavior they seed. Each now reads its baseline and asserts the count it adds and clears. * test: hold the unseen count to its direction on the mobile project The away poll that lights a dot also discovers conversations the client had not counted yet, so on the mobile project the title count can jump past any baseline taken mid-test. Opening still has to clear this conversation's contribution and an away reply still has to raise the count, which is what the assertions now hold. * test: enable the MCP Apps server through the redesigned palette The MCP Apps integration spec still drove the old composer's MCP Servers menu, which the redesign replaced with toggleable rows in the attach-and-tools palette; the lane failed on the missing menu on every run. The helper now opens the palette, waits for the server wave, and flips the row's pressed state. * fix: preserve numeric reasoning defaults on blur * test: reopen persisted code cards before checking highlights * test: follow the current file manager navigation * fix: offer only reasoning options a request override can carry * fix: keep the favorited copy when skills share a name * fix: restore composer focus by the interaction that closed the palette * test: expect a keyboard close to refocus the composer on every project * chore: re-record the design-rule backlog on the rebased composer * chore: drop the files table and label the rebase brought back * test: tolerate other projects' unseen conversations when marking one unread * test: open the file table header scenario from the account menu * fix: let the stop shortcut reach a hidden stop control from a dialog * fix: keep staged reasoning out of tool-limit recovery and MCP App turns * fix: degrade a queued turn's stale reasoning override instead of dead-lettering it * fix: refresh the attach target when web search changes the route * fix: re-arm the run-end drain after moving a steer to the queue * test: hover the MCP row before reaching its revealed Connect action * test: pick the parts scenario's MCP server from the tools palette * fix: cancel a recovered row's parked steer only after the composer takes it A recovered row's only durable copy is the steer parked on the server, and once its run has ended nothing can re-create it, so a composer that refused the row after the cancel left the words in memory until the next reload. The composer now takes the words first; a cancel that then fails holds the row out of the run-end drain instead of sending it a second time. * fix: name only the idle composer affordances that act * fix: keep steer escalation off until the pane owns the rendered conversation The branch tail that decides whether a run is paused is keyed by the pane's conversation, which can still name the previous chat while a cached one renders. Until the pane catches up, a pause on any cached branch now keeps Escalate disabled instead of reading a missing or foreign tail as unpaused. * fix: route the host's Enter and auto-send preferences into the composer's nested hooks ChatForm takes enterToSend and autoSendText from its host, but useTextarea, DuringRunSendButton and both speech engines still read the store atoms, so a host whose values differ advertised one behavior while the handlers ran another. Pass the host values through instead. * fix: settle a browser dictation take only after its final transcript lands stopListening resolves on the recognizer's end event, after the final result reached useSpeechRecognition's reducer but before React committed it, so the take settled while the composer still held the interim text. Settle from an effect declared after the transcript effect, off a state update queued behind the result. * fix: drop a restored reasoning override the current setting no longer offers The staged override was only checked for a matching parameter key, so a value restored after a capability change on the same target stayed staged, blocked live steering, and was dropped at send time. Validate it against the setting's options and range. * test: scan the attach and tools palette for accessibility The a11y suite still opened the removed tools dropdown. Open the palette instead, and add a scenario that runs Axe over the open palette. * fix: make the palette's virtualized list the grid that owns its rows The rows sat under a wrapper grid behind react-virtualized's scroller, which always renders aria-label and aria-readonly and so could not be made presentational. Axe reported both the disallowed attribute and the grid's invalid child. The scroller is now the labelled grid, with a rowgroup holding the rows. * fix: state the palette grid's row total and each row's position Only the rows in view are mounted, so without aria-rowcount and aria-rowindex assistive technology reads the rendered window as the whole grid. * fix: key an empty destination's pending steers to the route An empty tree has no message naming its conversation, so the pending-steer surface fell back to the chat context, which still names the source chat during warm navigation, and its controls acted on the source run. Fall back to the chat's messages key, which follows the route. * fix: keep assistant uploads available when the file config fails Assistants upload directly and never needed the destination policy, which is why the replaced AttachFileChat rendered their uploader outside the policy gate. Restore that exception so a failed or paused file config request does not remove every attachment entry. (cherry picked from commit f568a4b) Original-PR: LibreChat-AI#14473
(cherry picked from commit 3c7169c) Original-PR: LibreChat-AI#16361
(cherry picked from commit 25d74a6) Original-PR: LibreChat-AI#16361
(cherry picked from commit 66a6e63) Original-PR: LibreChat-AI#16361
…breChat-AI#16422) (cherry picked from commit f1d8d8a) Original-PR: LibreChat-AI#16422
…16737) * feat: Layered Surface Roles for the ClickHouse Theme Adds purpose-named roles for the chat canvas, user message, cards, sidebar rows, settings tabs, menu panels, composer and search pill. Each follows the surface it painted before, so the default theme is unchanged; ClickHouse steps them apart with Click UI tokens. * fix: Align Sticky Phase Headers, Boot Routes, and Row Fades With the Layer Roles * fix: Cover Trailing Slash and Bare Dashboard Paths in the Boot Canvas Route * fix: Match Boot Canvas Routes Case-Insensitively * test: Expect the User Message Role on Other-Author Turns * test: Expect the User Message Role on the Subagent Briefing
…6775) * feat: Move Chat Actions Into a Header Options Menu The header's export/share button becomes a chat options menu with Share, Export, Rename, Pin, Change project, Remove from project, Duplicate, Archive and Delete for the open chat. The sidebar row menu keeps Pin, Mark unread, Rename, Archive and Delete. * fix: Address Review Findings on the Header Chat Options Menu Limit read-only subagent threads to share and export, apply the sidebar's rename gate for running chats, let an unchanged title claim ownership, keep later navigation after a late archive, hide history-only actions for temporary chats, expose dialog semantics on the dialog items, and fix import sorting. * fix: Scope Header Chat Option Dialogs to the Conversation They Opened For ChatView stays mounted across a route change, so dialog flags now carry the conversation id they were opened for and reset when the open chat or the read-only state changes. * fix: Settle Header Delete and Rename Dialogs Against the Open Chat Read the open route when a delete settles, give the delete dialog the id its menu item controls, return focus to the menu item when a rename saves, and only let a close clear the dialog it belongs to. * fix: Ignore a Late Rename Result After the Dialog Is Gone A rename that settles after its dialog unmounted no longer closes the parent state or moves focus to the reused menu item. * fix: Dismiss Header Chat Options When the Route Leaves Their Chat The route moves before the chat state does and the menu and dialogs are portaled outside the hidden pane, so they are now dismissed on a route change and gated on it. A delete that settles after another chat opened no longer closes that chat's menu, and the options read the freshest cached copy of the chat instead of an older point entry.
…I#16436) * 📴 fix: Persist Cancelled Compactions With Their Identity * 🧪 test: Build the Cancelled-Compaction Leaf on a Real Conversation * 🧪 test: Assert the Withheld Controls on the Live Cancelled Compaction * 📴 fix: Keep Live Disconnect Compaction Rows Free of a Synthesized Failure * 🧪 test: Cover the Compaction Flag in the Redis Job Round Trip * 📴 fix: Keep a Stopped Compaction Honest and Its Anchor Untouched
…t-AI#16551) * 📴 fix: Finalize Compaction Snapshots and Move Abort Policy Into packages/api * 🔕 fix: Inspect Every Part When Finalizing and Reuse the Loaded Row * 📴 fix: Settle Finalized Compaction Rows and Verify Their Anchor * 📴 fix: Withhold the Unanchored Abort Final and Settle Marked Snapshots * 📴 fix: Consolidate the Compaction Abort Policy Into One Injected Operation * 📴 fix: Settle the Live Compaction Row Past an Anchor-Shaped Collision * 📴 fix: Lift the Failed-Turn Settlement Into the Compaction Module * 📴 fix: Keep a Removed Round's Failure and Guard Settled Jobs From Snapshots * ⚡ perf: Read Only the Anchor Id in the Abort Existence Check * 📴 fix: Withhold the Abort Final Only When a Row Needed Writing * test: Cover Stopped Compaction Finalization and the Missing Anchor in E2E * fix: Withhold a Stopped Compaction That Carries No Anchor Id * fix: Settle Stopped Compactions, Announce Finalized Rows, and Own the Abort Read in packages/api * test: Accept the Model Spec Query on the Stopped Reply Conversation URL * fix: Report the Anchor Read Failure Beside a Withheld Compaction Abort * fix: Synthesize a Compaction Failure Only for a Round After the Latest Outcome * fix: Skip the Anchor Read When a Stopped Turn Writes No Row * fix: Decide the Disconnect Snapshot Gate in packages/api The last-subscriber partial save now asks resolveDisconnectSnapshotMode for the settled-job decision instead of branching on the job record in the CJS controller. * fix: Leave an Already Settled Compaction Row to the Path That Settled It
* feat: Add a Focus Role for Rows Inside Content * test: Cover the Subtle Focus Role in the Mock Harness * test: Probe the Subtle Focus Role Through Its Theme Variable * test: Focus the Probe to Read the Subtle Focus Utility * test: Read the Subtle Focus Ring From Its Utility Property * fix: Ring the Subagent Progress Disclosure in the Subtle Focus Role
* feat: Let a Theme Quiet Decorative Borders * test: Cover the Chrome and Inset Border Roles in the Mock Harness * fix: Draw the Selected Category Tab Edge at the Chrome Share * fix: Route Chips, Subtle Icon Buttons and Hover Rings Through the Chrome Share * fix: Draw the Remaining Inner Card Dividers at the Inset Share
* fix: Keep Reasoning Out of Every Read Aloud Path Read aloud spoke the Thoughts section along with the answer. The hover button flattened think parts into the string it handed to TTS, browser autoplay and the hooks parsed content without skipping reasoning, and server streaming spoke the stored text field, which an aborted turn persists with its reasoning included. Add getSpeechText to data-provider as the single source for spoken text: text parts only for structured content, and the legacy :::thinking block stripped from plain text, treating a still-open block as reasoning so streamed speech only ever grows. Route the hover button, browser autoplay and server streaming through it, and skip reasoning in the TTS hooks' own content parsing. * fix: Speak String Message Content Without Its Reasoning Imported or client-built messages can carry their answer as string content rather than parts; read it like plain text instead of falling back to an empty text field. * fix: Keep Answers Between Repeated Legacy Thinking Blocks Strip every closed :::thinking block before treating an unterminated one as reasoning, so a second closed block no longer swallows the answer that follows it. * fix: Skip Read Aloud and Autoplay When Only Reasoning Remains A turn stopped mid-reasoning has no answer to speak once reasoning is filtered. Gate autoplay on the same speech text it will play, so the external engine never opens a stream that can only time out, and hide the Read aloud action when there is nothing to read. Every speech path reads a finalized message, so speak an unmatched :::thinking marker as the answer text the UI shows instead of dropping the rest. * refactor: Move Speech Chunk Polling Into packages/api The decision of what text to stream as speech lived in the legacy CJS service. Move the chunk processor to packages/api as createSpeechChunkProcessor, taking the message cache and lookup from the caller, and leave streamAudio.js wiring them in. Its tests move with it onto an in-memory cache instead of module mocks. * fix: Speak Exactly the Answer the Message View Shows Share the legacy :::thinking parser between the message view and speech, so Read Aloud strips the same block the view collapses instead of a separate pattern drifting from it. Trim structured speech text so a whitespace-only answer is not speakable, and key autoplay's audio cache by the spoken text, as manual playback already is, so audio cached with reasoning before this change is never replayed. * fix: Fall Back to Text When String Content Is Empty
…ibreChat-AI#16778) * 🚪 fix: Let Concurrent First OpenID Logins Continue as the New User Two requests for the same not-yet-created OpenID identity both missed findOpenIDUser and both called createUser; the unique email and OpenID indexes rejected the second insert, and that login failed with E11000. createUserIfAbsent (data-schemas) inserts a first-login user and reports a lost race as { ok: false, error: { code: 'user_exists' } }. It initializes the start balance under the new user's id before inserting the user, so the account is never visible without it, and removes that balance when the write fails or a unique index rejects the insert. createUser shares the insert-only credit (creditStartBalance). createOpenIDUser (packages/api) creates through it and, on user_exists, repeats findOpenIDUser with its provider and issuer checks and continues as the account that won, admitted and refreshed exactly as if the first lookup had found it: the account's own config (tenant config for a tenant account) and its email-domain policy apply, and applyOpenIDProfile writes this callback's claims. It continues only once the winner finished provisioning the account: when that config sets a start balance, the balance must exist, which a previous-release creator credits only after its insert. UserRecord and NewUserData are the plain, storage-neutral contracts. Related to LibreChat-AI#16766 * ↩️ fix: Hold a Recovered Login on the Creation Config's Start Balance The provisioning barrier read the recovered account's tenant config. With a tenant start balance the base config does not set, every concurrent first login in that tenant failed again, because the winner creates the user under the base config and login balance sync only initializes the tenant balance afterwards. The barrier guards against a previous-release creator's $inc landing on a balance login balance sync already initialized, and that creator only credits the base config's start balance. createOpenIDUser now holds the recovered login only when the config new users are created under sets a start balance; a tenant-only start balance is initialized insert-only by login balance sync and cannot be added twice.
…at-AI#16779) * 🔁 fix: Recover Concurrent First SAML, LDAP and Social Logins SAML, LDAP and social logins looked the user up and then inserted it with nothing in between, so two first logins for one new identity both inserted and the unique index failed the second with E11000. Every strategy now provisions first-login users in packages/api: createUserOnce inserts through createUserIfAbsent and, on user_exists, repeats the strategy's own lookup and resolves to the account that won, once its creator finished provisioning it (its start balance exists when one is configured), admitted exactly as a found account (a tenant account resolves its tenant config and that config's email-domain policy applies). provisionSamlUser, provisionLdapUser and provisionSocialUser own the create / recover / refresh decision, so a recovered account gets the same refresh as any account the login finds (claimSamlIdentity, the LDAP directory overwrite, handleExistingUser), and the CJS strategies only pass their models and services and map the result to passport. The lookups take a plain FindUserByFields and the contracts carry UserRecord and NewUserData. createOpenIDUser now goes through createUserOnce. Related to LibreChat-AI#16766 * ⚡ perf: Read a Recovered Account's Config and Balance Together After losing the insert race, createUserOnce resolved the tenant config and only then read the start balance, although the balance check now depends only on the creation config. Both reads start together.
…#16792 (LibreChat-AI#16792) Code workers can report the branch and head commit of the lane a command ran in. LibreChat now accepts an optional laneGit key on execute_command results, bounded to a 256 character branch without control characters and a 40 or 64 character lowercase hex head, and rejects anything else. The data-provider gains the pull request types, endpoint, query key and service method, and an endpoints.agents.pullRequests config block that is off by default and takes the token only as an environment variable reference.
…ibreChat-AI#16793) * feat: Record Lane Git State and Serve Conversation Pull Requests A conversation now stores the branch, head and repository its code lane last reported, written only by the server after a finished bash command and excluded from ordinary reads, generic saves, unset fields and imports. GET /api/convos/:conversationId/pull-request finds the pull request for that branch through an injected GitHub source with a bounded cache, answers null for an absence, and maps failures to stable codes without upstream text. The feature is off unless endpoints.agents.pullRequests is enabled with a token reference. * fix: Address Review Findings on Pull Request Lookup and Lane Writes Scope the lookup cache, shared in-flight request and rate limit cooldown to the credential, so one tenant's token never serves another. Ask for open pull requests on their own before falling back to the newest closed one, read every page of check runs up to a bound and never report an incomplete rollup as passing, order lane writes per conversation so an older write cannot land last, and name both required token permissions in the example config. * fix: Address Second Round Review Findings on Pull Request Lookup Clear the stored lane when the conversation moves to another workspace or leaves attached execution, so the previous workspace's pull request is never served. Fence the lane write on the time it was reported, so an older report from another replica cannot replace a newer one. Honor GitHub's reset window up to an hour instead of ten minutes, and expose the request timeout, whole lookup timeout and check run page limit as endpoints.agents.pullRequests settings with the previous values as defaults. * fix: Authorize Pull Request Repositories and Fence Lane Writes Require an administrator allowlist of repositories before the server token is used, since a worker reports its own repository. Match a pull request to the commit the conversation last ran at, so a reused branch name does not return another pull request. Fence the lane write on the workspace the command ran in, so a write queued before a move cannot bring the old lane back, and record a subagent thread's lane on the visible root conversation. Key the lookup cache and shared request by the recorded commit and the lookup policy, and document the worker flag in the example config. * fix: Order Lane Reports by a Database Sequence and Tighten Head Matching Replace wall clock ordering of lane reports with a per conversation counter reserved in the database when the command settles, so replica clock skew cannot reject a newer report. Treat a pull request whose head is behind the recorded commit as no match, and expose the candidate and comparison bounds of the history search as settings. Read the app config and the owner scoped lane together, inject the HTTP transport from the route instead of falling back to a global, skip a report identical to the last one written, and name the Contents permission that commit comparison needs. * fix: Queue Lane Reports by the Visible Conversation and Read Checks for the Current Head Order lane report reservations by the conversation the write lands on, so two subagent threads of one conversation share a chain, and write every report instead of skipping a repeat, since another recorder may have changed the lane in between. Read a pull request's detail before its check runs and use the head it reports, so line counts, mergeability and checks describe one commit. Treat an expired temporary chat as gone when reserving, writing and reading the lane, and make the shared cache capacity a setting. * fix: Fence Lane Reports by Attachment Epoch and Place Recorders Before Reporting Strip dotted descendants of lane fields from saves and imports, partition the pull request cache per credential, resolve the lane write target when the tool is created, and fence reports with a codeAttachmentEpoch that advances only on an owner move or detach. * fix: Fence Subagent Lane Reports by the Root Epoch and Cover Forks and Moved Heads Read the visible root's attachment epoch for a subagent report, gate lane recording on pullRequests.enabled, make the credential partition bound a config field, revalidate the detail head against the recorded commit, and find fork pull requests through the recorded commit. * fix: Keep the Credential Cache Bound Deployment Wide and Budget Head Revalidation Apply the largest configured credential bound to the shared cache, count cache hits as use, spend the comparison budget on a moved head, and fix the data-provider config default test. * fix: Cool Down Headerless Secondary Rate Limits A 403 whose message names a secondary rate limit now starts the credential cooldown for at least a minute instead of being cached as a ten second failure.
…ons (LibreChat-AI#16751) * feat: Quiet Prompt Editor Boxes, Table Columns and Destructive Buttons for a Theme * fix: Outrank the Cell Border Color With the Column Rule Share * fix: Keep the First Table Cell's Outer Edge Under the Inset Share * fix: Keep the Batched Edit Divider on the Light Border * fix: Keep Web Search Source Row Separators on the Light Border * fix: Quiet the Result Switcher Divider and Keep the RTL Table Edge * fix: Resolve Table Edges by Cell Direction and Keep the Variables Card Opaque in Details * fix: Draw Table Frame Edges in Logical Terms So Direction Needs No Selector * fix: Keep Soft Destructive Hover and Pressed Tints Above 4.5:1
…e Resolves (LibreChat-AI#16798) * 🏝️ fix: Apply the No-Workspace Decision Wherever an Agent's Code Route Resolves A conversation set to run without attached workspaces failed mid-turn with "Choose an attached workspace before using this agent." when its agent's default machine was attached and the turn loaded a skill or another tool at execution time. Request-start initialization honoured the decision, but the execution-time tool loader re-resolved the agent's attached default and then demanded a workspace selection the conversation intentionally never made. The per-agent code rule now lives in one typed function, resolveAgentCodeExecution (packages/api/src/code/agent.ts): it applies the conversation's no-workspace opt-out, the run-code grant, the stateful capability and the stateful environment policy, then resolves the base route. Request-start initialization, lazy subagent metadata and graph members, the execution-time and definition tool loaders, and the legacy execute_code loader all go through it, so an opted-out agent lands on the managed default route with no code tools at every site. Per-call subagent machine choices offer and accept no attached machine in such a conversation, and a source invariant test keeps new resolver call sites on the shared rule. * fix: Pass the Implicit Stateful Route Rollout Into the Shared Code Rule The per-agent code rule read the implicit stateful route rollout from the environment itself. It now takes it as a parameter, and the request boundaries (initializeAgent and withRequestCodeInputs) resolve it from the deployment configuration, so the rule stays a pure function of its inputs. * fix: Carry the Initializer's Code Opt-Out Into Tool Definition Loading initializeAgent hands the tool loaders a partial agent record (id, tools, provider, model) together with the code route it already resolved. Without the agent's route fields the definition loaders could not see a no-workspace opt-out and recomputed code execution as enabled, so tool classification could advertise the programmatic bash tool that execution then refuses. initializeAgent now passes attachedEnvironmentOptOut through every tool loader, and the shared rule keeps an opt-out its caller already decided.
…breChat-AI#16788) * fix: Reconcile stopped generations before detaching the stream * test: Cover immediate Stop and delayed terminal reconciliation * test: Accept both terminal outcomes of an immediate Stop --------- Co-authored-by: Lia <lia@librechat.ai>
Resolved 24 conflicted files. Kept MindFerry's rebrand, archive tabs, hub sections, Browse/Import UI and curated MCP browser, on top of upstream's refactored sidebar, ConvoOptions, routes and MCP panel header. - ConvoOptions, routes/index, ConversationsSection: took upstream's structure and re-applied the MindFerry additions. - convos route: dropped a duplicate getAppConfig require. - CLAUDE.md: upstream folded it into AGENTS.md; kept a one-line CLAUDE.md that imports AGENTS.md so Claude Code still loads the guidance. Not yet verified: typecheck and tests were still to run at commit time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu
Upstream's new test counted the archived view as a filter. MindFerry shows Chats/Archived as tabs, so the badge leaves it out; the test now says so. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu
Upstream's new shadcn design rules (no-restyle, require-static-classes) flag the MindFerry hub UI, which predates them. Following the tree's existing policy (eslint-suppressions.json gates new and edited code without a tree-wide migration), record the 9 files' current violations (31 total) so the ESLint config regression sweep sees no new diagnostics. No UI change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu
|
Two checks are red for reasons outside this PR's code, so I am leaving them as is:
The one red check caused by this merge, "ESLint config regression sweep", is fixed in 72cfd20 (the hub UI's design-lint violations are recorded in Generated by Claude Code |
|
Tests: api (shard 3/3) was red, and it was not caused by the sync itself. Three tests in Fixed in ac6039b (test-only): the three tests now resolve the attached environment the way their passing neighbours do, by setting When upstream fixes these tests, the next sync will conflict only if they change the same lines. Generated by Claude Code |
useSideNavLinks.spec.tsx (MindFerry's own spec) mocked ~/components/SidePanel/Files/Panel, which upstream removed in the sync, so the suite failed to start with "Could not locate module". The hook no longer loads that panel, so the mock goes. All 18 client specs MindFerry added or changed now pass against the merged tree. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu
Upstream's lane tests (LibreChat-AI#16793) call mockResolveCodeExecutionContext, which does not exist since LibreChat-AI#16798 renamed the code-route mock, so they throw a ReferenceError. Resolve the attached environment the way their passing neighbours do: configure statefulCodeSessions.environments and give the agent code_environment_id. Test-only; upstream main (e1dfc10) has the same failure. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu
…files The synced tooling now orders Tailwind classes and sorts imports differently, so 14 MindFerry files (hub UI, About, MCP browser, export menu, sidebar) failed the Static checks job. Formatting and import order only; no behavior change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu
Summary
MindFerry last merged upstream LibreChat at
c8c5478(2026-09-28, LibreChat-AI#16425). Upstream has since moved 326 commits (to LibreChat-AI#16803, 2026-10-06). This PR merges upstreammaininto MindFerry and resolves the 24 files that conflicted, so the fork picks up upstream's fixes and refactors again while keeping the MindFerry rebrand, archive tabs, hub sections, Browse/Import UI and the curated MCP server browser.How it works
Most conflicts were in client files that upstream refactored. For the three heaviest, upstream's version was taken and the MindFerry additions were re-applied on top:
ConvoOptions.tsx: upstream moved share, duplicate and project actions out of the menu. Re-applied the archive announcement (archiveAnnouncementKey) and the "failed to save to the MindFerry archive" toast.routes/index.tsx: re-added themindferry/connectroute and its import.ConversationsSection.tsx: re-addedStatusTabs, theHubSectionfor the Archived view, and hid Projects and Pinned there.Other resolutions keep both sides:
api/server/index.js: the JSON parser keeps therawBodycapture the Slack bridge needs, wrapped in upstream's newexcludeRumBodyParser.routes/convos.js: both sets of@librechat/apiimports; dropped a duplicategetAppConfigrequire.chatFilters.ts: bookmarks count once (upstream), and the Chats/Archived view is left out of the badge because the tabs already show it (MindFerry). The new upstream test that expected archived to count was updated to match.MCPBuilderPanel.tsx: upstream's newPanelHeader, with the curated-server browse button placed beside the add button.DEFAULT_APP_TITLE,Marketplace.tsxtitle andStartup.tsxkeep the MindFerry name on top of upstream's title-revision helper..github/workflows/docker-publish.yml: Docker Hub token stays optional; the legacy GHCR mirror upstream removed is dropped.CLAUDE.md: upstream folded it intoAGENTS.mdand deleted it. A one-lineCLAUDE.md(@AGENTS.md) is kept so Claude Code still loads the guidance; delete it if unwanted.Follow-up fixes made after CI first ran on the merge:
eslint-suppressions.json: records the 31 design-lint violations (shadcn/no-restyle,require-static-classes) in 9 hub UI files that predate upstream's new rules, the same way upstream baselines its own legacy code. No UI change; clearing them means restyling those files with@librechat/clientvariants.useSideNavLinks.spec.tsx: dropped a mock ofSidePanel/Files/Panel, which upstream deleted.ToolService.spec.js: three upstream lane tests called an undefined mock (mockResolveCodeExecutionContext); the same failure exists on upstreammain(e1dfc104). Test-only fix.Type of change
Testing
CI on
c726a11d: all unit, typecheck, static-check, ESLint-sweep, integration (Mongo and Redis), Docker, lighthouse and e2e jobs pass. One e2e job (redis transport, shard 1/2, a timing-sensitivesteering-escalationtest untouched by MindFerry) failed once and passed on re-run, while all six memory shards passed first time.Two checks stay red for reasons outside the code:
Retarget on open: tries to move the PR onto adevbranch this fork does not have.axe-linter: the workflow cannot resolvedequelabs/axe-linter-action@v2.Not verified by anyone yet: the merged Archived tab, conversation ⋯ menu, MCP panel header and
/mindferry/connecthave not been exercised in a running app.Risk / compatibility
/mindferry/connectafter deploy.contextHubschema is unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu