[pull] main from danny-avila:main - #262
Merged
Merged
Conversation
* 🧭 fix: Let a Sealed Chat Follow Its Agent to a New Machine A conversation's code-environment decision is sealed on its first turn, but the environment an agent uses stays editable. Pointing an agent at a different attached machine left every chat sealed to the old one with no valid move: the client showed an unusable workspace chooser, and any other selection was rejected by the server as locked. Forking copied the stale seal. The owner can now move such a chat explicitly. The composer names the machine the chat used and the one its agent uses now, and moving it goes through an owner-scoped endpoint that compare-and-swaps the persisted decision after validating the new workspace against the live worker. A move never switches the workspace of an environment the decision already covers, never upgrades a chat that continues without an attached environment, and is refused while a generation is in flight. A saved chat also no longer auto-selects a sole workspace for an environment its decision never covered, which submitted a selection the server always rejected. * fix: Hold Moves Through Terminal Saves and Move Every Target at Once A terminal claim marks a generation complete before its response save lands, and that save still writes the decision the run started with, so a move is now refused until terminalPersistencePending clears. The API advertises codeEnvironmentMoveVersion, and the client offers a move only when it is present, so a new client never exposes an endpoint an older API lacks. The move menu picks one workspace per new environment, with a sole workspace preselected, and a single confirm sends the complete target set in one request, so a partial move can no longer leave a half-migrated decision. A new machine that advertises no workspace shows its unavailable state instead of an empty section, and the confirm stays disabled. * fix: Keep Runs From Rewriting Moved Decisions and Gate Moves by Policy Every agent turn used to persist the decision it started with, so a run from any ingress that settled after a move could write the old decision back: the chat controller reads the conversation before its generation job exists, and Responses API runs never enroll one. Runs now persist a decision only for a conversation that does not store one yet, and a legacy row records only the mode it inferred, so no admission or persistence order can revert a move. Moves are now an operator setting, statefulCodeSessions.conversationMoves.enabled, off by default. The API advertises codeEnvironmentMoveVersion and accepts moves only where the effective policy enables them. A saved chat carrying attached selections no longer auto-selects a sole workspace even when selection-less decisions are not advertised, because the server seals those selections either way. * fix: Fence Moves Across Remote Runs and Let Moves Drop Unused Machines A Responses API run is keyed by its response id, so the conversation stream alone could not show it still working on a chat. A move is now refused while any generation that can still act for the conversation exists, using the same conversation-scoped enumeration conversation deletion drains. A move now validates every workspace it persists, not only the added ones, so a successful move never hands back a decision whose kept workspace is gone. A chat sealed to machines its agents no longer all use could not be sent or moved: the client trimmed the unused selection and the server rejected the trimmed set as locked. A saved chat now treats that seal as unresolvable, offers a move that drops the unused machine, and the server accepts drop-only moves. * fix: Own the Decision Fallback in TypeScript and Settle Move Caches The fields a run may persist when it never resolved a decision were chosen by a branch in AgentClient; resolvePersistableCodeEnvironmentDecision now owns that fallback too, under the same never-overwrite rule, and the CJS method only wires the request into it. Workspace choices made in the move menu belong to the chat they were made in, so another relocatable chat targeting the same machine starts undecided. A successful move cancels conversation, pinned, and list reads already in flight before writing the moved decision, so a response from before the move cannot restore the replaced decision in the cache.
* fix: elect one expired-file sweeper * fix: unify clustered sweep ownership * fix: retain clustered sweep fallback * fix: use normalized Redis sweep ownership * fix: reconcile sweep startup and concurrent leader ownership
* 📜 style: Scroll the Sidebar as One Surface Projects, Pinned and the chats list each owned a scroll pane, so only the chats list scrolled while the sections above it stayed pinned. The sidebar now has one scroll viewport holding all three: the virtualized chats list renders at its natural height and is windowed by that viewport instead of scrolling itself. `useOuterScrollWindow` translates the viewport's scroll position into the list's coordinates. It re-measures on scroll and observes both the viewport and the content wrapper, because a section collapsing above the list moves the list without resizing either the viewport or the list. * 🧪 test: Cover the Sidebar's Single Scroll Surface Four scenarios in the mock harness: the whole sidebar rides one wheel gesture, older chats keep loading as it reaches its end, collapsing a section above the chats leaves the visible rows painted, and a long pinned list scrolls with the sidebar instead of inside its own box. The chat list grid no longer matches its container's height — it renders at the height of the rows it holds — so the width-tracking spec measures width alone and seeds the rows it measures. * 🪟 fix: Keep the Windowed List Honest Without ResizeObserver A section collapsing above the chats list moves it without resizing the viewport or the list and without a scroll event. Where ResizeObserver is missing, the fallback only listened for window resizes, so the list kept windowing against the old offset and could paint the wrong rows. It now watches the content wrapper the same way the observer does: its mutations and the bubbling transitionend of its height tween. The hook gets its own spec — offset translation, clamping at the top, frame coalescing, the collapse re-measure, the fallback trigger and listener cleanup — and the scenario specs reach the drawer on emulated touch devices, where the header's opener appears only once the chat has rendered and a wheel gesture is not what a person produces. * 👁️ fix: Window the Chats List on What the Reader Can See The list was handed the whole sidebar's height as its window, so with a screenful of projects or pins above it, react-virtualized counted a viewport of chat rows as rendered at the top of the list and the eight-row pagination threshold was crossed before a single chat had been exposed — a second conversations request nobody had asked for, on the load path the Lighthouse lane guards. The window is now the slice of the list the viewport actually shows: zero while it is below the fold, the viewport's height once it fills it. Searching also kept the shared surface's scroll position while replacing what it held — Projects and Pinned leave and the chats become results — so results long enough to escape the browser's clamp opened partway down. The surface returns to the top whenever it swaps contents. * 🧪 test: Seed the Pagination Scenario's Pins Out of the First Page * 🧪 test: Claim the Settled Layout in the Pagination Scenario * 🩹 fix: Keep a Pixel of Window So an Unseen List Can Be Reached A list handed a zero window renders nothing, and react-virtualized only emits its inner container when it has children, so the list collapsed to no height and the surface had nothing to scroll toward: the chats below a screenful of pins could never be reached. The window keeps a single pixel, which renders the first row and restores the list's own height without claiming a screenful is on display. * 🧪 test: Scroll Until the Chats List Answers * 🚦 fix: Ask for Another Page Only Once the Chats Are in View A list below the fold still renders its first row — that pixel of window is what gives it a height to be scrolled to — and on a page whose chats are nearly all pinned, that single row already sits inside the eight-row threshold, so the list asked for another page before anyone had looked at it. Reaching the end of what is rendered now counts only while the list is on screen; the hook reports that separately from its height, and a page holding no chats at all is still drained by the all-pin effect. * 🔎 fix: Ask the Layout Whether the Chats Are in View Visibility was a value sampled on a frame, and a commit that swaps what the sidebar holds — leaving a search restores the sections and the unfiltered page together — moves the list before any observer runs, so rows reported during that commit were judged against the layout before it. The hook now answers from the layout as it stands when asked, and the pagination gate asks.
…15913) * 🧷 fix: Skip Failed Summaries When Resolving the Conversation Checkpoint * 🧷 fix: Keep a Failed Summary Out of the Model-Facing History Boundary * 🧭 refactor: Own the Summary Checkpoint Predicate in packages/api * 🧯 fix: Reject Unfinished Summaries Everywhere and Recount Stripped Turns * 🧬 fix: Version Event-Actor Summary State So Legacy Prefixes Cannot Warm * 🏷️ fix: Stamp Inherited Summaries Where Actor State Is Assembled * ⚖️ fix: Drop Unusable Summaries While Building the Prompt * 🧷 fix: Repoint Prompt Content Instead of Splicing Stored Parts * 🧭 fix: Treat a Streamed Summary Without a Boundary as Unfinished The server has stamped `failed` on an errored summarize round only since #14546, and `summarizing` never reaches storage, so every round that errored on v0.8.7 or rc2 was persisted as a plain summary holding its partial deltas. The flag checks cannot tell that row from a checkpoint, so upgraded conversations kept losing the history before it. The aggregator settles it structurally, identically in every SDK release that has shipped compaction (3.1.63 through 3.8.6): deltas stream `content` blocks into the part, and only a completed round replaces the part with the final block, which is the sole writer of `boundary`. A `content`-block summary without a boundary therefore never finished. `isUsableSummaryPart` and the shared `isCompactedLeaf` both apply that rule; bare-`text` rows predate the aggregator and stay governed by the flags. Fixtures that stand for completed summaries now carry the boundary a real run writes, and the flag fixtures keep one so they still test the flags. * 🔁 fix: Strip Unusable Summaries From Responses API Continuations `previous_response_id` loads the stored conversation and hands each message's `content` array straight to `formatAgentMessages`, whose summary scan takes the last summary part carrying text as the history boundary. The chat client strips unusable summaries before that scan; this entry point did not, so a conversation continued through the Responses API still lost everything before a failed round. The same packages/api helper now runs ahead of the formatter. * 🧭 refactor: Own Event-Actor Summary Selection in packages/api `getLatestEventActorSummary` decides which summary a warm continuation carries forward, how a missing token count is recorded, and how the state is stamped. That is compaction behavior, and every helper it relies on already lives in `packages/api/src/agents`, so it moves beside `isUsableSummaryPart` and the controller imports it. * 🧪 test: Give the Finished Compaction Leaf the Boundary a Completed Round Writes `isCompactedLeaf` now requires the boundary only a completed summary block carries, so the client fixture for a finished compaction needs one, and a boundaryless leaf joins the interrupted compactions that can be retried. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
* fix(schedules): forward renewable MCP credentials * fix(schedules): require unattended MCP credentials * fix(schedules): resolve MCP credentials during execution * refactor(schedules): inject unattended credentials * fix(schedules): scope renewable credential resolution * fix: isolate lazy scheduled OBO credentials across MCP lifecycle * fix: preserve MCP cancellation and retryable credential lookup * fix: bind MCP cancellation to the owning run signal * test: supply flow manager in OBO cancellation regression * fix: preserve MCP cancellation across exchange and API ingress * style: sort MCP manager imports * fix: propagate cancellation through agent discovery * test: preserve agent discovery edge literals * fix: propagate agent initialization cancellation * fix: expose initialization signal in OpenAI adapter
* 🧯 refactor: State Failed Turns in Copy a Reader Can Act On
Every client-facing error code now has authored copy, so no failed turn renders
raw JSON, an unresolved `com_error_*` key, or a provider message cut at 512
characters. `client/src/components/Messages/Content/Error.tsx` becomes a module
directory: a dispatcher resolving `json.code ?? json.type`, a registry splitting
one-sentence copy from payload-aware renderers, and shared parts that resolve the
provider, model, user-key ownership and compaction capability behind the failure.
Message-limit violations persist `resetAt`/`retryAfterSeconds`, so the row counts
down to the window's reset instead of naming a window length alone.
* 📸 chore: pin before/after error copy screenshots for the pull request
* 🧹 chore: drop the pinned pull request screenshots from the tree
* 🔻 style: Trail the error disclosure chevron after its label
* ⏱️ refactor: Derive Limiter Resets in packages/api, Stamp Key Expiry as ISO
The message limiter's reset fallback and retry arithmetic move out of the
CJS middleware into `getRateLimitReset` in packages/api, which both the
message and agent-event handlers now call, so /api keeps only the wiring.
`checkUserKeyExpiry` persists `expiredAt` with `toISOString()` instead of
the server's `toLocaleString()`, which a client in another locale could
parse with day and month swapped.
* 🪪 fix: Attribute Failed Turns to the Row, Agent and Payload That Failed
Error content parts render through `Part` without their message, so their
copy named the conversation's current endpoint and model rather than the
ones that failed. The chat, flat-thread and search row renderers now supply
the row's identity through `ErrorSourceProvider`, and the dispatcher reads
it whenever no row is passed directly.
A saved agent's row carries the `agents` endpoint and the agent id as its
model, so identity, key ownership and the key dialog resolve against the
agent's own provider and model; an endpoint named by the payload (an
expired key) outranks both. The agent builder is only suggested where the
side panel would offer it: agents configured, builder not disabled, USE and
CREATE held, and an EDIT grant or administrator role.
Also in the renderers:
- structured refusals read Anthropic's `stop_details.explanation`
- provider bodies nesting their message under `error` keep that message
- upstream model failures name the provider when it is known
- `INVALID_USER_KEY` offers the key dialog on a user-provided endpoint
- an oversized message is never told to compact the conversation
- the countdown is bounded by the limiter's window, not a fixed day, and its
ticking text is hidden from the assertive live region
- an expiry persisted in a server locale is shown as written
- search rows that failed render through the dispatcher, not as markdown
- the payload type is an explicit JSON value type
* 🧪 chore: Seed Real Refusal, Legacy Expiry and Nested Provider Error Shapes
The gallery's refusal case used a string `info` no producer writes; it now
carries Anthropic's stop metadata and a Bedrock content filter. It also
seeds an expiry stamped in a server locale, an unreadable key on a
user-provided endpoint, and a provider body nesting its message under
`error`, and removes whatever it inserted when a write fails.
* 🧵 fix: Keep the Reason From a Provider Body Embedded in Failed-Run Text
A failed run persists a provider error as `<base message>: <SDK message>`,
and Anthropic's SDK message embeds the response body, so the persisted text
reads `…request: 400 {"type":"error","error":{"message":…}}`. The
fallback renderer dropped that body and kept only the prefix, losing the
reason. It now swaps the body for the message it carries.
* 🔀 fix: Resolve Handoff Agents, URL-Owned Credentials and Nested Error Codes
An error part after an `AGENT_UPDATE` belongs to the agent the run was
handed to. `ErrorSourceProvider` now records where a row's handoffs sit,
and an error part resolves the agent active at its own `partIndex`; search
and shared rows give error parts that position too. A handoff the agents
map cannot resolve names no provider instead of the agent that handed off.
A user-provided base URL makes the reader's own record hold the key as
well, so key failures on such an endpoint offer the key dialog. `NO_BASE_URL`
and `INVALID_BASE_URL`, which only a user-provided URL produces, render
through the same renderer with named copy and an add or update action.
OpenAI-compatible bodies nest `code` and `type` under `error`; when the top
level names neither, the dispatcher reads the envelope, so a nested
`invalid_api_key` reaches the key renderer.
* 🕰️ fix: Keep Unknown Key Ownership Generic, Measure Resets From the Row
A shared link's viewer has no endpoint configuration, so whether the reader
or the deployment owns a key is unknown; key copy there read that as
deployment-owned and told the viewer to ask an administrator. Ownership is
now undefined without a configuration, and the copy stays generic.
The countdown's window bound used this device's clock, so a device running
behind the server could drop a valid reset. The window now runs from the
row's own timestamp, which the server stamped alongside `resetAt`.
* 🗜️ fix: Suggest Compaction Only Where the Compact Action Is Offered
Manual compaction is only reachable from the context usage popover, which
`interface.contextUsage: false` removes. Context errors on such a deployment
suggested compacting anyway; they now fall back to the standard next steps.
* 🧭 fix: Keep Row Identity Authoritative and Every Generation Charge Visible
A row's own endpoint and model now stand even where it lacks them, so a
restored failure no longer borrows whatever the conversation selected since;
only an error rendered with no row reads the conversation, and an agents row
with no stored agent id (the live row of a turn that just failed) still
resolves the conversation's agent.
Also:
- a token-balance row lists every generation instead of the first 20
- search rows re-render when their `error` flag changes
- `AUTH_CROSS_ORIGIN`, added since this branch began, renders the login
page's copy and has a gallery case, so the seed's completeness check runs;
a table test now fails for any enum member without authored copy
- upstream model failures keep the provider-neutral sentence the server
persists as their text, which the e2e suite reads the row by
* 🪞 fix: Resolve Ephemeral Agent Ids and Keep an Agents Row's Model Name
An agents row whose model is a model name, as an ephemeral agent's is,
lost both its provider and its model whenever no saved agent resolved.
Agent ids now resolve the way sibling headers resolve them: a saved agent
through the map without the parallel `____N` suffix, an ephemeral agent
through the endpoint and model its id encodes. A row that still resolves
nothing keeps the model name it recorded; an agent id is never shown.
* 🛤️ fix: Attribute Lane Errors to Their Agent, Count Live Retries Relatively
A parallel run stamps each part with the agent of its lane, and an error
part in one lane was attributed to whichever handoff last preceded it. The
row now records error parts' own agents alongside its handoff positions,
and an error part's own agent comes first.
A live countdown subtracted this device's clock from the server's
`resetAt`, so a device running fast could report the limit already over.
`retryAfterSeconds` is now counted from the row's own timestamp, which the
device stamps on a live row and the server on a restored one.
Search results read a cached startup config but have no compact control, so
compaction guidance now also requires the chat surface.
* 🧷 fix: Resolve an Agents Row From Its Own Model, Never the Current Agent
An agents row that stored a model name, as an ephemeral agent's does, fell
back to the conversation's current agent when no saved agent resolved, so
switching the conversation to a saved agent later rewrote who produced the
old failure. The fallback served no real row: the client's live placeholder
for an agents turn stores the conversation's agent id as `model`, as
persisted rows do. The row's `model` now decides on its own — a saved or
encoded ephemeral id resolves, a model name is kept, and the `ephemeral`
placeholder names nothing; only an error with no row reads the conversation.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: route Azure Astra through Responses API
* fix: keep Azure Astra deployment alias out of summarizer overrides
The Azure deployment alias in `modelKwargs.model` is spread after `model`
by langchain, so the SDK's same-provider summarizer, which copies the
agent's client options and overrides only `model`, still reached the Astra
deployment when configured with a different model. Hand the summarizer the
agent's kwargs without the alias whenever its model differs.
Also derive the Astra routing decision once, reuse the first-party OpenAI
predicate in the GPT-5.6 rule, pin the opt-out shape in the Azure spec, and
expect `firstPartyEndpoint` for canonical Azure hosts in the backward-compat
spec.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: resolve Azure request URLs and summarizer deployments
* fix: honor explicit OpenAI summarization for Azure agents
* 🧭 fix: Degrade Unresolvable Azure Summarizers Without Failing the Run
Summarization target resolution for Azure agents threw inside createRun, so a
summarization setting that could not be resolved failed every agent run before
any model call, not just compaction. The documented `summarization.provider:
openAI` block with the `.env.example` default `OPENAI_API_KEY=user_provided`
broke all Azure agents, and a summary model missing from the Azure
configuration did the same.
- An explicit OpenAI summarizer without server credentials now disables
summarization for that Azure agent with a one-time warning instead of
throwing; the agent's own client is not a fallback because Azure Responses
shares the `openAI` provider.
- A summary model the Azure configuration cannot resolve warns once and keeps
the agent's client, as before this branch.
- A summary group without a base URL no longer inherits the agent group's
`azureOpenAIBasePath`, which sent the summary key and deployment to the
agent's resource.
- Serverless summary groups without a version send no empty `api-version`.
* 🧪 test: Assert No Summary Request Leaves a Disabled OpenAI Summarizer
* 🧯 fix: Disable Summaries Whose Azure Deployment Cannot Be Resolved
When the Azure configuration could not resolve a summary model, the summarizer
fell back to the agent's own client. On an Azure Responses agent that stripped
the deployment alias and sent the logical model name where Azure expects a
deployment, so every compaction failed; on a chat agent the URL-pinned
deployment silently served a different model than the one configured.
An unresolvable Azure summary target now disables summarization for that agent
with a one-time warning, matching the unreachable OpenAI target: the run
proceeds on context pruning and no summary request is sent.
* 🔑 fix: Treat an Empty Azure Summary Key as Unavailable
`azureBaseSchema` accepts `apiKey: ''`, and `mapModelToAzureConfig` passes it
through, so a summary group with an empty key kept summarization enabled and
built a client with no credential. The agent flow already rejects an empty
Azure key during initialization; the summary resolver now disables
summarization for that target with the same one-time warning as a
user-provided key or base URL.
* 🧭 fix: Resolve Azure Summaries From Every Supported Azure Configuration
Three Azure configuration shapes the summary resolver and URL builders did not
cover:
- Legacy `AZURE_*` environment credentials: without an `azureOpenAI` block the
resolver skipped Azure, so an Astra agent's summarizer reused the agent's
Responses client and sent the logical summary model where a deployment
belongs. The target now resolves from the environment the way
`initializeOpenAI` does (including `AZURE_USE_MODEL_AS_DEPLOYMENT_NAME`) and
is disabled when those credentials are user-provided or missing.
- `summarization.parameters` `baseURL` and `apiKey` overrides now apply to Azure
summary targets, as they already did for explicit OpenAI targets, through one
shared helper.
- A full Azure hostname used as the instance name (as `genAzureEndpoint`
accepts) is used as the host for both the default Responses URL and the chat
deployments path, instead of being suffixed into
`<host>.openai.azure.com`.
* 🔐 fix: Expand Summarizer Credential Placeholders and Bound Warnings
Admin-authored `summarization.parameters` `apiKey` and `baseURL` values were
forwarded verbatim, so `${SUMMARY_KEY}` reached the client as a literal key
while Azure groups and custom endpoints expand the same syntax. They are now
expanded once through `extractEnvVariable` (keeping its sensitive-variable
denylist) before resolution and before the parameters are layered over the
client, and a credential or base URL that still holds an unresolved
placeholder disables summarization like a missing one. Azure base URL
templates (`${INSTANCE_NAME}`, `${DEPLOYMENT_NAME}`) are not treated as
unresolved.
The one-time warning set is now keyed per tenant, so one tenant's report no
longer hides another's, and resets after 256 distinct entries so changing
configurations cannot grow it without bound.
* 🧩 fix: Keep Azure URL Templates Out of Summarizer Env Expansion
Expanding `summarization.parameters.baseURL` through `extractEnvVariable`
also replaced Azure's reserved `${INSTANCE_NAME}` and `${DEPLOYMENT_NAME}`
templates whenever the host environment happened to define those names, so a
template override could route compaction to the host's instance or deployment
instead of the summary group's. Expansion now skips those two templates and
leaves them for `constructAzureURL`, and the unresolved-placeholder check reads
the same segments.
The warning deduplication set now evicts the least recently seen entry at its
bound instead of clearing, so warnings for problems that are still recurring
are not all logged again at once.
* 🔀 fix: Reset Inherited API Mode for Custom-Endpoint Summarizers
A custom OpenAI-compatible summarization endpoint resolves to the same `openAI`
provider as an Azure Responses agent, so the SDK layered its overrides over the
agent's client options and kept the agent's `useResponsesApi`,
`firstPartyEndpoint`, reasoning and request kwargs. An Azure Astra agent
therefore sent its compaction to the gateway's `/responses` with Astra shaping.
When the resolved custom provider matches the agent's, those fields are now
replaced from the target's own configuration, and explicit summarization
parameters can still opt back in.
Azure's reserved `${INSTANCE_NAME}`/`${DEPLOYMENT_NAME}` URL templates are now
preserved only for Azure summarization targets, so an OpenAI or custom base URL
can reference environment variables with those names.
* fix: Resolve Azure Summary Identity, Transport, and Token Limits 🧭
* fix: Resolve Headers Before Building Summary Clients 📨
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🪧 feat: Unify Artifact Triggers as Tool Rows Every chat-side artifact trigger — code-execution output, a model-authored `:::artifact` block, and a mermaid diagram — now renders through one `ArtifactRow` that sits on the tool-row axis instead of three card variants in a wrapped chip strip. The row carries the file type twice: the language's brand glyph where one exists, and a format badge for titles that are not filenames. `kind.rendersPreview` tints the glyph and folds the same fact into the accessible name, so the row says whether opening it yields a rendered view or source to read. The mermaid panel routes every download through its export menu — SVG, PNG and the diagram source — rather than parking a second, unlabelled button beside it. The menu turns the pressed format into its own loading row, defers the synchronous SVG export by a macrotask so the spinner paints before the frame blocks, and no longer disappears on the code tab. A mermaid download keeps its real filename: its content *is* the stored `.mmd`, so it is not suffixed `.preview`. `useArtifactDownload` holds the original-file vs. edited-content decision that both the generic download button and the mermaid menu need. * 🩹 fix: Save the Stored Artifact File, Not Its Cached Text The panel serialized a mermaid diagram from `artifact.content`, which is the backend cache `extractUtf8` truncates at 512 KB, so "Download source" saved a corrupt `.mmd` for any larger stored diagram. An unedited mermaid artifact now takes the same original-file route as every other text-backed artifact, `handleDownload` reports whether bytes were actually delivered, and the export menu turns a failed delivery into its failure status and toast instead of "Diagram download started.". A `text/plain` row announced "Opens as source" while the panel opened it on the rendered markdown preview; it now announces the preview. A pending preview placeholder keeps the row slot it arrived in rather than sinking to the end of the artifact group and jumping up once it resolves. * 🧪 test: Exercise Artifact Rows and Mermaid Exports End to End Seven acceptance scenarios in the mock harness cover what the unit suites cannot: that a message which writes five artifacts shows five trigger rows on one axis, that a row's accessible name matches what the panel actually does with the artifact (rendered preview vs. source, including `text/plain`), that the mermaid export menu offers SVG, PNG and the diagram source on both the preview and the code tab, that "Download source" delivers the stored file and announces a failure when the stored file is unreachable, and that a pending preview placeholder keeps the row slot it arrived in. Each test carries its `@scenario:<id>` tag and is self-contained: fixtures come from the existing fake-model markers, Mongo seed helpers, or page-scoped route stubs, and every seeded conversation is deleted again. * 🏷️ fix: Name a Mermaid Blob After the Preview It Is A share with snapshots disabled strips the download route from a code-execution `.mmd` but keeps the cached `attachment.text`, and that text is what `extractUtf8` truncated at 512 KB. With no route to the stored file the panel can only serialize that cached copy, and the mermaid exception in `getArtifactDownloadFilename` handed those partial bytes over as `flow.mmd` — the stored file's own name, from a menu item labelled "Download source". Mermaid now follows every other file-backed artifact: a serialized blob is named `flow.preview.mmd`, and only the original-file route produces `flow.mmd`. A model-authored diagram has no stored file to be a preview of, so it keeps its own name. * 🎹 test: Reach the Export Menu's First Item From Either Start `supports keyboard export` opened the menu with Enter and pressed ArrowDown once, assuming no item was active yet. CI observed the opposite — the first item already active — so the single ArrowDown walked past SVG onto PNG and the test failed there while passing locally (it failed the same way on the previous head). `Home` lands on the first item from either starting point. * 🔧 test: Fix the Fixtures Three Artifact Scenarios Ran On The first verification run failed five of the seven scenarios on fixture defects, not on product behavior: - `artifact-trigger-rows` drove a fake-model marker (`E2E_ARTIFACT_SHOWCASE_REPLY`) that does not exist, so the reply carried no artifacts and no trigger rows. Both tests now seed the assistant message text through Mongo, the way the passing plain-text scenario does. - `mermaid-source-download` waited for an `ArtifactRow` that a code-execution `.mmd` never renders — it renders an inline diagram whose trigger is "Open as artifact" — and its cached diagram embedded the literal truncation marker, which mermaid cannot parse, so the diagram failed to render at all. The cached copy is now a valid shorter diagram and the stored file keeps the sentinel that proves which bytes were saved. - `pending-artifact-row-slot` gave the tool call's id to only one of its two attachments. Attachments reach a renderer through `mapAttachments` keyed by tool-call id, so the other one rendered nowhere; both now carry the same id and land in the one `artifact-row-group` whose ordering the scenario is about. * 🪜 test: Count Four Authored Rows, Then the Mermaid One\n\nA mermaid fence is not a trigger row while it is inline: it renders the diagram\nwith an "Open as artifact" button and only becomes an ArtifactRow once opened.\nThe axis scenario now asserts the four authored artifacts stack as rows, then\nopens the diagram and asserts its row joins the same column. * 📐 test: Measure the Row Axis in One Layout State\n\nOpening an artifact narrows the chat column, so comparing the mermaid row's x\nagainst a box measured before the panel opened reported an 8px offset that no\nuser sees. All five rows are now measured after the open, and the panel is\nlocated at page level, where it renders \u2014 not inside the message list. * 🧩 fix: Give the Mermaid Row Its Own Filename and Download A code-execution `.mmd` arrives wrapped in a header that prints the filename and a download button, and that header stayed put when the diagram collapsed into its trigger row — so the chat showed `flow.mmd` twice and left the download outside the row that every other file-backed artifact keeps it in. `Mermaid` now takes the file's download action and hands it to `ArtifactRow`, and reports whether it is currently rendered as that row; `ToolMermaidArtifact` drops its header while the row owns both. Opening and closing the diagram therefore moves the filename and the download between exactly one place each. * 🔖 test: Match a File-Backed Diagram's Own Trigger Id\n\nThe row ownership scenario looked for a \ trigger id, which\nonly a model-authored fence gets; a code-execution diagram's row carries the\nattachment's artifact id. * 📱 test: Dismiss the Artifact Sheet Before Using the Row\n\nOn a phone the artifact panel opens as a sheet over the message list, so the\nrow's download button sits behind it. The scenario now closes the panel with its\nown control first, which also shows the trigger stays a row after closing. * 🔔 fix: Raise One Toast per Failed Source Download\n\n\ already toasts "Error downloading file" before reporting a\nfailed delivery, and the export menu then raised its own — two notifications for\none press, on every ordinary 4xx/5xx, because React Query's refetch resolves\nwith an error result rather than throwing.\n\nA reported failure now only updates the menu's live region, which keeps the\nexport's own phrase for a screen reader; a thrown task still raises the toast,\nsince nothing else has spoken for it. * 🔕 fix: Let the Download Layer Own Every Failure It Reports The previous round split the failure notification by cause and left a hole in the other direction: `useAttachmentLink` toasted only its empty-response branch, so a thrown fetch — and the artifact hook's own blob failure — returned `false` with nobody having spoken, and the export menu's live-region-only announcement reached no sighted user. One rule instead of two branches: whichever layer returns `false` has already told the user. `useAttachmentLink` toasts its thrown branch as well, and `useArtifactDownload` toasts when the blob path throws or there is nothing to serialize. The mermaid export menu keeps announcing the export's own phrase in its live region, so one press still raises exactly one toast — this time for every failure, not just the polite ones. * 🪄 fix: Hold the Row Slot and the Frame Before Blocking Work Two fixes on the same theme — a row or a control that moves at exactly the moment it is meant to stay put. Writing salience-sorted resolved entries back into their arrival slots still moved a pending row once it resolved: the placeholder joined the sort and could pass an empty artifact that had been sitting above it. `attachmentSalience` reads only `bytes`, which resolution does not change, so one sort over the whole panel row — pending entries included — is stable across the transition and is also less code. The export deferral used `setTimeout(…, 0)`, which only moves the blocking SVG work to another task; the timer can still run before the browser paints the loading row, which is the whole point of deferring it. It now waits for an animation frame and then a task, falling back to a timer when the tab is hidden and serves no frames. * 🏷️ fix: Keep the Recovered Filename on the Mermaid Row\n\n\ turns a sandbox-generated \ back into\n\, but that name lived only in the wrapper header. The moment the\ndiagram collapsed into its trigger row, the row title and its download button's\naccessible name fell back to \ — the internal filename.\n\n\ now takes the caller's display name for the row, and\n\ passes the one it already computed. * 🗂️ fix: Open Each Artifact on the Tab Its Row Promises A code-only artifact forces the panel onto Code, and that constraint was written back into the panel's single `activeTab`. The next HTML, Markdown, plain-text, React or diagram artifact then opened on Code while its trigger row announced "Opens as a rendered preview" — the row was telling the truth about the artifact and a lie about the click. The constrained tab no longer leaks: the panel resets to the preview tab when a different, unconstrained artifact is opened, so a manual Code choice lasts as long as that artifact stays open and never follows the user to the next one. `displayedTab` already derived the forced tab for rendering, so nothing else depended on the write-back.
* fix: Reject aliased tool approval identities * fix: Keep approval validation in TypeScript * style: Format approval validation
* fix: preserve agent management upload IDs with E2E coverage * test: clean up agent management upload fixtures
docs.librechat.ai/install/configuration/ai_setup.html 301-redirects to the generic docs homepage; the page was renamed to www.librechat.ai/docs/configuration/pre_configured_ai.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )