[pull] main from danny-avila:main - #271
Merged
Merged
Conversation
Co-authored-by: aeyeopsdev <275853971+aeyeopsdev@users.noreply.github.com>
) * feat: expose skill discovery and authoring through management API * fix: declare public skill management schema and handler types * fix: preserve skill permissions and shared management error semantics * fix: validate skill file paths before writing storage * test: isolate execution limiter routes from skills management * fix: align skills discovery access and file upload protections * fix: authorize skills operations before sync and upload admission * perf: overlap independent skills authorization reads * style: sort skills management imports
* test: Add Native BYOM Acceptance Harness * test: Keep Acceptance Startup and Mongo Environment Isolated * test: Drain In-Flight BYOM Acceptance Resources on Shutdown * test: Tolerate Transient Process Probe Denial During Teardown
* 🪪 feat: Configurable Langfuse Trace User Identity and Metadata * 🪪 fix: Prefer createRun's conversationId and warn once on an unset userIdField * 🪪 fix: Validate Langfuse trace field names at the point of use * 🧹 chore: Sort identity imports * 🪪 fix: Source the trace model label from the initializer, not a prompt-affecting option * 🧪 fix: Build the trace context without a prototype so partial client contexts resume * 📦 chore: Bump `@librechat/agents` to v3.8.0 for the Langfuse trace userId override
…15669) * fix: dismiss menus when composer focus changes * 🎯 fix: Keep Composer Focus Off Clicked Controls So Menus Can Close Ariakit records document.activeElement at open time as a menu's disclosure. The composer surface focused the textarea on every bubbled click, including the click that opened the Tools or attach menu, so the textarea became the disclosure and the menu ignored every later textarea interaction. The Tools menu went from modal to non-modal in #14979 (v0.8.8-rc2), which removed the backdrop that had been closing it anyway. Hoists the interactive-target selector, adds label to it, documents the mechanism at the guard, and gives the composer surface a stable test id so the empty-space focus test no longer depends on a utility class. Adds a test that opens a menu and proves a textarea click closes it. Closes #15624 * 🎯 fix: Restore Textarea Focus After Send, Steer and Stop Controls The interactive-target guard also skipped the bubbled click that used to return focus to the textarea after a mouse click on send. The send button is then disabled or swapped for the stop control, leaving focus on body. Route that refocus through a shared helper called from the form submit, the during-run consume callbacks, and the stop button, keeping the touchscreen exception. Adds a test that a mouse click on send leaves the textarea focused; it fails without the submit refocus. * 🎯 refactor: Exempt Only Focus-Owning Targets From the Composer Refocus The blanket 'button' exemption inverted the surface's long-standing behavior for every control, so each control that relied on the bubbled refocus (send, stop, steer, badge toggles) became its own regression. State the rule the other way round: the surface refocuses the textarea after any click except on a target that owns focus itself (links, form fields, labels) or opens or belongs to a popup (aria-haspopup disclosures and menu/listbox/dialog content, which React bubbles through portals). Matches that contain the surface itself are ignored so a host dialog can never disable the refocus. Drops the explicit refocus calls, which plain buttons no longer need. * 🎯 fix: Restore Textarea Focus From Popup Actions That Consume the Composer The during-run alternate actions live in an Ariakit hovercard, which is portaled dialog content and therefore exempt from the surface's bubbled refocus. Choosing Steer or Queue there consumed the text and unmounted both the button and the hovercard, leaving focus on body. Actions that consume the composer from inside a popup now restore focus themselves through a shared consume callback. Adds a ChatForm test that opens the real hovercard with screen-coordinate mouse travel, chooses Queue, and asserts the textarea is focused; it fails without the refocus. * 🧪 test: Expect Escape to Return Focus to the Quote Pill The quotes e2e asserted that Escape on the selections popover focused the textarea. That held only through the bug this branch fixes: Enter on the pill fired a click that bubbled to the composer surface, the textarea took focus mid-open and was recorded as the popover's disclosure, and Ariakit then 'restored' focus to it on hide. With the surface no longer stealing focus from a popup disclosure, the pill is the disclosure and Escape returns focus to it, as PendingQuoteChips documents. The guard against focus landing on body is unchanged. * 🎯 fix: Restore Focus When Removing a Quote From the Selections Popup The remove buttons in the selections popup are popup content, so the surface no longer refocuses the textarea for them, and the clicked button unmounts with its row. Removing the second-to-last quote also unmounts the popup and its pill, so Ariakit has nothing to restore focus to and it fell to body. The chip now restores focus itself: to the textarea when the popup collapses, otherwise to the popup so keyboard users stay inside it. Adds tests for both, plus one proving the primary during-run submit still refocuses through the surface (the hovercard anchor carries no popup attributes, so it bubbles like any button). * ♿ fix: Keep Quote Removal Focus Guarded and on a Visible Control Route the chip's collapse refocus through the composer's guarded helper so a tap on a touchscreen does not raise the keyboard, and after removing one of several quotes focus the remove button now at the same row (or the last one) once React has re-rendered the list, instead of the outline-less popup container. Tests pin both; each fails without its fix. * test: make quote popup focus checks deterministic --------- Co-authored-by: Jackson Riding <99007683+jacksonriding@users.noreply.github.com>
* fix: surface Bedrock content-filtered responses * refactor: extract model refusal metadata * test: preserve refusal helper in agent callback mock --------- Co-authored-by: aeyeopsdev <275853971+aeyeopsdev@users.noreply.github.com> Co-authored-by: Danny Avila <danny@librechat.ai>
* 🔒 fix: Assert the Tenant When Saving a Persisted Document `doc.save()` on an already-persisted document issues `update` filtered on `_id` alone. The tenant-isolation plugin stamps `tenantId` onto the payload but never asserts it in the predicate, so the write is scoped only by provenance — safe in the normal flow, since you can only fetch a document your tenant can see, but unguarded when the `_id` comes from an unscoped source: `runAsSystem`, a cached id, or a client-supplied id. Found by the driver probe added in the previous commit, which is also what proves the fix: its characterization test flips from asserting the predicate is absent to asserting it is present. `$where` is Mongoose's documented public hook for this — "additional properties to attach to the query when calling `save()` and `isNew` is false" — and its own sharding plugin uses it to attach a shard key the same way. It is absent from the TypeScript types, hence the cast. ## Why this cannot break existing writes - `isNew` documents return early; all three `transaction.ts` saves and the `pluginAuth.ts` save construct their document, so they are untouched. - A document that never carried a `tenantId` yields no predicate, so pre-tenancy rows and the global `Role` collection still save. - With no tenant context — every single-tenant deployment — the scope is unscoped and no predicate is ever added. - The remaining two call sites fetch and save inside the same request context, so the carried tenant matches by construction. `session.ts` reaches `save()` via `createSession`, which passes an `isNew` document. - A mismatch fails loudly with `DocumentNotFoundError` rather than silently no-op'ing, and optimistic concurrency, array writes and subdocument writes were each verified to still work alongside it. 80 suites / 2793 tests green. * 🧭 fix: Reset Tenant Save Predicates Across Scopes * 🛡️ fix: Close Persisted Tenant Save Bypasses
Co-authored-by: aeyeopsdev <275853971+aeyeopsdev@users.noreply.github.com>
* feat: harden early buffer recovery observability * fix: fence recovery to emitted frontier * fix: close recovery observability races * fix: require atomic recovery capabilities * fix: fail incomplete recovery attachments closed * fix: preserve legacy store contract * fix: correlate failed overflow persistence * fix: break recovery type dependency cycle * fix: reconcile distributed recovery lifecycle * fix: preserve content snapshot contract * fix: harden cross-replica recovery lifecycle * fix: lease recovery subscriber state * fix: close recovery lifecycle edge cases * fix: close recovery validation races * test: align subscriber lease cleanup * fix: harden subscriber lease lifecycle * fix: finalize subscriber lease handoffs * fix: reconcile recovery error replacements * fix: preserve durable recovery frontier * test: expect durable frontier gaps * fix: scope durable snapshot protocol * fix: trust durable terminal recovery payloads * fix: close overflow recovery rollout races * test: bound overflow race fixture memory * fix: bound overflow marker refresh * test: reduce recovery fixture memory * fix: fence overflow recovery admission * fix: close overflow recovery admission races * fix: scope overflow admission fencing * fix: preserve owner replay after remote admission * test: await pre-admission append fencing * test: reflect pre-admission durability
* 🛡️ fix: Harden Conversation Imports and Upload Handling * 🩹 fix: Address Import Hardening Review Findings * 🩹 fix: Map Clone Size Validation Errors * 🩹 fix: Chunk Import Cleanup Queries
* feat: Define Conversation Code Approval Constraints * style: Sort Code Approval Test Imports * fix: Enforce Explicit Code Environment Policy
`@lhci/cli@0.15.1` pins `lighthouse` to an exact `12.6.1`, so the Lighthouse
lane could never move off a release whose `puppeteer-core` still depends on
`extract-zip` — an advisory with no patched version. That pin, not Lighthouse
itself, was holding 8 of the repository's 16 `npm audit` findings open.
LHCI was only ever a subprocess wrapper here: `audit.ts` shelled out to
`@lhci/cli/src/cli.js` for `collect` and `assert`, and LHCI's own node runner
shells out to `lighthouse/cli/index.js` in turn. Call that CLI directly, run
the three navigations in a loop, and assert the median budgets in TypeScript
next to the assertions `load.spec.ts` already makes.
- `npm audit`: 16 findings (7 high, 1 moderate, 8 low) -> 6 low. The remainder
is the pre-existing `elliptic` chain under `vite-plugin-node-polyfills`,
which has no fixed version and is unrelated to this lane.
- Lighthouse 13 removed `largest-contentful-paint-element` and moved the LCP
node into `lcp-breakdown-insight`. Reading it through a `lcpElement` helper
keeps the "the transcript must be the LCP element" guard working and makes
the next rename fail loudly in one place.
- Reports move to `.lighthouse/` as `lhr-N.report.{json,html}`; cookie
redaction, the API timing table and the desktop/provided-throttling settings
are unchanged. Budgets are now printed as a table immediately before they are
asserted, so the workflow's 80-line failure comment always contains them.
- `CHROME_PATH` and `LIGHTHOUSE_CHROME_FLAGS` are documented: chrome-launcher
prefers the Windows Chrome under WSL, whose debugging port Linux cannot
reach.
* 🎯 fix: Land a Steer Applied Before the First Run Step on the Live Placeholder
An interrupt (steer + preempt) sent before the model has produced a token
is applied server-side at content index 0 and stamped with the response
id the server pre-allocated at job creation. The pane, however, still
renders the in-flight response under the `${userMessageId}_` placeholder
until the FIRST run step renames it, so the exact-id lookup missed, the
bounded next-frame retry ran out silently, and the interrupt vanished
from the live view until a refresh — while the spinner kept going.
`findResponseMessageIndex` now accepts the pane's OWN placeholder
identities as an explicit fallback (never a positional guess, per the
regenerate rule) and both the steer and activity-label resolvers
delegate to it. `useResumableSSE` passes the submission's placeholder id
and the padded user-message id, read at call time so the `created`
reassignment is honored. The rename copies placeholder content forward,
so the part rides into the renamed row.
* 🧭 fix: Carry an Early Steer Through the Rename for Regenerates and Edited Resubmissions
A regenerate seeds the renamed response from `submission.initialResponse`
rather than the store tail, and an edited resubmission seeds from that
object's content, so a steer landed on the placeholder only in the store
was dropped by the first run step's rename. `syncSubmissionPlaceholder`
keeps the submission the step handler receives in step with the
placeholder this pane mutated.
The steer also claims the server-local index space that run steps and
labels already shift past the retained edit prefix; it now shifts the
same way through one shared `editPrefixLength()` instead of landing
inside the kept content. Codex round 1 (P2) on #15692.
* 🔤 fix: Decode percent-encoded S3 keys so non-ASCII filenames are readable extractKeyFromS3Url returns `URL.pathname` as the S3 object key without decoding it. The AWS SDK percent-encodes the Key again when it signs the request, so a file stored under `Ársreikningur.pdf` is fetched as `%C3%81rsreikningur.pdf` and every read fails with NoSuchKey. ASCII keys are byte-identical either way, which is why this only appears for non-English filenames. Decode keys derived from a URL path in all three branches (path-style endpoint, bucket-in-path, virtual-hosted). A key passed in raw — not a URL — still returns untouched, and a malformed escape sequence falls back to the raw value with a warning rather than throwing. * 🔗 fix: Percent-encode CloudFront URL keys so both URL forms agree buildCloudFrontUrl interpolated the raw S3 key into the URL while SDK-generated S3 URLs carry an encoded one, so the two forms of `file.filepath` disagreed about what a `%` means. `assertS3FileName` permits `%`, so a key containing the literal text `report%20final.pdf` produced a CloudFront URL indistinguishable from one for a key containing a space — and decoding on extraction would then target the wrong object on read, re-sign, and delete. Encoding each path segment here (separators stay literal) makes both producers consistent, which is what lets extractKeyFromS3Url decode unconditionally. * style: sort imports in cloudfront/crud.ts (pre-existing drift) The changed-file import-sort gate flags this file; the drift predates this PR (the untouched upstream version fails the same check). Kept as its own commit so it does not obscure the fix. * 🔏 fix: Encode the CloudFront Invalidation Path Like the Viewer URL `buildCloudFrontUrl` now percent-encodes each key segment, so the cached viewer path for a key with a literal `%` or a non-ASCII character is the encoded form. `deleteFileFromCloudFront` still handed the raw key to `CreateInvalidationCommand`, so the invalidation no longer matched the cached object and deleted content stayed served until the entry expired. Both producers now share one `encodeKeyPath` helper. Also asserts that `getS3FileStream` sends the decoded key to `GetObjectCommand`, which is the call that actually failed with `NoSuchKey`, rather than only checking the extractor. Co-authored-by: dinershtein <228485+dinershtein@users.noreply.github.com> --------- Co-authored-by: Danny Avila <danny@librechat.ai> Co-authored-by: dinershtein <228485+dinershtein@users.noreply.github.com>
#15686) The DocumentDB compatibility guard flagged Mongoose per-document save-condition bag reads and writes (document.$where) in tenantIsolation.ts as the unsupported $where operator, leaving dev red on Tests: data-schemas. The guard now judges a dotted $where only where the syntax is unambiguous: a call in any form, or code assigned through any operator or wrapper, is an offense; a read or a non-literal assignment is not claimed, with filter.$where = predicate stated as the one declared limit and the method sweep and live cluster run as its backstop. Every other way of writing the operator remains an offense. unwrapExpression also peels angle-bracket assertions, closing a gap in pipeline-update detection.
#15695) S3 and CloudFront records have carried `storageKey` since #12987, yet six readers still handed `file.filepath` to `getDownloadStream` and re-derived the key by parsing a presigned or CDN URL, while four others had grown their own `storageKey || filepath` expression. One resolver now serves all ten: `resolveDownloadPath` returns the recorded key when present and the path otherwise, so records without a key (local, Firebase, Azure, code output) behave exactly as before, and the share route keeps its local-only query-string strip on top. `resolveStoredS3Key` reuses the same `StoredFileRef` type. Covered by unit cases for the resolver and an S3 case proving a record with a key streams correctly even when its stored URL no longer parses. Closes #15693
* fix: enforce FILE_SEARCH role permission server-side
The FILE_SEARCH role permission is stored, served by GET /api/roles/:name and
settable through the admin API, but nothing ever checks it.
PermissionTypes.FILE_SEARCH occurs zero times under api/server; the three
occurrences in @librechat/api are all in the interface-to-role sync, which
writes the permission rather than checking it.
Measured on v0.8.8-rc1 and confirmed unchanged in rc2: a user whose role has
FILE_SEARCH.USE = false can still upload a document with
tool_resource=file_search and gets 200 with "embedded": true. The same user
calling execute_code correctly gets 403.
Two gates, both mirroring toolAccessPermType in ~/server/controllers/tools.js:
* Upload (api/server/routes/files/files.js): a tool-resource-to-permission map
checked before any branching, so it also covers the assistants path. Returns
403 with the same log line as the RUN_CODE gate. execute_code is included
because it had the same hole: the tool CALL was gated, the upload was not.
* Tool loading (api/app/clients/tools/util/handleTools.js): checked outside the
lazy loader so a denied user never gets the tool equipped, rather than one
that fails when called. Fails closed if the permission check itself throws.
All four required imports were already present in that file, next to an
existing FILE_CITATIONS check.
Both changes live in /api (JavaScript) rather than /packages/api (TypeScript)
as CLAUDE.md prefers for new backend code: each mirrors an existing pattern in
exactly these files, and a second permission gate in another language and
package would be harder to keep in step with the first.
Tests: api/app/clients/tools/util/handleTools.fileSearchPermission.test.js
covers permit, deny, the log line, and the fail-closed path.
* fixup! fix: enforce FILE_SEARCH role permission server-side
* 🔐 fix: Gate `file_search` and `execute_code` on Role Permissions
The `FILE_SEARCH` gate added in the previous commit lives in
`handleTools.loadTools`, which `loadAgentTools` only reaches when
`definitionsOnly` is false. That default is false on the chat path
(`Endpoints/agents/initialize.js`) but true on the responses and
OpenAI-compatible controllers, so on those paths a denied user still got
the tool definition advertised to the model and their files primed by
`primeSearchFiles`, and the call failed later in `loadToolsForExecution` —
the "gets one that fails when called" outcome the gate set out to avoid.
Move the decision to the capability filters that both loaders already run,
mirroring how `canUseMCP` resolves the `MCP_SERVERS` permission ahead of the
synchronous filter. `AgentCapabilities` stays the instance-wide deployment
switch; the role grant is a second condition a tool has to clear. Because
`hasFileSearch`/`hasExecuteCode` derive from the filtered list, priming is
skipped for a denied user too.
`RUN_CODE` had the same hole: `toolAccessPermType` in
`~/server/controllers/tools.js` gates only `POST /tools/:toolId/call`, not
the agent run, so a role without `RUN_CODE.USE` could still execute code
through an agent. Both tools go through the same map.
Checks use `checkAccessWithRequestCache` and run only for tools whose
capability is already enabled, so a run costs at most one role read. A check
that throws denies the tool.
Also formats the test file added in the previous commit, which Prettier
rejects as-is.
* 🔐 fix: Close the Remaining `RUN_CODE` and `FILE_SEARCH` Bypasses
Codex review of the previous commit found four ways past the gates. All four
share a cause: the permission map was copied per call site, so every boundary
that was not copied into stayed open.
Moves the maps and the check into `packages/api/src/tools/rolePermissions.ts`,
per CLAUDE.md ("all new backend code must be TypeScript in `/packages/api`",
"keep `/api` changes to the absolute minimum"), and points the four `/api`
boundaries at it.
- `handleTools.loadTools` now gates every tool in `toolRolePermissions`, not
just `file_search`. It is the shared boundary the Assistants required-action
flow crosses via `processRequiredActions`, which never passes the agent
capability filter — so a legacy Assistant with a raw function named
`execute_code` ran arbitrary code for a role denied `RUN_CODE`.
- The check is now request-cached (`checkAccessWithRequestCache`). The previous
`checkAccess` call omitted `req`, so the agent path resolved the same grant
twice and issued a second serial `getRoleByName` during chat startup, against
the rule in AGENTS.md.
- `codeExecutionEnabled` is gated by the role result at all three loaders, not
only the `execute_code` entry in the filtered list. It drives tool
classification and the programmatic bash tool, so an agent pairing
`execute_code` with an MCP `code_execution` tool still advertised and
instantiated `run_tools_with_bash` for a denied role.
- The upload map covers `EToolResources.code_interpreter`, which the Assistants
builder posts instead of `execute_code`, and native `code_interpreter` /
`file_search` tools are dropped when an assistant is created or updated.
Those run inside the provider and reach neither loader, so configuration time
is the only place they can be gated.
Renames `handleTools.fileSearchPermission.test.js` to
`handleTools.rolePermissions.test.js` — it now covers both tools.
* 🔐 fix: Gate the Image Upload Route and the v1 Assistant Writers
Second Codex round found the same shape again: boundaries the map had not been
wired into. Adds two shared helpers next to the maps in
`packages/api/src/tools/rolePermissions.ts` so a new upload handler or assistant
writer gets the check by calling one function.
- `/files/images` accepts `tool_resource` and routes agent uploads to
`processAgentFileUpload` on its own. The Code Files UI sends images there, so
every image was a way around the upload boundary for a role denied
`RUN_CODE`. Both upload routes now call
`checkToolResourceUploadPermission`.
- That helper passes `req`, so the role read joins the request cache. The
previous `checkAccess` call omitted it, and an allowed Assistants upload goes
on to `processFileUpload` → `addResourceFileId` → `updateAssistant`, whose own
check does pass `req` — two serial lookups per upload, against AGENTS.md.
- `/assistants/v1` is still mounted and its `createAssistant` / `patchAssistant`
write native tools straight to the provider. Both now filter through
`resolveAssistantToolPermissions`, which v2 also uses in place of its local
copy.
Run-time enforcement for native assistant tools is deliberately not here: it
means overriding `body.tools` at `createRun` in both chat controllers, which
changes run semantics, and is better argued on its own.
* 🔐 fix: Gate Legacy `retrieval` and the Code-Environment File Tools
Third Codex round, same shape twice more.
- The v1 assistant builder submits `{ type: 'retrieval' }` where v2 submits
`file_search` (`AssistantPanel.tsx:203`). Only the v2 spelling was mapped, so
`resolveAssistantToolPermissions` treated legacy retrieval as ungated and both
v1 writers preserved it for a role denied `FILE_SEARCH`. Both spellings now
answer to the same grant.
- `codeEnvAvailable` in the agent initializer was capability-only, and
`initializeAgent` rebuilds `bash_tool`, `read_file` and the workspace file
tools from it — after the tool loader has already dropped `execute_code` for a
denied role. The bash gate stops command execution, but those handlers still
read, search, create and edit files in an attached code environment. The flag
now carries the role grant, and short-circuits before the role read when the
deployment has the capability off.
* 🔐 fix: Resolve Tool Grants Once Per Request and Pair Every Capability Gate
Four review rounds found thirteen issues, all one class: a boundary the
permission check had not been copied into. Rounds 2 and 3 each found gaps
created by the previous round's fix, and a manual sweep of tool constructors
and upload routes still missed the `codeEnvAvailable` family entirely — because
the thing worth enumerating is not "paths that reach a tool" but "reads of the
capability".
There are 28 such reads. This replaces per-site permission checks with one
resolution per request, pairs every read that is a gate, and pins the list so a
new one cannot be added silently.
- `resolveToolRoleGrants` resolves `RUN_CODE` and `FILE_SEARCH` together and
memoizes on the request. Both perf findings disappear by construction: gates
share one role read instead of each issuing its own.
- Newly paired gates: `codeEnvAvailable` in the OpenAI-compatible, Responses and
memory-agent initializers (`initializeAgent` rebuilds `bash_tool`, `read_file`
and the workspace file tools from it, and hands their handlers the code
environment); the capability checks in agent upload processing and in
agent-management upload purposes; and the library chat-completion service,
behind an optional `getRoleByName` dependency so embedders are not broken.
- The v1 Knowledge upload posts `assistant_id` with no `tool_resource`, so there
is no resource to authorize. It now reads the assistant's own native tools and
requires their grants, instead of treating a missing resource as unrestricted.
- The startup role read moved off the critical path into the existing
`Promise.all`, and the direct tool-call check passes `req` so the shared
loader gate reuses its result rather than repeating the lookup.
- `toolCapabilityGates.spec.js` pins every file that reads either capability
with a count and a reason. A new read fails the suite until it is classified,
so the next boundary announces itself instead of waiting for a reviewer.
* 🔐 fix: Abort Assistant Writes on Role-Lookup Failure, Authorize Legacy Uploads First
Round 5 found no missed boundary — the inventory held. All three findings are
about how the gates behave.
- Failing closed is right where a denial blocks the operation, and wrong where a
denial instead filters a payload the caller persists. A transient role-store
outage during an assistant create or patch was silently stripping
`code_interpreter` / `file_search` / `retrieval` and saving the result, turning
a momentary failure into permanent config loss. `checkToolRolePermission`
gains `throwOnError`, and `resolveAssistantToolPermissions` uses it: every one
of its callers either persists the filtered list or rejects the request, so a
lookup failure has to propagate.
- The legacy-assistant upload gate ran after `sanitizedUploadFn` had already
pushed the file to the provider, so a denied role left an untracked remote file
and got a 500 for what is a 403. Moved to the route, next to the tool-resource
gate, before any bytes are sent.
- `resendFiles` hydration derived its resource set from unfiltered `agent.tools`,
so a denied role still paid the thread walk and code-file reads for a tool
about to be dropped. The `execute_code` half now keys off
`effectiveCodeEnvAvailable`, which already carries the grant.
The `file_search` half of that last one needs the grant threaded into
`initializeAgent`, which is plumbing in the most delicate file here for a latency
win on an already-correct path — deliberately left for a follow-up.
* 🧪 fix: Declare `resolveToolRoleGrants` in the Upload Spec's Module Stub
`Tests: api (shard 2/3)` failed on `a0b38c3`: `process.spec.js` stubs
`@librechat/api` wholesale, so the `resolveToolRoleGrants` call added to the
upload gate resolved to undefined and threw. Five failures in
`processAgentFileUpload`, plus one cascading `processFileURL` assertion from the
same suite.
The gate was verified through its own spec and the routes above it, but not
through the spec of the file it lives in — `process.spec.js` was never run
locally. Every spec belonging to a file this PR touches now has been, and the
mongodb-backed ones that still cannot run here were checked statically for the
same stub shape: all but `process.integration.spec.js` spread `requireActual`,
and that one never reaches these paths.
* ⚡ fix: Skip the Role Lookup When Code Execution Is Disabled
The browser initializer already guards this; the OpenAI-compatible and Responses
controllers did not, so every request on those endpoints issued a role read whose
result the capability short-circuit then discarded. Both now start the lookup
only when the deployment has `execute_code` enabled, matching the third.
* ⚡ fix: Drop Three Redundant Reads on the Gated Paths
- The memory initializer resolved grants unconditionally, so a deployment
without `execute_code` paid a role read for a flag that could only be false.
Guarded like the three chat initializers.
- Agent-management upload purposes resolved grants before `checkCapability`, so
a purpose the deployment has switched off paid a role read on the way to being
rejected. Capability first, then the grant.
- The legacy-assistant preflight builds an OpenAI client to read the assistant's
tools, and `processFileUpload` then built a second one — re-reading the user's
key expiry and values. The preflight now hands its client on, and processing
builds one only when it wasn't given one.
---------
Co-authored-by: Paul <200737214+SSIG-IT@users.noreply.github.com>
#15691 paired every read of `AgentCapabilities.execute_code` / `file_search` with its role grant, and skipped `execute_code` from the resend-file priming when `RUN_CODE` was denied. It left the `file_search` half: the grant was not available inside `initializeAgent`, so a role denied `FILE_SEARCH` still had its prior-turn search files re-hydrated on every resend. Priming is not free. The files are fetched, `updateFilesUsage` bumps their counters, and `primeResources` builds `tool_resources.file_search` — all for a tool the loader is about to drop. A deployment with the capability off paid the same cost. `InitializeAgentParams.fileSearchAvailable` carries the capability AND the grant, resolved from the same single `resolveToolRoleGrants` read that already answers `codeEnvAvailable`, so pairing the second flag costs no extra role lookup. Every initializer that resolves grants forwards it: the chat path and its handoff, added-convo and discovery hand-offs, both API routes, and the OpenAI-compatible embedder route. Absent leaves priming unconditional, so an embedder that resolves no grant keeps its current behavior. The loop that selected the resend tool resources becomes `resolveResendToolResources`, which makes both halves of the gate testable rather than reachable only through a full agent initialization. The memory agent is untouched: it passes no `conversationId`, so it never reaches this priming. Co-authored-by: Claude <noreply@anthropic.com>
* fix: Bound Code Environment Status Polling * refactor: Move Code Limiters Into API Package * fix: enforce status polling boundaries
* fix: Require Platform Grants for Code Workers * fix: Separate Platform Scope From Tenant Identity
* feat: Add conversation code approval modes * refactor: Reuse shared code approval types * fix: Scope approval selector to stateful agents * fix: harden code approval state propagation * fix: fail closed during approval discovery * test: cover approval mode acceptance * test: use visible approval mode label * test: drive approval menu by keyboard * test: expose accept edits in BYOM acceptance * fix: read approval mode atomically on send * fix: align code modes with endpoint policy * fix: initialize code limiters at startup
…ck Icon (#15706) * 🕓 style: Move the Queued-Message Hint Into a Hover Tooltip on the Clock Icon "Sends when this response finishes" rendered as a caption row under the queued chips for as long as a run was pending, costing composer height for a single reassurance. The clock icon of each queued row now carries that sentence as a hover tooltip and as its accessible name, and the caption row is gone. Nothing else about the row changes: warning states keep their triangle, and the hint disappears with the run as before. * ⌨️ fix: Give the Queued-Hint Clock an Explicit Tab Stop and Focus Ring The tooltip anchor was already focusable through Ariakit's Focusable default, and opens on focus-visible, but nothing said so and no ring showed where focus was. Make the tab stop explicit, add the same focus-visible ring the row's buttons use, and prove the keyboard path with a test. The hover test now allows for Ariakit's show timer on a loaded CI shard, which is what failed the first run. Codex round 1 (P2) on #15706. * 🧪 test: Move the Pointer Between Coordinates Before Expecting the Queued Hint Tooltip Ariakit's hover detector treats a pointer as moving only when consecutive mousemove events differ in screen coordinates; its NODE_ENV=test shortcut does not apply under the CI runner's NODE_ENV, which is why the hover test passed locally and failed in the shard. Two moves at distinct coordinates make the test environment-independent; the keyboard test was already deterministic.
* fix: Clean Up MCP OAuth Teardown State * fix: Harden OAuth Teardown Boundaries * fix: harden MCP OAuth teardown races * fix: avoid flow cache dependency cycle * fix: bind OAuth teardown to credential generations * test: cover active OAuth server fixtures * style: sort OAuth cleanup imports * fix: discard mismatched OAuth token snapshots * fix: fence OAuth teardown across callback races * test: align OAuth callback fixtures with server fencing * test: complete OAuth callback server fixtures * fix: version OAuth flow settlement * fix: fence OAuth teardown snapshots * test: align OAuth teardown fixtures * test: complete OAuth snapshot fixtures * fix: snapshot OAuth deletion lifecycle * test: bind OAuth revocation snapshots * test: assert per-record OAuth fence * fix: harden MCP OAuth teardown consistency * test: expect snapshotted OAuth readers
* fix: fence concurrent MCP OAuth teardown * fix: hold MCP refresh teardown fences * fix: suppress OAuth fallback during teardown * fix: retire MCP connections before OAuth cleanup * fix: annotate refresh teardown tenant scope * fix: fence OAuth teardown across replicas * test: provide OAuth teardown leases in controller mocks * fix: fail safely around OAuth teardown leases * fix: complete OAuth lease lifecycle handling * fix: preserve OAuth single-flight admission
* fix: recover direct OpenID MCP bearer sessions * fix: harden direct bearer recovery lifecycle * fix: close direct bearer lifecycle races * test: align direct bearer checks with current dev * fix: unify direct bearer recovery budget * fix: infer direct bearer recovery from placeholder * fix: preserve bearer precedence and recovery budget * fix: propagate bearer recovery across joiners * fix: preserve direct bearer fallback without session provider * fix: close direct bearer recovery race windows * chore: sort bearer recovery test imports * fix: align bearer recovery with OAuth cleanup lifecycle * fix: fence request-scoped MCP creation during config mutations * fix: retain suspended MCP catalog authentication outcomes * fix: cancel MCP bearer recovery and reuse refreshed credentials * fix: close MCP bearer recovery lifecycle boundaries * fix: preserve ordered catalog authentication rejection * fix: isolate bearer recovery across request and transport boundaries * fix: preserve shared recovery ownership and credential precedence * fix: wait for recovery borrowers and validated OpenID publication * fix: observe the validated browser publication flight * fix: cancel abandoned OpenID publication followers * fix: fence refresh grant admission after lease acquisition
* 🗜️ feat: Manual Context Compaction as a Summarize-Only Turn Adds a user-triggered "Compact context" action that summarizes the active branch on demand. The compaction is an ordinary agent turn: the client submits it through the normal chat pipeline with `compact: true`, the controller hangs it off the branch's leaf instead of creating a user message, and the agent client runs the graph summarize-only (the SDK's new `summarizeOnly` input), so the summary streams into the response placeholder under the leaf exactly like the automatic detour's and is persisted as the boundary every later turn starts from. Nothing new is needed for job registration, billing, abort, persistence, or the usage snapshot: the run emits the same summary step and post-summary context snapshot the automatic path already emits, and the response message carries the summary part plus the `summaryUsedTokens` baseline the gauge reads. The controller validates the request (existing conversation, a real leaf, summarization enabled, no edit/regenerate/continue combination) and answers 409 when the leaf is already a bare summary. Client-side the submission borrows the regenerate shape — no new user bubble, the placeholder parents onto the leaf — while the wire payload drops `isRegenerate` so the server never treats it as a regenerated user turn. The action sits in the context-usage popover, gated by the same `summarization.enabled` switch as the automatic detour. * 🗜️ fix: Localize a Compaction That Cannot Run A compaction the graph could not attempt (summarization disabled, the instructions already exceeding the budget, or an explicit recency window covering the whole conversation) rejects with the SDK's `ManualSummarizationSkippedError`, which rendered as the generic admin error. It now maps to a typed `compaction_skipped` payload with the reason, and a run that ends with neither a summary nor a recorded error fails as `compaction_failed`; the Error component localizes both. A run that already recorded why it stopped keeps that error part instead of failing twice. * 🗜️ fix: Anchor a Compaction on the Leaf Everywhere It Is Read Review round 1. - The client sent the leaf's parent as `parentMessageId`, which the controller treats as the anchor, so the summary would have skipped the latest reply and landed as its sibling. The leaf's own id is now both the placeholder's parent and the server-side anchor. - A compaction has no user message of its own, and every consumer of the job's user-message slot now knows it: the error path parents its row on the loaded anchor and never upserts the leaf, and the job records the regenerate shape so a reconnecting client rebuilds the turn under the leaf instead of recreating it as a user message. - The controller no longer reads the anchor before the branch is loaded; `BaseClient` validates it against the history it loads anyway and answers 404 for a missing leaf or a typed `nothing_to_summarize` for a finished compaction. That predicate is shared through the data provider and ignores a summary still streaming or one that failed, so an interrupted compaction can be retried. - A compaction never consumes files staged in the composer, the action is shown only when the server advertises `compactionEnabled`, and the in-flight marker is feature-local Jotai state rather than a read of the Recoil submission store. * 🗜️ fix: Keep a Compaction Turn to Its Summary Review round 2, plus the class it belongs to. A compaction submits no user turn, so nothing that keys off one may run: the memory pass no longer runs over the history (it would have spent a model call and could repeat memory writes), and skill primes are no longer injected into the transcript the run is about to summarize. The client's in-flight marker is also reconciled on mount: a compaction that finished while the view was away left the marker set, and the next ordinary turn in that conversation would have shown as compacting. * 🧹 chore: Sort Imports in the Compaction Hook Spec * 🔧 chore: Update @librechat/agents dependency to version 3.8.1 in package-lock.json and package.json files
* 🧐 feat: Review Pending Tool Actions in the Composer (#15714) * feat: add interactive approval review to the composer * fix: Harden interactive approval boundaries * fix: Bound every approval display label * test: exercise composer approval review in BYOM acceptance * fix: reveal all directional approval controls * fix: canonicalize code approval targets across replicas * style: sort resumable approval test imports * test: Exercise Coexisting Approval Surfaces * Revert "🧐 feat: Review Pending Tool Actions in the Composer (#15714)" This reverts commit 5f79ded. * Reapply "🧐 feat: Review Pending Tool Actions in the Composer (#15714)" This reverts commit 52c7d9c. * chore: bump agents sdk to v3.8.2
* 🔒 fix: Isolate Checkpoint Cleanup by Generation Ownership * 🔒 fix: Harden Checkpoint Cleanup Ownership * 🔒 fix: Retain Checkpoint Cleanup Evidence * 🔒 fix: Persist checkpoint cleanup receipts * 🔒 fix: Bound checkpoint cleanup receipt lifecycle * fix: Bind checkpoint cleanup to durable ownership and deletion intent * fix: Bind event actor checkpoint storage to authenticated owners * fix: Resolve exact actor checkpoint ownership before lifecycle cleanup * fix: Preserve actor storage compatibility with durable ownership and prune work * fix: Defer checkpoint erasure until conversation deletion commits * fix: Reclaim orphan actor ownership and batch deletion lookups * fix: bind actor checkpoint ownership to payload rows * fix: isolate actor readers and await account persistence * fix: gate owned suspensions and drain complete account state * fix: unify checkpoint deletion lifecycle and storage authority * fix: Replay checkpoint deletion from durable owner state * fix: retain checkpoint cleanup identity before topology loss * fix: complete checkpoint catalog capture and retirement lifecycle
Classify HTTP 408, 429, and 5xx before legacy body-message classifiers can invalidate client registration or start consent. Preserve typed temporary failures across refresh storage and verify retry with the existing grant against the real SDK test provider. Keep permanent grant errors and existing UI retry behavior intact. Co-authored-by: Lia <lia@librechat.ai>
…16120) * 💵 feat: Show Cost per Record, Step and Response in the Trace Ledger * 💵 fix: Cost for Whole Responses Only, Hidden Records Included, and Said Aloud * 💵 fix: Give the Oldest Loaded Response No Cost While Older Records Remain
* 🥊 test: Prove MCP Refresh Coordination Across Replica Processes Fork real replica processes with production Mongo, Redis, encryption and OAuth adapters, and assert one redemption per rotation, peer adoption, and recovery after the authorizing replica is killed. A negative control with coordination disabled observes two redemptions, so a green coordinated run cannot pass on timing alone. Adds a refreshGate hook to the shared OAuth test provider so concurrent refreshes can be held open. No production code changes. * test: Establish Replica Overlap Instead of Assuming It The coordinated assertion counted provider redemptions after a fixed delay, so a peer that arrived once the winner had already persisted would read a fresh credential, never contend, and still leave one redemption: the test would have passed while proving nothing. Both replicas now report entering the refresh path on a credential they observed expired, and getTokens own onRefreshSuccess/onTokensAdopted hooks report which side each took, so the pair (one redeemed, one adopted) is asserted rather than inferred from timing. A worker that never signalled ready was also left running with live Mongo and Redis connections, which would outlive the suite and disturb the rest of the integration lane; boot failures now reap the child and report the workers own initialization error. --------- Co-authored-by: Lia <lia@librechat.ai>
* 🧹 fix: Build the Legacy Meili Cleanup Index MongoDB Rejected
`meili_excluded_legacy_cleanup_v3` declared `_meiliCleanupVersion: { $exists: false }`
in its `partialFilterExpression`. MongoDB rewrites that to `$not`, which no partial
index accepts, so the build failed on every startup for Message and Conversation and
the index never existed.
The missing-version condition moves into the key, where a missing field indexes as
null and the legacy cleanup query reaches it on the same scan. The filter keeps the
two conditions a partial index can express, and the name moves to `_v4` so no
deployment that somehow holds the old spec hits an options conflict.
* 🎯 fix: Bound the Legacy Cleanup Index to Documents Awaiting Cleanup
The buildable filter admitted every excluded document carrying `_meiliIndex: false`,
which the schema writes on every new subagent message and conversation. Those
documents already carry the current `_meiliCleanupVersion`, so the cleanup query never
selects them and their entries never leave the index.
The unstamped state is now expressed as a null equality, which a partial filter
accepts and which matches an absent or null version. The legacy cleanup branch asks
the same way, so the planner can still reach the index, and a document leaves it as
soon as cleanup stamps the version.
…16037) * ⚓ fix: Keep a Resumed Compaction Anchored to the Turn It Summarizes A compaction submits no user turn: its user-message slot names the leaf it summarizes up to. The resume paths adopted that identity-only projection as a ROW, rewriting the answer being summarized into an empty, parentless user message, which buildTree files as a phantom root: the thread folded and the pane could return on another branch. Treat the slot as an anchor everywhere rows are written, and let the anchor name the branch to restore. * Recognize a compaction anchored on a user leaf Compact runs on whatever leaf the branch ends with and canCompact does not restrict its author, so the anchor is a user message as often as an answer. Detecting it by not-user-created recognized only the assistant-leaf kind: the other was rebuilt as an ordinary turn, so the sync path merged the identity-only projection over the stored row and blanked the prompt still on screen, and a failed abort appended a second row under its id. Judge the anchor by the projection shape instead, and carry the resolved answer on the resumed submission so a flagless re-attach agrees. --------- Co-authored-by: Lia <lia@librechat.ai>
* fix: bind preserved MCP API keys to server configuration * fix: surface MCP API key rebinding errors * test: use typed MCP OAuth re-entry error
* test: add deployed instance Playwright smoke profile * test: normalize deployed smoke account state * test: harden deployed smoke lifecycle * test: secure deployed smoke cleanup
* 🫧 feat: Hold a Live Run's Activity at One Row
Fold the span a run is still writing into a single collapsed row from its first tool call, with a header that bubbles up the newest activity (streamed intent, generic running text, filled batch label, or the thought streaming after them) on a 500ms leading+trailing throttle. The settled rendering is unchanged.
* 🫧 fix: Name Every Live Line and Take the Fold Preference From the Host
Live header resolves failed/cancelled through resolveToolCallPhase, names trailing commentary, and only folds agents-shaped calls it can name. ContentParts takes foldLiveActivity from its host instead of reading Recoil; subagent panels opt out.
* 🫧 fix: Carry Background Stops and Nested Sign-Ins Into the Live Fold
* 🫧 feat: Preview Tail Reasoning One Sentence at a Time in the Live Row
* 🫧 fix: Keep Handoffs, Detached Failures and Status Announcements Out of the Fold's Blind Spots
* 🫧 fix: Keep the Live Line's Shimmer Off the Ticker's Animated Element
Found in Chromium: .shimmer declares animation/position/display, so on the same element it replaced the slide-out and the retired line never left. The sweep now rides an inner span, and the header button is block-level flex so a live row measures the same 28px as the settled row.
* 🧹 chore: Sort Imports in the Live Activity Helper
* 🫧 refactor: Decide a Live Line's Outcome With the Group Header's Resolver
The live header derived pass/fail on its own and re-learned, one review at a time, signals the cards already handle. getToolMeta moves out of ToolCallGroup into Content/outcome.ts and the live line reads it, which covers cancelled status attachments and memory-tool prose failures. A legacy Assistants call now ends a live span like a handoff, and the live disclosure is named by its current line alone (aria-labelledby) so the polite region's previous line stays out of its accessible name.
* 🫧 fix: Make the Live Row Agree With the Cards It Hides
Span-level outcome beside the newest line (an earlier call can fail while a later one runs), announcements with their own identity in one polite region that outlives the live header, the memory-error artifact and step-scoped attachment ownership in the shared resolver, one localized 'Failed: {{0}}' template for the card and the row, and a parity suite that compares the folded row with the REAL cards on unmocked outcome logic.
* 🫧 fix: Keep Live Subagents Unfolded and Give Reused Ids and CJK Sentences Their Own Lines
* 🫧 fix: Hold a Finished CJK Sentence and Honor Open Thinking in Live Folds
* 🫧 fix: Say "Running in Background" as the Card Does and Hold Reasoning Identity Across Whitespace
* fix: Harden live activity folding boundaries and lifecycle
* 🫧 feat: Surface a Span's Own Glyphs on Its Collapsed Header
A header stands for rows it hides, so it carries the most specific glyphs they show. The icon stack swaps the generic search glyph for the sites a web search read (from attachments, or the streamed results while live), and the settled phase header keeps the span's tool, MCP and site icons instead of a bare check; a failed phase keeps its warning glyph. Applied to the live row, the settled phase header and the tool group header. Source helpers move out of WebSearch into Content/sources.ts.
* fix: Preserve owned glyphs and outcomes across collapsed headers
---------
Co-authored-by: Lia <lia@librechat.ai>
* ⚡ perf: Split Streaming Markdown Blocks Incrementally * test: cover streamed markdown block rendering * fix: isolate streamed markdown splitter caches * fix: satisfy markdown splitter static checks * test: mock isolated markdown splitter * test: isolate markdown splitter spies * test: cover concurrent streamed markdown messages * fix: preserve provisional streaming markdown boundaries --------- Co-authored-by: Lia <lia@librechat.ai>
…16070) * ⚡ perf: Exclude Agent Version History From Default `getAgent` Reads getAgent now defaults its projection to { versions: 0 }; loadAgent/loadAddedAgent and canAccessAgentFromBody use getAgentWithVersionCount so the version count stays exact without transferring the unbounded versions array on every chat request. Callers that need history (v1 update/revert handlers) request it explicitly. * fix: tighten agent version projection types * fix: type projected agent version counts * fix: type default agent projection * style: format agent projection overload * fix: expose projected agent return type
* ⚡ perf: Preserve Message References in the Content Handler * test: cover content handler message reconciliation * fix: preserve thread metadata for incomplete content events * fix: retain fallback response thread identity * fix: select parent from response thread * fix: retain untagged conversation messages * fix: preserve history while resolving parent * fix: refresh cached response metadata * fix: honor synchronized parent metadata * fix: scope parent fallback to response thread * fix: avoid cross-thread parent fallback * fix: keep content handler selection semantics
* ⚡ perf: Virtualize the Model Selector's Search Results * fix: make model search accessibility metadata global * test: stabilize model selector search scenarios * test: assert model selector keyboard semantics * test: simplify model selector keyboard assertion * test: exercise keyboard pin navigation * fix: preserve search result announcements * test: follow active row pin through keyboard * test: scope pin toggle assertion * test: scope pin toggle to active row * test: stabilize keyboard pin scenario * test: isolate model selector favorites * fix: preserve virtual search boundary navigation * test: cover virtual search boundaries * fix: own virtual boundary by logical position
Co-authored-by: Lia <lia@librechat.ai>
…6143) `useLazyHighlight` tokenized the same input twice on every mount that could highlight immediately. The state initializer highlighted during render whenever the grammars were already loaded, which holds for every card after the first in a session, and then the effect highlighted the same input again because its dedupe ref started empty and could not match on a fresh mount. Each card paid two tokenizations, two hast-to-React conversions and an extra commit. Card bodies also tokenized while collapsed. Every code, bash, read-file and file-authoring card highlighted its content on mount, so opening a long agent conversation highlighted code nobody had asked to see. The tokens now carry the key they were produced from, so the effect recognizes what the initializer already did, and each call site passes its content only while its pane is open. ExecuteCode gains the raw fallback its three siblings already had: with highlighting deferred to the pane opening, rendering only the highlighted nodes would leave the pane empty until the grammars load.
…d Thoughts (#16145) * 🫧 fix: Fold a Streaming Thought Into the Live Row Before Its First Tool Call A thought at the live tail rendered as its own row with a multi-line peek until a tool call arrived, which then snapped it into the one-row fold. With a reasoning model that talks between calls that is a jump up and back down on every step. The span now folds from the thought's first character and previews it one sentence at a time, under a reasoning glyph until a tool gives it icons. * 🫧 fix: Keep a Live Card's Key When Its First Tool Call Arrives * 🫧 fix: Stop Unfolding the Whole Live Span for Every Code Call Without a Leading Intent blocksLiveFold unfolded the entire span while a bash/code call with complete args, no output and no intent was running, so its card could show the sandbox-startup label. When the model writes intent after command, every code call qualifies: a 47-call run opened to its full height and shut again on each call. The row now reads the same sandbox signal for its newest call and the span stays one card. Also keeps tool-anchored card-key aliases across finalization so a reasoning-led card a reader opened stays open when the response settles. * 🧹 chore: Sort Imports in ActivityPhaseGroup * 🫧 fix: Preserve Reasoning Disclosures and Isolate Streaming Siblings * 🧪 test: Keep Real Part Identity in Content Renderer Mocks * 🫧 fix: Migrate Sandbox Startup Readers and Writer to Jotai --------- Co-authored-by: Lia <lia@librechat.ai>
`style.css` pinned every `code` and `pre` element to `Consolas, Söhne Mono, Monaco, Andale Mono, Ubuntu Mono, monospace !important`. The repository ships none of those faces, so Windows rendered code in Consolas and macOS in Monaco, which carries neither an italic nor a bold face for the browser to use. `!important` also outranked the 21 `pre` and `code` elements that ask for `font-mono` by class, so the self-hosted Roboto Mono the app already bundles was never used for code anywhere. Move the stack to `theme.fontFamily.mono`, where `sans` already lives, so Tailwind's preflight styles the bare elements and the `font-mono` utility carries the same value. The tail is ordered so the glyphs the bundled latin subset omits keep Roboto Mono's advance width. Co-authored-by: Lia <lia@librechat.ai>
* 🖼️ fix: Resolve Pinned Model Icons from Endpoint Config - pass `endpointsConfig` and `model` to `MinimalIcon` from the pinned favorites row so a custom endpoint's `iconURL` is honored Fixes #16088 * test: Cover Pinned Model Endpoint Config Propagation --------- Co-authored-by: Lia <lia@librechat.ai>
* fix(client): map image/svg+xml artifacts to a renderable template (#16087) * fix: Rebuild the SVG preview shell from edited source An SVG artifact previews a derived index.html holding a copy of the source, so replacing only the edited index.svg left the preview on the original drawing. Expose the derived-file builder as deriveFiles and rebuild the whole set from the editor text in SandboxArtifactTabs. * fix: Scope the SVG preview sizing rule to the root viewport CSS overrides SVG presentation attributes, so the unscoped `svg` rule in the preview shell stretched every nested `<svg>` viewport to the panel, corrupting the layout of sprites and inset diagrams. Scope it to `body > svg`. --------- Co-authored-by: Frank_zhu <58329837+Frank-zhu0404@users.noreply.github.com> Co-authored-by: Lia <lia@librechat.ai>
* 🌁 fix: Stop Painting the Closed Mobile Drawer * 🌁 fix: Re-assert the unpainted value when the slide settles React owns the resting value and writes it when the travel window closes at TRANSITION_MS; the release ran a buffer later and cleared the property, which dropped that value while React still believed its unchanged prop was applied. React never wrote it again, so every animated close left the drawer painted and the artifact could recur. Re-asserted through one shared constant, the same way the drawer width already is. --------- Co-authored-by: Lia <lia@librechat.ai>
* ✒️ fix: Persist a Generated Title Before the Turn Ends * 🧷 fix: Keep the Early Title Save From Rewriting Message References * 🩹 fix: Carry the Recovered User Message Reference * 🧵 fix: Reference Every Recovered Message Row * 🪢 fix: Reference Paused and Resumed Response Rows --------- Co-authored-by: Lia <lia@librechat.ai>
* fix: preserve tool pane choices through response finalization * test: read real message context in content part mock --------- Co-authored-by: Lia <lia@librechat.ai>
* ⚡ perf: Throttle Code Highlighting While a Message Streams * fix: harden streamed code highlight throttling * test: cover wall clock changes during highlighting * style: tidy highlight regression test * fix: restart highlight work across lifecycle changes * style: avoid nested highlight timing ternary * fix: propagate highlight throttle to startup config * fix: share and validate highlight throttle * fix: keep streamed code visible during highlighting * test: stream highlight tool arguments * fix: preserve highlights when cadence config resolves * fix: reschedule highlights when cadence changes * test: preserve default fake tool chunking * fix: prevent stale streamed highlights * test: open the tool pane before asserting streamed highlights Every scenario in this file waited for highlight tokens without opening the card. Dev's #16143 stopped passing a closed pane any code at all, so the four runs polled a card that renders raw text and had nothing to find: the failed traces show the disclosure at aria-expanded="false" for the whole run, with no click against it and no tokens in the code element. Highlighting is now asserted through the card's disclosure, which ProgressText owns and which carries an aria-expanded state only once the card has input to show, so waiting on it also waits for the first streamed argument chunk. The open happens inside expectHighlightedCode, so a scenario cannot assert tokens without it, and it lands while arguments are still streaming, which is the only window where the throttle is under test at all. * test: reopen the rebuilt card for the settled highlight Opening the pane got the streaming assertions passing and the traces show 240 highlight spans mid-stream, so the throttle itself lands. The settled check still failed because persisting the streamed message rebuilds the card, and a rebuilt card starts closed while autoExpandTools is off: the disclosure goes back to aria-expanded="false" and a closed pane renders raw text. The settled and cancelled states now assert through the same open-then-wait helper, so each one measures the card the reader would actually be looking at instead of the tokens the streaming card happened to leave behind. * test: leave a window to cancel a streaming highlight With the settled assertions fixed, the cancellation scenario timed out on the Stop button instead: it waited for a highlight before stopping, and the ~40 argument chunks it streams at 35 ms are gone in under two seconds, so the run had already finished and there was nothing left to cancel. It failed on all three attempts. The card is now opened and the run stopped before any token is awaited, since tokens after cancellation are what the scenario is about, and the cancel variant streams its arguments at a wider cadence so the window does not depend on how loaded the runner is. Only that label changes cadence; every other scenario keeps the timing it had. * fix: preserve initial highlights and honor shared-page cadence --------- Co-authored-by: Lia <lia@librechat.ai>
Co-authored-by: Lia <lia@librechat.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 : )