Skip to content

Sync with upstream LibreChat main (326 commits since 2026-09-28) - #2

Merged
innovatorslabsky merged 332 commits into
mainfrom
claude/upstream-sync-2026-10
Oct 12, 2026
Merged

innovatorslabsky merged 332 commits into
mainfrom
claude/upstream-sync-2026-10

Conversation

@innovatorslabsky

@innovatorslabsky innovatorslabsky commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

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 upstream main into 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 the mindferry/connect route and its import.
  • ConversationsSection.tsx: re-added StatusTabs, the HubSection for the Archived view, and hid Projects and Pinned there.

Other resolutions keep both sides:

  • api/server/index.js: the JSON parser keeps the rawBody capture the Slack bridge needs, wrapped in upstream's new excludeRumBodyParser.
  • routes/convos.js: both sets of @librechat/api imports; dropped a duplicate getAppConfig require.
  • 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 new PanelHeader, with the curated-server browse button placed beside the add button.
  • DEFAULT_APP_TITLE, Marketplace.tsx title and Startup.tsx keep 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 into AGENTS.md and deleted it. A one-line CLAUDE.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/client variants.
  • useSideNavLinks.spec.tsx: dropped a mock of SidePanel/Files/Panel, which upstream deleted.
  • 14 files: Prettier Tailwind class order and import order under the synced tooling. Formatting only.
  • ToolService.spec.js: three upstream lane tests called an undefined mock (mockResolveCodeExecutionContext); the same failure exists on upstream main (e1dfc104). Test-only fix.

Type of change

  • Merge of upstream changes (no new feature)

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-sensitive steering-escalation test 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 a dev branch this fork does not have.
  • axe-linter: the workflow cannot resolve dequelabs/axe-linter-action@v2.

Not verified by anyone yet: the merged Archived tab, conversation ⋯ menu, MCP panel header and /mindferry/connect have not been exercised in a running app.

Risk / compatibility

  • The conflicted client files are where MindFerry's UI meets upstream's refactors, so regressions there are the main risk. Check the Archived tab, the conversation ⋯ menu, the MCP panel header and /mindferry/connect after deploy.
  • No config or stored-data changes beyond what upstream shipped; the contextHub schema is unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HUZnrJ6DoLGzKRxd5uZGFu

berry-13 and others added 30 commits October 1, 2026 03:46
* 🫗 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
…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
* 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
…-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
…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
berry-13 and others added 22 commits October 5, 2026 21:22
…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

Copy link
Copy Markdown
Owner Author

Two checks are red for reasons outside this PR's code, so I am leaving them as is:

  • Retarget on open (pr-retarget-dev.yml): the workflow tries to move every PR against main onto a dev branch, and this fork has no dev (Proposed base branch 'dev' was not found). This PR deliberately targets main. It will fail on any PR to main in this repo until the workflow is adjusted or a dev branch exists; no fix is ported here.
  • axe-linter (a11y.yml): the job cannot resolve the action (Unable to resolve action dequelabs/axe-linter-action@v2, unable to find version v2). It fails before any code is checked.

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 eslint-suppressions.json, no UI change). I will merge once the remaining checks on that commit finish green, apart from these two.


Generated by Claude Code

innovatorslabsky commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner Author

Tests: api (shard 3/3) was red, and it was not caused by the sync itself. Three tests in api/server/services/__tests__/ToolService.spec.js (the "records the lane…" test and the two "does not enable lane recording…" cases) failed with ReferenceError: mockResolveCodeExecutionContext is not defined. That file and ToolService.js are byte-identical to upstream main (e1dfc104, the latest upstream commit), so upstream has the same failure: LibreChat-AI#16793 added the tests against a mock that LibreChat-AI#16798 renamed.

Fixed in ac6039b (test-only): the three tests now resolve the attached environment the way their passing neighbours do, by setting statefulCodeSessions.environments and giving the agent code_environment_id: 'personal-machine'. The whole suite passes locally (158 tests). An earlier suggestion in this comment, to swap the call for mockResolveCodeExecutionWorkspaceContext.mockImplementationOnce, was wrong: it leaves the once-mock unconsumed and breaks later tests, so it was not used.

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
@innovatorslabsky
innovatorslabsky marked this pull request as ready for review October 12, 2026 07:19
@innovatorslabsky
innovatorslabsky merged commit 59a383a into main Oct 12, 2026
57 of 61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.