Commit graph

2210 commits

Author SHA1 Message Date
Marco Beretta
45d0d34c9e
chore: remove translation keys orphaned by the tool library redesign 2026-07-03 02:10:15 +02:00
Marco Beretta
a213b2e10f
feat: anchor the favorite star at the card's right edge
Swap the ToolCard action-bar order so the star sits rightmost with the
configure/info icon to its left. Every card can be favorited but only some
are configurable, so anchoring the star keeps it in a consistent position
across the grid.
2026-07-03 01:49:06 +02:00
Marco Beretta
d5cccb5f18
feat: add favorites for marketplace tools, MCP servers, and skills
Reintroduce the favorite star from the old skill picker, generalized to
every marketplace item kind except per-agent actions. Cards in the Tool
Library and Skills dialogs get a hover-revealed star (always visible once
favorited), and the existing Favorites views in both dialogs now filter to
starred items.

Favorites persist in a dedicated ToolFavorite collection, one document per
(user, itemType, itemId) with a unique compound index, exposed through
atomic per-item PUT/DELETE endpoints under /api/user/settings/favorites/
tools. Per-item writes are idempotent and race-free across tabs/devices
(the unique index backstops concurrent toggles), reads are a single
index-backed query capped at 100 favorites per user, and the client keeps
React Query as the source of truth with optimistic updates. Handlers live
in @librechat/api with a thin route wrapper; methods follow the
data-schemas factory pattern with tenant isolation.

The favorites filter now matches on compound kind:id keys instead of bare
ids, closing a cross-kind collision where a tool and a skill sharing an id
would both match. The skill-favorites data-service stubs and the reserved
TUserFavorite.skillId field are replaced by the new tool-favorites service.
2026-07-03 01:12:31 +02:00
Marco Beretta
f6eccee9c7
fix: scope tooltip elevation to dialogs and restore dialog close button size
Tooltips go back to z-150 globally; inside a dialog they now borrow the
depth-aware popover z-index so they still clear nested dialogs (the Tool
Library item dialog) without outranking freshly opened modals everywhere
else. The default dialog close icon returns to its original size, and the
lc-field pointer-focus suppression ships with the package next to Input and
Textarea so external consumers get the whole mechanism from @librechat/client.
2026-07-02 21:06:42 +02:00
Marco Beretta
5c1e0b831a
fix: restore MCP attach semantics and confirmations in the tools marketplace
Connecting an MCP server from the item dialog now enables all of its tools
once the connection settles, deselect-all keeps the server attached via its
placeholder token instead of detaching it, adding a server writes the token
so a zero-tool attachment survives a save, and removing a server from the
tools list asks for confirmation again. Consume-only servers are excluded
from the catalog, matching the old select dialog.

Also share the catalog/selection pipeline between ToolsSection and the
marketplace through useAgentItems, hoist NEW_ACTION_ID next to ActionItem,
drop unused status/view union members and stale TranslationKeys casts,
document the phase-2 Favorites/Made-by-you views, fix the needs-setup dot
semantics and card focus suppression, remove the redundant close button in
CreateSkillDialog, move useInputModality into @librechat/client so external
consumers can mount it, and delete dead files and orphaned translation keys.
2026-07-02 21:06:33 +02:00
Marco Beretta
9fa19a71d2
feat: make the skills create button a compact icon button 2026-07-02 17:12:57 +02:00
Marco Beretta
77d1cb58ae
feat: show a saving spinner and allow cancelling credential edits
Drive the tool credential Save button from the real mutation state so it
shows a spinner while the request is in flight, and add a Cancel button
when re-editing already-saved credentials so the edit can be dismissed.
2026-07-02 16:28:52 +02:00
Marco Beretta
cec2b92459
feat: match Code Interpreter file upload to the File Search dropzone
Swap Code Interpreter's thin btn-neutral bar for the same dashed dropzone
(DropzoneContent + dropzoneClassName) File Search already uses, so the two
capabilities' upload UIs are consistent.
2026-07-02 16:28:52 +02:00
Marco Beretta
7d18dd6dc4
feat: smoothly animate auth field changes in the MCP server dialog 2026-07-02 16:28:52 +02:00
Marco Beretta
d189f126d1
fix: vertically center OAuth dialog title against the MCP icon 2026-07-02 16:28:52 +02:00
Marco Beretta
2782af34c8
feat: show MCP server icon in OAuth dialog title 2026-07-02 16:28:52 +02:00
Marco Beretta
999301f10b
feat: cross-fade MCP tools between loading, list, and empty states 2026-07-02 16:28:51 +02:00
Marco Beretta
da551558fa
feat: smoothly collapse MCP connect button once connected 2026-07-02 16:28:51 +02:00
Marco Beretta
b31dbfa0c6
refactor: streamline MCP OAuth dialog
- Remove the Cancel button (the flow auto-closes on connect / times out)
- Show the URL in a read-only single-line scrollable input (cursor moves
  through it, not fully visible) with the shared CopyButton's smooth
  Copy/Check icon swap, matching the OAuth callback-URL field
- Put the primary Continue with OAuth action (icon trailing) and an
  icon-only QR toggle together in a row at the bottom, below the URL
- The QR reveals between the description and the URL with a smooth height
  animation (grid-rows 0fr to 1fr, matching MCPToolItem's reveal)
2026-07-02 16:28:51 +02:00
Marco Beretta
6c53a83a9e
feat: refine agent tools picker (skills, MCP connect/OAuth, web search)
- Skills picker: per-card visibility (public) and shared-author badges,
  category filtering, and an in-place Create skill flow that auto-attaches
  the new skill without leaving the builder
- MCP: inline Connect button in the first dialog plus a dedicated OAuth
  dialog (continue, copyable URL, QR code) shown only when OAuth is required
- Web search: auth-aware affordance, settings cog when user-provided and an
  info icon when system-defined
- Remove orphaned com_ui_unavailable/com_ui_initializing keys and the dead
  Tools/MCPToolItem component
2026-07-02 16:28:51 +02:00
Marco Beretta
71eb21d80c
feat: restore Memory capability toggle in agent builder tools catalog 2026-07-02 16:28:51 +02:00
Marco Beretta
178be2d4c7
feat: refine agent builder tools, actions, and MCP sections 2026-07-02 16:28:51 +02:00
Marco Beretta
e3ec9544ef
feat: redesign the agent builder tools, skills, and advanced panels
Replace the stacked capability/MCP/skill/tool/action form sections with a unified tools marketplace, per-item configuration dialogs, and a consolidated Advanced panel.

- unified tools marketplace (catalog, sidebar, polymorphic cards/rows) covering built-in capabilities, plugins, MCP servers, and actions, each with a detail/config dialog
- dedicated Skills picker and a Tools section with selected-item summaries and empty states
- redesigned action editor and authentication dialog (method cards, segmented controls)
- rebuilt Advanced panel: orchestration hub (subagents, handoffs, chain), max steps, skills kill-switch, copyable agent id
- restyled version history (timeline, tool/capability counts, in-app restore confirmation)
- shared component updates (Radio, Input/Textarea, dropdown z-index, dialog primitives) and keyboard-only focus rings via useInputModality
- format-hint placeholders for tool credential fields
- sanitize numeric parameter inputs to prevent comma truncation
2026-07-02 16:28:51 +02:00
Danny Avila
477ee3439c
🧪 ci: De-flake ConversationsSection memoization spec on slow runners (#14071)
waitFor resolves as soon as BookmarkNav's data hook has fired once, but the Suspense resolution commit can still have a trailing render pass pending on slow runners (Windows CI shards). The first stream tick then flushes that leftover pass alongside the tick, inflating the memoized children's render counts past the captured baseline (Expected: 1, Received: 2). Flush pending commits with an empty async act() before capturing baselines.
2026-07-02 10:11:43 -04:00
Danny Avila
0eef64344d
🌍 i18n: Update translation.json with latest translations (#14070) 2026-07-02 09:19:43 -04:00
Danny Avila
e6f5b6e70a
🌍 i18n: Update translation.json with latest translations (#14053) 2026-07-01 11:18:00 -04:00
Marco Beretta
6576688f1b
📥 fix: Download Original File From Artifact Preview Panel for Office Documents (#14026)
* fix: download original file from artifact preview panel for office documents

The preview panel download button serialized the rendered HTML preview
instead of the original binary for office artifacts (pptx/xlsx/docx)
produced by the code interpreter, so users got an `index.html` text
scrape rather than the file. The inline chat card was unaffected because
it downloads the real file via `useAttachmentLink`.

Thread the original-file download metadata (filepath/file_id/source/user)
through `fileToArtifact` onto the Artifact, and update `DownloadArtifact`
to fetch the original file through that same path for preview-only office
artifacts. Text, source, and markdown artifacts keep the blob path so
their in-panel content (and edits) still download as-is.

Closes #14002

* fix: require a usable route before downloading the original artifact file

A shared link to a non-snapshotted code-execution office artifact strips
source/user and deletes filepath while keeping file_id (share
sanitization + applyShareFileRoute). The preview-panel download gate
treated that lone file_id as sufficient, so it routed to an empty
useCodeOutputDownload fetch and downloaded nothing instead of falling
back to the preview-content blob.

Take the original-file branch only when useAttachmentLink can actually
fetch: a non-empty filepath (http target, share route, or code-output
URL) or full local-file metadata (isLocallyStoredSource + file_id +
user). Export isLocallyStoredSource from LogLink so the panel reuses the
same predicate.

* fix: only show artifact download success after the file is delivered

useAttachmentLink swallows fetch errors (an expired code-output URL or a
404 share download) and resolves without throwing, so the preview-panel
download button flipped to the success checkmark even when no file was
downloaded.

Return a boolean from handleDownload (true once a download is initiated,
false on error/empty response) and only mark the artifact download as
succeeded when a file was actually delivered. The return value is
ignored by the existing onClick callers.
2026-07-01 08:19:34 -04:00
Danny Avila
6523a5add6
🗺️ refactor: Light Up In-Viewport Ribs at Rest in Message Nav (#14041)
At rest (no hover/focus) the rail now reads like a minimap: only the
in-viewport message ribs are at full opacity while the rest fade to 40%,
updating live as you scroll. Replaces the blanket nav opacity-30 with
per-rib opacity driven by the existing viewport-visibility highlight, so
hover/focus still brings every rib to full opacity for the fisheye.
2026-06-30 16:49:01 -04:00
Danny Avila
dd8a4558f1
🪗 feat: Dock-Style Fisheye Nav Rail With Instant Hover Preview (#14021)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
*  feat: Dock-Fisheye Message Nav Rail with Instant Hover Preview

* 🎚️ refactor: Uniform resting ribs + clickable cursor for message nav

* 🧹 fix: One rib per message in nav rail (dedupe nested message-render)

* 🎯 fix: Accurate fisheye focus + click-anywhere-to-jump in message nav

- Measure rib centers relative to the column (getBoundingClientRect) instead
  of offsetTop, which was relative to the positioned <nav> and shifted the
  pointer->rib mapping by the chevron height (hovered line wasn't the peak,
  preview showed an earlier message).
- Column-level click jumps to the focused rib, so clicking anywhere the
  preview is showing works even when the pointer is off the thin line.
- Restore @librechat/client jest stub to keep the unit isolated.

* 💡 fix: Highlight only the hovered rib white in message nav

* 🫥 style: Transparent message nav (drop pill background)

*  feat: Keyboard focus mirrors hover (magnify + highlight + preview) in message nav

Tabbing to / Shift+Alt+M focusing a rib now drives the same fisheye
pipeline as pointer hover via onFocus/onBlur on the column: the focused
rib magnifies, highlights white, and shows the shared preview. Also
addresses Codex finding on keyboard-focus previews.

* 🩹 fix: Live tooltip preview + legacy media-query fallback in message nav

- Derive the shared preview text from entryById at render time instead of
  snapshotting it into tip state, so a streaming/updating message refreshes
  the open tooltip without leaving and re-entering the rail.
- Feature-detect MediaQueryList.addEventListener and fall back to
  addListener/removeListener so the reduced-motion watcher no longer throws
  (and breaks the nav) on Safari/iOS < 14.

Addresses both Codex findings on review 4601236141.
2026-06-30 14:21:22 -04:00
Marco Beretta
e5d5018d7f
perf: memoize FavoritesList and BookmarkNav to prevent re-renders during streaming (#14011)
* perf: memoize FavoritesList and BookmarkNav to prevent streaming re-renders

ConversationsSection re-renders during message streaming as its
conversation-list query and title generation update the cache. Its
FavoritesList and BookmarkNav children were not memoized, so they
re-rendered on every parent commit despite their props and
subscriptions never changing during a stream.

Wrap both in React.memo to insulate them from the parent cascade.
Their props (toggleNav, isSmallScreen, tags, setTags) are referentially
stable, so memo fully decouples them. Add a regression test asserting
FavoritesList does not re-run when its parent re-renders with stable
props.

* test: verify ConversationsSection insulates Favorites/Bookmarks from streaming re-renders

Renders the real ConversationsSection (mocking only data hooks) and
forces repeated re-renders via a subscription it depends on, mirroring
the conversation-list/title-generation cache churn during streaming.
Asserts FavoritesList and BookmarkNav do not re-render, proving the
parent passes referentially stable props so React.memo holds in the
real render path (not just with hand-fed stable props).
2026-06-30 11:30:04 -04:00
Danny Avila
6dbf9d5ad3
🪝 feat: Human-in-the-Loop Runtime - Tool Approval + Ask-User-Question (Slice B) (#13942)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* chore: add @langchain/langgraph-checkpoint-mongodb for HITL durable resume

* feat: HITL tool approval runtime — backend (Slice B)

- endpoints.agents.checkpointer config + durable Mongo checkpointer (seam over the app
  connection; SDK MemorySaver fallback) with a TTL index + deleteThread pruning
- HITL run wiring (PreToolUse policy hook + humanInTheLoop) attached in createRun, fully
  inert when toolApproval.enabled is off
- interrupt gate (pause job -> requires_action + emit on_pending_action) and a resume
  route that rebuilds the run from the durable checkpoint and run.resume()s it
- atomic single-winner resolve; agent-consistency guard; expireStaleApprovals terminal
  event; checkpoint pruned on every non-paused completion (thread_id == conversationId)

* feat: HITL tool approval UI — frontend (Slice B)

approve/reject/edit/respond + ask-user controls in the tool card (OAuth-button precedent),
batch-aware single submit, live + reconnect (resumeState.pendingAction) wiring, and resume
mutations posting to /agents/chat/resume.

* fix(hitl): decouple ApprovalProvider from chat context

ApprovalProvider is now pure state (safe to mount in provider-less / shared / test
renders); the context-dependent submit moved to a useResumeSubmit hook the cards call.
Part imports getAskUserQuestionPart from ~/utils/approval directly so suites that
partial-mock ~/utils render Part without throwing.

* fix(hitl): address Codex review — backend

- P1: enforce per-tool allowed_decisions on resume (reject a crafted decision the
  policy disallows) via findDisallowedDecisions
- prune the durable checkpoint on user-abort of a paused run, and before a fresh
  HITL turn, so a new turn cannot rehydrate an expired/aborted interrupt (thread_id
  is the stable conversationId)
- persist + use isTemporary and the original parentMessageId on resume (temporary
  chats stay temporary; initializeAgent scopes thread files off the right parent)
- generate a deferred first-turn title BEFORE completeJob so its event reaches the
  client and the final event carries the real title
- moderateText: skip when there is no text (tool-approval resume) and moderate the
  ask-user answer, instead of denying on an empty input

* fix(hitl): address Codex review — frontend

- render ToolApproval for ANY paused agent tool card (bash/code/file/etc.), not just
  the generic ToolCall, by wrapping the tool-card branch in Part (moved the rendering
  out of ToolCall)
- findPendingActionMessageIndex only matches an assistant message, never the user
  message (the underscore-strip could target the user bubble before the assistant
  placeholder exists)

* fix(hitl): address Codex re-review

- title eligibility checks the user message’s parent (first turn), not the response’s
  parent — the previous check could never be true and skipped title generation
- use client.buildResponseMetadata() for the resumed message so contextUsage /
  thoughtSignatures survive (the abort-only helper dropped them)
- moderate decisions[].responseText (the respond action’s user text)
- give /chat/abort req.config (configMiddleware) so the HITL checkpoint prune on abort
  actually runs
- read resume state BEFORE setContentParts so the in-memory store does not lose the
  pre-pause seed content
- count resumes against LIMIT_CONCURRENT_MESSAGES (increment/decrement) so paused-then-
  resumed turns cannot bypass the limit
- require actionId on resume so a body without it cannot resolve the current action

* fix(hitl): address Codex re-review (round 3) — resume fidelity

Bring the lean resume path to parity with sendMessage for things it bypassed:
- carry userMCPAuthMap into the rebuilt run so approved MCP tools keep the user's creds
- seed initialSessions (buildInitialToolSessions) so approved code/file/skill tools have
  the pre-pause uploaded-file context (esp. cross-replica / after restart)
- await client.artifactPromises and persist them as response attachments (else tool
  artifacts created after the pause vanish on reload / for late subscribers)
- merge metadata: cumulative usage (+ summary marker) from the job, contextUsage /
  thoughtSignatures from the client — fixes the round-2 regression that underreported
  post-resume cost

* fix(hitl): address Codex re-review (round 4) — resume hardening

- resume: require an EXACT paused agent_id match (reject omitted/ephemeral
  agent_id, not just a different one) and reject an endpoint mismatch, so a
  request can't rebuild the claimed checkpoint on a different graph
- moderateText: also moderate a tool-approval decision's reject `reason` and
  stringified `editedArguments`, not just `responseText`
- request: re-mark the paused response `unfinished:true` after BaseClient saves
  it as completed, so an expired / never-resumed approval doesn't leave a
  "finished" response in history; the resume path overwrites it on success

* test(hitl): route-level integration test for the resume controller

Adds api/server/controllers/agents/__tests__/resume.spec.js, a supertest
integration test that drives the real ResumeAgentController over the full
pause -> approve -> resume -> finalize lifecycle with the SDK run, durable
checkpointer, Mongo, and concurrency cache mocked. The pure decision/liveness
helpers run for real via requireActual, so the guard ladder is exercised end to
end rather than stubbed.

25 cases covering:
- the authorization / staleness / agent-and-endpoint / actionId guard ladder
- tool_approval validation (undecided tool call, policy-disallowed decision)
- ask_user_question answer requirement
- the concurrency gate (429) and the atomic single-winner claim (409)
- the happy path: ACK, run reconstruction, decision->SDK mapping, finalize
  (save the now-finished response, emit done, complete job, prune checkpoint)
- first-turn title generation before stream completion
- re-pause (no double finalize), abort-during-resume (no double finalize),
  and the resume-failure terminal path (emitError + completeJob + prune)

* test(hitl): strengthen resume coverage + add approval util tests

Acts on a self-audit of the new resume integration test.

resume.spec.js (25 -> 32 cases):
- replace the tautological emitDone assertion (it only checked the hardcoded
  `final: true`) with a structural check of the finalEvent payload —
  responseMessage content/id/unfinished, requestMessage identity, title
- cover the previously-unwalked finalize branches: tool-artifact attachments
  (null-filtered), the aggregatedContent fallback when live content is empty,
  and client response-metadata attachment
- add guard cases: unsupported pending-action type (400) and the
  pre-multi-tenancy null-tenantId pass-through (must not 403)
- add error-path cases: first-turn title generation throwing must still
  finalize, and a completeJob failure during a resume error must force a
  terminal job state via the last-resort updateJob

client/src/utils/approval.spec.ts (new, 15 cases):
- applyPendingAction tool_approval: join by tool_call_id not position,
  skip completed calls, default allowed_decisions to [], referential
  stability when nothing changes
- applyPendingAction ask_user_question: append, idempotent replace on replay,
  non-array content coercion
- getAskUserQuestionPart type guard; findPendingActionMessageIndex
  assistant-only resolution (never resolves to the user bubble)

* fix(hitl): address Codex re-review (round 5)

Five findings verified against the code before fixing:

- resume: require an EXACT endpoint match (like agent_id) — a resume that OMITS
  endpoint must not fall through, since the shared chat middleware treats a
  missing/non-agents endpoint as the ephemeral agent and could rebuild the
  claimed checkpoint on a different graph
- resume: filter malformed content parts before saving the finished response,
  matching the normal AgentClient path (a resumed turn could otherwise persist
  an empty/invalid tool_call part that breaks reload/rendering)
- resume: accumulate tool artifacts across pause segments — persist them on
  re-pause and MERGE (not overwrite) at finalize, so artifacts produced before
  a second approval pause aren't dropped by the next rebuilt client
- approval (client): findPendingActionMessageIndex returns -1 when a provided
  responseMessageId isn't found, so the caller retries instead of attaching the
  prompt/approval to a prior assistant reply; fall back to the last assistant
  only when no responseMessageId is given
- RedisJobStore: make appendChunk extend-only (XADD + EXPIRE-if-shorter via a
  single eval) so the on_pending_action chunk emitted after a pause can't reset
  the chunk-stream TTL back to the running window and evict pre-pause content
  before the approval is resolved

Tests: +endpoint-omitted/unsupported-type/malformed-filter/attachment-merge/
re-pause-persist cases in resume.spec.js (36); ask-retry -1 semantics in
approval.spec.ts (16); extend-only TTL assertion in the RedisJobStore Redis
integration spec.

* test(hitl): mongodb-memory-server integration test for the checkpointer seam

The checkpointer unit spec covers config/selection with no DB connection; this
exercises the durable Mongo seam against a real (in-memory) MongoDB — the part
correctness actually depends on:

- getAgentCheckpointer builds a real MongoDBSaver when Mongo is connected and
  setup() creates the TTL index (expireAfterSeconds) on the checkpoint collection
- memory type returns undefined (SDK MemorySaver fallback) even when connected
- saver is memoized per resolved config
- deleteAgentCheckpoint prunes a thread's persisted checkpoint (the cross-turn
  isolation guarantee: turn N+1 on the same conversationId can't rehydrate it)
- pruning is thread-scoped — deleting one conversation leaves others intact
- undefined threadId is a no-op

* fix(hitl): address Codex re-review (round 6)

Four findings verified against the code before fixing:

- messageFilterPii: scan the resume payload's user-authored text (ask-user
  `answer`, and a tool-approval decision's `respond` text, `reject` reason, and
  edited tool arguments) — the shared /resume route ran through the PII filter
  but it only inspected req.body.text, so a blocked token rode the resume
  payload back into the model/tool (mirrors the earlier moderateText fix)
- resume: re-prime skill files invoked in the pre-pause segment before rebuilding
  the run, so an approved code/file-backed tool keeps the injected skill-file
  session refs instead of running without them (mirrors the normal path's
  primeInvokedSkills; the pre-pause content stands in for the message payload)
- hitl: pin the graph identity. Persist a fingerprint of the graph-determining
  request fields (endpoint, agent_id, model, spec, ephemeralAgent — normalized)
  on the pending action at pause, and reject a resume whose recomputed
  fingerprint differs. This closes the ephemeral-agent gap, where agent_id is
  undefined so the id guard can't tell two ephemeral configs apart
- resume: reject incomplete edit/respond decisions (findIncompleteDecisions) —
  an `edit` without an object editedArguments or a `respond` without non-empty
  responseText is 400'd before mapping, rather than defaulting to {} / '' and
  resuming with behavior the user never approved

Tests: incomplete-decision + fingerprint match/mismatch cases in resume.spec.js
(41); findIncompleteDecisions + computeAgentRequestFingerprint unit tests; and
resume-field PII cases in messageFilterPii.spec.ts.

* fix(hitl): address Codex re-review (round 7)

Four findings verified against the code before fixing:

- RedisJobStore: clear `agent_id` on createJob (add it to staleHitlFields). The
  job hash is keyed by conversationId and reused across turns; updateMetadata
  only writes agent_id when truthy, so a conversation that switched from a saved
  agent to an ephemeral/no-agent turn kept the old id and the resume guard
  rejected the valid pause as a different agent. (real correctness bug)
- fingerprint: include `promptPrefix` in computeAgentRequestFingerprint, and
  re-send it on resume (ResumeAgentFields + buildResumeFields). Ephemeral agents
  derive their system instructions from promptPrefix, so a resume changing it
  previously passed the pin and rebuilt different instructions. (completes the
  round-6 fingerprint)
- resume: the re-pause branch now persists the segment's accumulated CONTENT
  (filtered), not just artifacts, so an approval that expires/reaps without a
  final resume no longer loses everything streamed during the resumed segment.
- request: carry `manualSkills`/`alwaysAppliedSkills` on the persisted user
  message so a resumed turn's reconstructed requestMessage keeps its skill pills
  instead of dropping them until a full reload.

Deferred (narrow, no safe contained fix yet — see PR thread replies):
- resume rebuild without `addedConvo` for a multi-conversation/added-agent pane
- cross-replica re-prime of manually-selected (not model-invoked) skill files

Tests: stale-agent createJob clearing (Redis integration), promptPrefix
fingerprint match/mismatch (resume.spec.js + policy.spec.ts), re-pause content
persistence (resume.spec.js).

* fix(hitl): address Codex re-review (round 8)

Five findings verified against the code before fixing; the headline is a durable-
resume correctness fix (the fingerprint had surfaced it as a 403):

- resume durability (the important one): persist the graph-determining request
  fields (endpoint, agent_id, model, spec, promptPrefix, ephemeralAgent) on the
  pending action as `resumeContext`, and REPLAY them onto the resume request via
  a router-level middleware that runs before buildEndpointOption. The client
  can't reconstruct the ephemeral-agent config after a reload/cross-session, so
  the round-6/7 fingerprint would 403 a valid durable resume — and even without
  it the rebuilt agent would lose its tools. Replaying server-side rebuilds the
  SAME graph regardless of client state (and a crafted resume can't swap it; the
  fingerprint still matches because the body is restored first).
- RedisJobStore: also clear `isTemporary` on createJob (same class as agent_id):
  a prior temporary turn's flag would otherwise survive a reused conversation
  hash and a later non-temporary resume would save its response as temporary.
- resume: persist `contextMeta` (context-window calibration) onto the saved
  response like BaseClient does, so the next turn can seed its pruner.
- request: carry manualSkills/alwaysAppliedSkills into the onStart metadata
  update (not just the preliminary one it overwrites), so a resumed turn's
  requestMessage keeps its skill pills.

Deferred (narrow — see thread reply):
- saved-agent edited WHILE a run is paused: agent_id matches but the definition
  changed; needs an agent version/config hash, which is a larger change for a
  narrow window.

Tests: resumeContext pick/apply + round-trip (policy.spec.ts), contextMeta +
manualSkills-on-requestMessage (resume.spec.js), isTemporary clearing (Redis
integration).

* style(hitl): prettier line-wrap in policy.spec.ts (R8 lint fix)

* fix(hitl): address Codex re-review (round 9)

Five findings, all fixed (addedConvo — deferred in rounds 7/8 — is now trivial
thanks to the round-8 replay):

- replay addedConvo: add it to RESUME_CONTEXT_KEYS so the resume middleware
  restores the parallel/secondary-agent config from the paused request; the
  client can't reconstruct it, and it determines the rebuilt graph.
- skill pills (the real fix this time): the round-8 onStart metadata write was
  overwritten by trackUserMessage (the authoritative userMessage writer). Carry
  manualSkills/alwaysAppliedSkills in the emitted `created` message and persist
  them in trackUserMessage; widen UserMessageMeta + SerializableJobData.userMessage.
- execute-code files on resume: seed the paused user message's own files onto
  req.body.files before initializeClient — they're excluded from the
  parent-walk code-session rebuild, so an approved code/read-file tool would
  otherwise resume without them.
- in-memory pending-action UI: route ApprovalEvents.ON_PENDING_ACTION in the
  resume replay/pending-event loops to applyPendingActionToMessages (mirror the
  live handler), so a pause that lands in the snapshot window still renders its
  approval controls instead of sitting paused with no UI.
- abort isTemporary: the /chat/abort partial-save now sources isTemporary from
  the job metadata, not req.body (the stop button posts only conversationId), so
  aborting a paused temporary chat no longer persists an orphaned partial.

Tests: addedConvo in pickResumeContext (policy.spec.ts), file-restore on resume
(resume.spec.js), abort-from-job-isTemporary (abort.spec.js).

* fix(hitl): address Codex re-review (round 10) — resume/expiry races

Three concurrency/coherence findings, verified against the code before fixing:

- expiry-sweep CAS scope: both stale-approval sweeps (GenerationJobManager
  expireStaleApprovals and the RedisJobStore requires_action cleanup) called
  expire()/transitionStatus WITHOUT the observed pendingAction.actionId, so the
  CAS only checked status===requires_action. Between the read and the CAS a user
  could resolve the observed action and the run re-pause on a FRESH action; the
  stale sweep would then abort that valid new pause. Now both pass the observed
  actionId as expectActionId, so the CAS only fires for the action read as stale
  (a re-paused action has a different id → no-op).
- resume graph cache: resumeCompletion cached the rebuilt graph (created with
  messages:[]) via setGraph; RedisJobStore.getContentParts prefers a cached
  graph over reconstructing from the chunk log, so a same-replica reload/status
  poll mid-resume returned aggregatedContent missing the pre-pause content. Skip
  setGraph on resume so introspection falls back to the complete chunk
  reconstruction (setContentParts still seeds the in-memory store).
- pending-action UI: applyPendingActionToMessages scheduled a SINGLE
  animation-frame retry then dropped the pending action; Recoil/React updates can
  take several frames under load, leaving a valid requires_action run with no
  approval controls. Retry across frames (bounded at 120) until the target
  message commits.

Test: expire() with a mismatched expectedActionId no-ops while the matching id
expires (pendingAction.spec.ts).

* chore(deps): update @librechat/agents to version 3.2.53 and @langchain/langgraph to version 1.4.7 in package-lock.json and related package.json files

* refactor(hitl): add resolveToolApprovalPolicy seam for layered policy

Extract the single point where tool-approval policy is resolved for a turn
(`resolveToolApprovalPolicy`) and route the run call site through it instead
of reading `endpoints.agents.toolApproval` inline.

Behaviour-preserving: only the `endpoint` layer is wired today, so the result
is identical to reading the app policy directly. The `agent` and `skills`
layers are reserved seams with documented precedence (endpoint owns the
`enabled` kill switch; agent overrides mode/allow/deny/ask/reason; skills may
only tighten), so future per-agent and per-skill policy plumbing lands in one
function rather than at the `createRun` site. Adds focused unit tests.

* fix(hitl): address Codex re-review (round 11) — resume hardening

F1 (P2, security) — applyResumeContext now DELETES any RESUME_CONTEXT_KEY
absent from the persisted context, so the resume body carries exactly the
graph-determining fields the pause had. Previously only defined keys were
overwritten, leaving a client-supplied `addedConvo` (which the request
fingerprint does not cover) in place — a crafted resume could rebuild a
single-agent checkpoint as a different multi-agent graph/tool set.

F3 (P2) — the resume route ACKs (res.json) before initializeClient, so a
post-ACK getMCPRequestContext(req, res) saw the response as finished and
returned undefined, leaving the resumed run without its run-scoped MCP
connection store (approved MCP / OAuth-overlay tools then ran without their
request-scoped connections). Pre-seed the store with a null res +
cleanupOnResponse:false before the ACK and tear it down in the finally,
mirroring the normal stream path (request.js). userMCPAuthMap was already
preserved separately, so credentials were not lost — only the connection store.

Declined: the ApprovalContext NEW_CONVO guard (P2) is a false positive — the
`created` SSE event updates the conversation atom before any pause renders, so
the id is concrete by click time (details in the PR thread).

Tests: policy.spec (absent-key delete) + resume.spec (MCP context pre-seed/cleanup order).

* fix(hitl): address Codex re-review (round 12) — resume fidelity + multi-tool UI

F4 (P2) — temporal prompt vars: resume rebuilt the agent without restoring
req.conversationCreatedAt or req.body.timezone, so {{current_datetime}}-style
vars compiled a different system prompt than the paused graph (resume wall-clock,
unzoned). Add 'timezone' to RESUME_CONTEXT_KEYS (persisted at pause, replayed by
the resume middleware) and restore conversationCreatedAt from the convo before
initializeClient — mirroring the normal path's resolveConversationCreatedAt.

F5 (P2) — multi-tool approval: applyPendingActionToMessages stopped retrying once
ANY tool-call part was tagged, so siblings that rendered on later frames never got
approval controls and the resume route 400'd the partial batch. Add
countTaggedApprovalParts and keep the bounded RAF retry going until every
action_request is tagged (ask_user_question unchanged — one synthetic part).

F6 (P3) — Edit accepted `null`/`[]` (valid JSON, non-object), enabling Submit for
a value the resume route rejects via findIncompleteDecisions. Mirror the server's
plain-object check in the client (store + editIsValid) so Submit only enables for
an accepted value.

Tests: policy.spec (timezone round-trip), resume.spec (conversationCreatedAt
restore), approval.spec (countTaggedApprovalParts).

* fix(hitl): address Codex re-review (round 13) — recurse into subagent approvals

F9 (P2) — a tool paused INSIDE a subagent has its tool_call_id in the parent
subagent tool_call's nested `subagent_content`, not as a top-level message part.
applyToolApproval and countTaggedApprovalParts only scanned top-level content, so
the approval never attached and the round-12 retry loop counted 0 tagged parts and
spun to its frame cap with no controls. Both now recurse into `subagent_content`
(immutably, so React refs update): the nested call gets tagged and is counted, so
the retry terminates. Added approval.spec cases for the nested tag + count.

Note: surfacing the interactive approve/reject controls inside the subagent view is
a deliberate follow-up — ToolApproval -> useResumeSubmit -> useChatContext crashes
when rendered in the portaled subagent dialog (outside the chat/approval providers),
so that needs the controls scoped to the in-provider inline render (or the dialog
wrapped with the providers). This commit fixes the data/traversal layer only.

F7 (discovered-tool history on resume) and F8 (redis chunk TTL pause race) were
verified false positives — see the PR threads.

* fix(hitl): address Codex re-review (round 14) — resume fidelity + expiry relay

F13 (P2) — manualSkills are graph-determining (skill allowed-tools union into the
tool set before tools load) but weren't replayed, so a reload lost the skill tools
and a crafted resume could inject a different skill past the fingerprint. Add
'manualSkills' to RESUME_CONTEXT_KEYS (same replay-only pattern as timezone/
addedConvo; the delete-absent half blocks injection). Not alwaysAppliedSkills —
that's resolved server-side from the DB, not req.body.

F12 (P2) — the resume final SSE built requestMessage from job.metadata.userMessage
(persisted without files), so attachments vanished from the user bubble on resume.
Spread the already-restored req.body.files onto it, matching the normal path.

F11 (P2) — multi-replica approval expiry: RedisJobStore.cleanupRequiresActionIndex
on another replica can win the requires_action->aborted CAS (it sets the hash error
but has no event transport), and the local sweep then skips because the job is no
longer requires_action, so a client subscribed here never gets the terminal error
until the reap path. expireStaleApprovals now relays APPROVAL_EXPIRED_ERROR for a
locally-subscribed job already aborted FOR approval expiry (error-string gated,
idempotent via the errorEvent flag). emitError already publishes cross-replica.

Tests: policy.spec (manualSkills round-trip + inject-drop), resume.spec (final
requestMessage carries restored files).

* fix(hitl): render approval controls for subagent-nested tool pauses (F10)

Round-13 made applyToolApproval/countTaggedApprovalParts recurse into
subagent_content (data), but SubagentDialogPart rendered nested TOOL_CALL parts
with <ToolCall> only and never mounted <ToolApproval>, so a tool paused inside a
subagent showed no controls and the run was unresolvable.

Render <ToolApproval> in SubagentDialogPart's TOOL_CALL branch when the nested
tool_call carries an approval and isn't yet resolved, mirroring the top-level
Part.tsx render. The subagent dialog portals (OGDialog → ReactDOM.createPortal),
but React context flows through the React tree, not the DOM tree, so ToolApproval
resolves ApprovalProvider/ChatContext and the controls work + submit.

Also harden useResumeSubmit: read ChatContext via useContext (non-throwing)
instead of the throwing useChatContext wrapper, so the cards never crash when
rendered outside a ChatContext.Provider (e.g. a search/citation render that passes
chat context as a prop) — they degrade to inert (buildResumeFields returns null).

* style(hitl): re-sort run.ts imports after dev rebase

* fix(hitl): address Codex re-review (round 15) — resume content fidelity

F14 (P2) — hide_sequential_outputs was applied in chatCompletion before
saving/emitting content but not on resume, so a sequential-agent chain that
pauses for HITL and resumes persisted/emitted intermediate outputs the setting
is meant to hide. Extracted the filter into applyHideSequentialOutputsFilter()
and call it from both chatCompletion and resumeCompletion (after handleRunInterrupt,
covering the finalize + re-pause reads of client.contentParts).

F16 (P2) — on a reloaded HITL pause, the DB already holds the paused user row +
partial assistant row; useResumeOnLoad fed those as submission.messages, then
finalHandler/createdHandler appended the same pair via requestMessage/responseMessage,
duplicating the turn (buildTree doesn't dedupe children by messageId). buildSubmission-
FromResumeState now strips the paused user/response rows (by messageId, incl. the
padded/unpadded response id) from submission.messages — they're re-supplied by the
placeholders + final event. Frontend-only; live (non-reload) pause path untouched.

Deferred: F15 (collapsed-card subagent approval registration/visibility) — see thread.

Tests: client.test (filter keeps last + tool_call parts / no-op when off),
useResumeOnLoad.spec (paused pair stripped from submission.messages).

* fix(hitl): address Codex re-review (round 16) — chunk TTL, slot, job replacement

F17 (P2) — chunk-stream TTL on pause-before-chunk. CHUNK_APPEND_LUA derived its
ceiling only from the chunk key's current TTL, so when the chunks key didn't exist
at pause (fire-and-forget append in flight, or an ask-user pause before any chunk),
the on_pending_action append created the stream with only the 20m running TTL while
the approval window is 24h — content evicted before resume. The Lua now also reads
the job key (KEYS[2]); when status == requires_action it takes max(running, TTL(jobKey))
(the approval window transitionStatus set), else the running TTL. Extend-only preserved;
gated on paused status so normal runs never inflate. Both keys share {streamId} (cluster-safe).

F19 (P2) — with LIMIT_CONCURRENT_MESSAGES, the approval prompt was emitted before the
original request released its slot, so a fast Approve got /resume 429'd. handleRunInterrupt
now releases the slot (idempotent via pendingRequestReleased) right after the pause, before
the prompt; the request.js pause branch and resume.js finally only release if it didn't
(no double-release).

F20 (P2) — finalizeResumedTurn never checked the job wasn't replaced before emitDone/
completeJob/saveMessage, so a stale resume could clobber a newer turn that reused the
conversationId. Added the createdAt guard the normal request path uses (skip finalization
when the live job's createdAt != the paused job's).

Deferred: F18 (subagent_content not reconstructed on Redis resume) — joins the subagent
cluster (F15). See thread.

Tests: RedisJobStore integration (pause-before-chunk gets approval TTL; running stays short),
resume.spec (skip finalization on replacement; no double slot release on re-pause).

* 🛡️ fix: Guard HITL terminal side-effects against job replacement

Jobs are keyed by streamId == conversationId, so a new request REPLACES the
running one on the same conversation. The replaced generation's tail must not
clobber the live generation's state. Each path now re-reads the live job and
compares createdAt against the generation's captured identity before acting.

- Thread the generation's createdAt onto the client (request.js + resume.js)
  as client.jobCreatedAt — the identity every guard compares against.
- handleRunInterrupt: skip approvals.pause when this run is no longer the live
  job, so a stale interrupt can't flip the NEWER job to requires_action.
- chatCompletion finally: skip the checkpoint prune when replaced, so an older
  run's late finally can't delete the newer run's resume checkpoint.
- resume catch-path: gate emitError/completeJob/prune behind a stillLive check
  (fail-open if the read throws), mirroring finalizeResumedTurn's success guard.
- Persist the turn's uploaded files on job.metadata.userMessage (authoritative
  trackUserMessage writer) and prefer them on resume over the user DB row, whose
  save can still be racing a fast /resume.

Tests: 13 guard-predicate cases in jobReplacement.spec.js.

* 🔁 fix: Harden HITL resume — ownership re-check, file seeding, deferred-tool replay

Three follow-ups to the round-17 job-replacement guards (Codex review 4594099963):

- G1 (resume.js): the success-path ownership guard runs at the START of
  finalizeResumedTurn, but saveMessage + first-turn title generation await long
  enough for a new request to replace the job on the same conversationId. Re-read
  the live job immediately before emitDone/completeJob/prune so the terminal writes
  can't tear down the REPLACEMENT job — mirrors the catch-path guard.

- G2 (request.js): onStart's metadata/chunk writes that persist the turn's files
  are fire-and-forget, so a fast approval could read job.metadata.userMessage before
  files landed. Seed files into getPreliminaryUserMessage instead — that write is
  AWAITED before the run starts, so files are durable before any interrupt can emit.

- G3 (run.ts + client.js + resume.js + IJobStore.ts): the resumed graph is rebuilt
  with messages: [], so createRun's tool_search-discovery scan finds nothing. A
  deferred tool discovered earlier in the turn (and targeted by the paused call) was
  therefore absent from the rebuilt schema-only toolMap — resume would throw "unknown
  tool" (no loadRuntimeTools fallback is wired). Capture discovered tool names at
  pause via extractDiscoveredToolsFromHistory(run.getRunMessages()), persist them on
  job.metadata.discoveredTools, and replay them into createRun's new discoveredToolNames
  input (merged with message-extracted names, gated on hasAnyDeferredTools — inert
  otherwise). A new createRun test proves the deferred tool is promoted with the replay
  and absent without it (reproducing the bug).

Tests: real createRun deferred-replay suite (run-summarization.test.ts) + G1/G2/G3
guard predicates (jobReplacement.spec.js). Full suite green.

* 🔒 fix: Close HITL resume metadata + file-substitution + pause-race gaps

Four findings on the round-18 commit (Codex review 4594430222):

- H1 (P1, regression in round-18 G3): the discoveredTools captured at pause never
  reached resume — three metadata allowlists dropped it: GenerationJobManager
  .updateMetadata, RedisJobStore.deserializeJob, and buildJobFacade (plus the
  GenerationJobMetadata type). Added discoveredTools to all four, so the deferred-tool
  replay actually works end-to-end (in-memory store already kept it via Object.assign).

- H2 (P2, security): /resume honored a client-supplied `files` array, letting a crafted
  client resume an approved code/read-file tool against a DIFFERENT file set than the one
  approved (files aren't in the resume fingerprint/context). Resume now ALWAYS sources
  files from the paused job (metadata → DB row), clearing any client-supplied set.

- H3 (P2, ephemeral fidelity): non-default model parameters (temperature, max tokens,
  custom endpoint params) were lost on resume — ephemeral agents derive them from the
  request body, which the resume payload omits. Capture the resolved model_parameters in
  resumeContext at pause and replay them onto the body on resume (excluding `model`, which
  is replayed via the fingerprinted RESUME_CONTEXT_KEYS path). Saved agents already source
  these from the DB.

- H4 (P2, Redis race): a pause landing between the resume snapshot and the Pub/Sub
  subscription reached neither resumeState.pendingAction nor (Redis) pendingEvents, and
  approval events aren't persisted to replayEvents — the client attached to a paused job
  with no approval UI. subscribeWithResume now re-reads the live job AFTER subscribing and
  surfaces the pending action if the snapshot missed it (live read, no staleness).

Tests: discoveredTools metadata round-trip + subscribeWithResume re-read (pendingAction
.spec.ts); client-file substitution rejection (resume.spec.js); model-parameter replay
predicate (jobReplacement.spec.js).

* 🧹 fix: Clear stale discovered tools, release slot on claim error, extend run-step TTL

Three follow-ups on the round-19 commit (Codex review 4594783691):

- I1 (P2): the round-19 discoveredTools field wasn't cleared on Redis streamId reuse.
  HSET only overwrites listed fields and handleRunInterrupt only writes discoveredTools
  when THIS turn discovers a deferred tool — so a replacement turn that pauses without its
  own discovery inherited the prior run's tool names and force-loaded undiscovered deferred
  tools on resume. Added discoveredTools to createJob's staleHitlFields HDEL list (the
  in-memory store already builds a fresh object, so it was Redis-only).

- I2 (P2): with LIMIT_CONCURRENT_MESSAGES, approvals.resolve runs after the slot increment
  but before the run's try/finally, so a store/Redis error there leaked the slot until the
  counter TTL expired (spurious 429s on retry of the still-paused approval). Wrapped the
  claim in try/catch that decrements the slot and returns 500.

- I3 (P3): saveRunSteps did SET ... EX running unconditionally, resetting the run-steps key
  to the 20-min running TTL even while the job is paused for the longer approval window —
  a reload after that window lost the tool timeline. Now uses a paused-window TTL script
  mirroring the chunk-stream no-shrink behavior (extends to the approval window when the
  job hash is requires_action).

Also fixes a latent strict-tsc cast error in the round-19 pendingAction test.

Tests: claim-throws-releases-slot (resume.spec.js); discoveredTools cleared on reuse +
saveRunSteps preserves the paused TTL (RedisJobStore integration, USE_REDIS).

* 🛡️ fix: Guard fast-resume save race, gate HITL to resumable routes, expire on stale submit

Three findings on the round-20 commit (Codex review 4595045652):

- J2 (P1): a fast /resume can claim + finalize the COMPLETED response while the original
  request's pause branch is still awaiting `response.databasePromise`; the later
  unfinished-save then overwrites the completed content. Re-check the job is still paused on
  THIS generation's action (a claim leaves requires_action; a replacement bumps createdAt)
  before marking the row unfinished; fail open on a read error.

- J3 (P1): the tool-approval wiring (humanInTheLoop + PreToolUse hook + checkpointer) was
  applied to EVERY createRun caller when toolApproval.enabled, but the OpenAI-compatible and
  Responses controllers never inspect run.getInterrupt() or persist a pending action — an
  approval-gated tool would pause there with no approval surface or resume endpoint and the
  route would emit a normal final response / [DONE] with the tool call dangling. Gate the
  wiring on a new createRun `hitlCapable` flag, set only by AgentClient (chat + resume).

- J4 (P2): a stale-action 409 on submit returned without driving expiry, leaving the job
  requires_action with a dead action until the periodic sweeper ran — any attached SSE client
  got no terminal event and the stream appeared to hang. Extracted GenerationJobManager
  .expireApproval(streamId, actionId) (expire CAS + terminal SSE, shared with the sweeper) and
  call it from the resume route when the observed action is stale.

J1 (nested subagent approval controls not mounting while the details dialog is closed) is a
valid frontend issue in the deferred subagent-HITL path — tracked separately (replied on the
thread) since the fix touches the shared dialog primitive and needs UI verification.

Tests: HITL-gate both directions (run-summarization.test.ts); expire-on-stale-submit
(resume.spec.js); fast-resume unfinished-save guard predicate (jobReplacement.spec.js).

* 💄 style: Wrap captureAgents signature to satisfy prettier (CI lint)
2026-06-29 16:56:41 -04:00
Danny Avila
186b738d2d
🪟 fix: Re-measure Sidebar Chat List on Width Change to Fix Date-Group Spacing (#13981)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* 🪟 fix: Re-measure sidebar chat list on width change to fix date-group spacing

When the sidebar is expanded from a collapsed reload, virtualized rows first
measure mid-animation at a narrow width, so date-group headers wrap and cache an
inflated height. CellMeasurerCache(fixedWidth) keys heights by row, not width, so
the stale height persists once full width is reached — leaving gaps under headers.

Invalidate the measurement cache and recompute row heights whenever the measured
list width changes. Adds a Playwright mock e2e (seeds backdated convos across date
groups via a new db helper) that fails without the fix and passes with it.

* 🧪 test: Harden sidebar e2e (runtime-env path, midnight-safe seed, convo isolation)

Addresses Codex review on PR #13981:
- db.ts honors E2E_RUNTIME_ENV_PATH when locating the runtime Mongo URI.
- Seed timestamps anchor on local noon so the Today group stays in-day near midnight.
- Clear the shared user's conversations before seeding so later date-group headers
  are not pushed below the virtualized viewport by other specs' leftover chats.
2026-06-26 13:43:03 -04:00
Danny Avila
c948606a8c
🛗 perf: Fetch Pinned Agents Directly Past the Global Agents Map (#13972)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🚀 perf: Decouple Pinned Agents from Global Agents Map in Sidebar

Pinned/favorite agents in the sidebar waited for the full global agents map (useListAgentsQuery, which walks every pagination cursor) before rendering. In environments with many agents this left pinned items in a loading state even though their IDs were already known.

FavoritesList now fetches pinned agent IDs directly via getAgentById when the global map is still loading, and falls back to filtering only missing IDs once the map is available. The loading state tracks just the small set of pinned-agent queries instead of the entire catalog, so pinned agents appear as soon as their own data resolves.

Closes #13967

* 🩹 fix: Address Codex review on pinned-agent decoupling

- Stop caching the {found, agent} wrapper under the shared [QueryKeys.agent, id] key; direct fetches now return a plain Agent like useGetAgentByIdQuery, so opening/selecting a pinned agent within the stale window can no longer read a wrapper as an agent. Missing (404/403) agents are detected via the query error state.
- Gate the direct fetches on the agents endpoint being enabled, so pinned agents the endpoint list intentionally hides are not fetched, rendered, or cleaned up when the endpoint is disabled.
- Keep the loading skeleton while a direct fetch fails with a transient (non-404/403) error and the global agents map is still loading, so a pinned agent no longer disappears on a momentary 500/network error during startup.
- Remove the now-unused AgentQueryResult type.

* 🩹 fix: Address Codex round 2 on pinned-agent decoupling

- Keep the loading skeleton (not an empty/collapsed row) while the endpoints query is still loading. The endpoint gate previously treated the default empty config as disabled, so pinned-agent favorites rendered an empty row that could be measured and cached by the CellMeasurer before the config arrived. isAgentsLoading now stays true while isEndpointsLoading is true.
- Replace the blanket retry:false on direct pinned-agent fetches with a predicate that skips missing-agent (404/403) errors but still retries transient 500/network failures, restoring the prior default-retry resilience on the fast path.
- Add data-testid to the favorite skeleton and a regression test for the endpoints-loading window.

* 🛡️ fix: Don't delete pinned favorites on a global agents 403

GET /api/agents/:id runs the role-level AGENTS.USE check (checkAgentAccess) before the per-agent VIEW ACL, so a temporarily revoked role returns 403 for every agent. Because direct fetches now run while the agents map is undefined, treating those 403s as missing agents made the cleanup effect persist reorderFavorites and wipe all pinned agent favorites.

staleAgentIdsKey now returns early while agentsMap is undefined, restoring the original invariant that favorite cleanup only runs once the global map has loaded successfully (which also proves AGENTS.USE is granted). Rendering of pinned agents while the map loads is unaffected; only deletion is deferred.
2026-06-26 13:07:09 -04:00
Danny Avila
12fea693bb
🦥 perf: Lazy-Load Agent Version History in Editor (#13977)
Opening the agent editor fetched the full `versions` array (each a complete
config snapshot) alongside the agent, so agents with large histories were slow
to open. Version history is now loaded only when the user opens it.

- Add `getAgentWithVersionCount` (aggregation: version count, no versions array)
  and `getAgentVersions` data-schemas methods.
- `getAgentHandler` returns the version count without the heavy array; add
  `GET /agents/:id/versions` (EDIT-gated) for lazy retrieval.
- Add `useGetAgentVersionsQuery`; VersionPanel reads current config from the
  cached expanded query and fetches versions on open. Revert keeps the expanded
  cache and versions query in sync.
2026-06-26 12:19:54 -04:00
Dustin Healy
edeb1ecc2c
📋 fix: Route Clipboard Paste Through Upload Options (#13957)
* 🐛 fix: route clipboard paste through upload-option guards

Pasting a file skipped the composer's attachment guards, so unsupported types such as
csv and xlsx reached the provider as document blocks and were rejected. Paste, drag, and
the upload modal now share getViableUploadOptions to decide routing: zero viable
destinations shows a toast, one auto-routes, several open the upload-type modal.

* 🐛 fix: key ephemeral agent state by NEW_CONVO in upload-option flow

useFileUploadRouter writes ephemeral capability state under
`conversationId ?? Constants.NEW_CONVO`, but useUploadOptions and DragDropModal
read it under `?? ''`, so on a new conversation the option resolver missed
capabilities enabled by auto-routing. Align the reads on Constants.NEW_CONVO.

* 🐛 fix: harden paste upload routing for assistants, custom endpoints, and toasts

Bypass option resolution for Assistants endpoints on paste, matching drag-and-drop,
so non-image assistant uploads use the assistants upload path instead of mis-routing
to context or the unsupported toast. Honor a custom endpoint's configured
supportedMimeTypes for direct provider attach instead of hardcoding image and PDF.
Stop asserting upload success before validation runs; the single-route notice is now
an informational "Attached as text" for the text-extraction case only.

* 🐛 fix: refine paste upload routing for direct chats, custom endpoints, and disabled uploads

Restore Code Interpreter and File Search options in direct and ephemeral chats by
defaulting their permissions to allowed unless a saved agent omits the tool; selecting
one still enables the ephemeral capability. Treat a custom endpoint as broad provider
support only when its file config is permissive (matching the file picker), so an
inherited default no longer offers zip/audio/video for direct attach. Short-circuit
paste with the disabled-upload error before resolving options or opening the modal.
2026-06-26 12:00:07 -04:00
Danny Avila
0789a04d11
🪟 feat: Faithful Over-Window Context Estimate via Prune Mirror and Overhead Reserve (#13959)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
*  feat: Mirror send-path pruning in the over-window context estimate

For a snapshot-less branch whose tokens exceed the window, the send path
prunes oldest-first (getMessagesWithinTokenLimit), so the next call can sit
well under the window. The gauge previously clamped the full sum to 100%,
hiding that headroom. Add prunedBranchTokens — a newest->oldest walk that
keeps messages until the next would overflow the message budget (max minus the
summary baseline), mirroring the pruner — and use it on the estimate path in
place of the clamp. Approximation: omits the instruction/tool overhead and
tool-call pairing the real pruner accounts for (unknowable for a snapshot-less
branch); superseded by an exact snapshot once the branch is generated.

*  feat: Reserve cached instruction/tool overhead in the snapshot-less estimate

The over-window prune mirror and the gauge couldn't account for the fixed
instruction + tool-schema overhead the next call always sends, because a
snapshot-less branch has no breakdown. The backend already emits that overhead
in the ON_CONTEXT_USAGE breakdown, so cache it per agent/model (keyed
endpoint::model::agentId, already inclusive of tool schemas) from the live
usage events, then reserve it from the prune budget and add it to used so the
estimate is consistent with snapshots. Falls back to message-only until the
agent has run once this session. Surfaced as a System row in the estimate
breakdown.

* 🩹 fix: Address Codex review on the over-window estimate

- Key the overhead cache by agentId when present. useTokenLimits resolves an
  agent to its real provider/model, so the reader keyed `provider::model::agent`
  while the writer stored `agents::::agent` — a cache miss for the main agents
  case. Both sides now resolve to `agent:<id>` (non-agent configs: endpoint:model).
- Skip the overhead reserve when a summary baseline exists: computeSummaryUsedTokens
  already folds instruction/tool overhead into that marker, so adding it again
  double-counted on summarized branches.
- Collapse the breakdown's input/output/estimated rows into one pruned Messages
  row when over-window pruning ran, so the popover matches the gauge instead of
  summing to the discarded pre-prune history.
2026-06-25 17:12:53 -04:00
Peter
abf9fc307d
📇 feat: Agent Contact Visibility with Owner Fallback (#13663)
* Shared Contract

* Backend Resolution

* Frontend Display

* Contact Styling and more tests

* fix contact flicker when saving an agent

* fix display owner when contact deleted

* simplification of the last fixes

* github action fixes

* fixes failing tests

---------

Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
2026-06-25 15:58:15 -04:00
Danny Avila
376370d610
♻️ refactor: Compute Context Gauge Client-Side, Drop Projection Endpoint (#13953)
* ♻️ refactor: Compute Context Gauge Client-Side, Drop Projection Endpoint

The /api/endpoints/context-projection endpoint re-fetched a conversation's
messages from Mongo and re-tokenized them to project the context gauge for
snapshot-less branches. The browser already holds those messages and their
per-message tokenCounts, so this duplicated work on the request path (an
unbounded read + server-side BPE tokenization until it was later capped).

Move the snapshot-less estimate fully client-side, from the in-memory index:

- sumBranch accumulates an uncalibrated char/4 estimate (estTokens) for
  count-less messages (imports / pre-feature) under the same summary cutoff
- useTokenUsage folds estTokens (calibrated via the existing calibrationFamily
  ratio) into the existing fallback; known per-message counts render unchanged
- delete the endpoint, controller, rate limiter, route, the getMessageTextStats
  data-schemas method, and the data-provider surface (endpoint/key/type/service/query)

No DB read, no server tokenization, no rate-limit knobs; the gauge recomputes
reactively from the index. Net -793 lines.

* 🩹 fix: Count quotes and object-form content in client context estimate

Address Codex review on the client-side context estimate:

- messageChars now reads object-form content text (part.text.value), not
  only string text/think, so imported / pre-feature messages whose body
  lives in content parts are no longer estimated as zero.
- Count-less user messages include their merged quote excerpts in the
  estimate, mirroring what the send path prepends into the prompt.

* 🩹 fix: Cap over-window estimate and surface estimated tokens in breakdown

Address remaining Codex review on the client-side context estimate:

- Clamp the snapshot-less estimate's displayed usedTokens to maxTokens. The
  send path prunes an over-window branch before calling the model, so the
  gauge never actually exceeds the window; this avoids impossible values
  (e.g. 50k / 8k) without re-introducing client-side pruning.
- Surface the calibrated count-less estimate as its own "Estimated" row in
  the breakdown popover, so a branch of only count-less imported / pre-feature
  messages is no longer shown as Input 0 / Output 0 under a non-zero header.

* 🩹 fix: Refine client context estimate per Codex re-review

- Drop calibration from the snapshot-less estimate. The removed projection
  never actually calibrated (the client never sent a ratio), and a ratio
  inflated by provider-injected context over-estimates visible imported text.
- Exclude reasoning (think) / error parts from the estimate; the send path
  strips them, so they are not part of the next call's context.
- Fold quote text into the estimate even when a tokenCount is present, since
  the edit route recounts tokenCount from text only and drops the merged quote.

* 🩹 fix: Recount quoted user turns instead of topping up the stored count

The previous round added quote chars on top of a quoted message's stored
tokenCount, which double-counts the common (unedited) case where the count
already includes the merged quote prompt. Match the removed projection
instead: for quoted user turns, ignore the stored count and estimate the
full merged text. This both avoids the double-count and still corrects the
stale text-only count an edit leaves behind.

* 🩹 fix: Trust stored counts for quoted turns; count tool-call parts

- Quoted user turns: revert to trusting a present tokenCount. The send path's
  stored count already includes the merged quote (and any calibration), and
  the client's char/4 path is coarser, so recounting regressed normal turns.
  Only count-less messages estimate quotes from text.
- Count tool-call name/args/output for count-less assistant messages; the
  formatter sends them back as context, so omitting them under-reported
  imported branches with tool history.

* 🩹 fix: Exclude in-flight tail from estimate to avoid resume double-count

On resume the live path seeds liveTokens from the partial response and also
writes that content into the messages cache, where the count-less response
is estimated into estTokens too — double-counting the in-flight output on the
snapshot-less estimate path. sumBranch now exposes the tail message's own
estimate (tailEstTokens); the estimate path drops it while a stream is live,
so the in-flight response is counted once (via liveTokens). The breakdown's
Estimated row uses the same in-flight-adjusted value.

* 🩹 fix: Recount quoted user turns in context estimate (match send path)

A quoted user turn's stored tokenCount is unreliable for the gauge: a
text-only Save edit recomputes it from text alone, and the send path
(needsCanonicalTokenCount in agents/client.js) recounts the quote-merged
prompt every turn regardless of the stored value. Mirror that on the client
— estimate quoted turns from the merged text+quotes and ignore the stored
count — so snapshot-less branches don't under-report by the quote block.
Reverts the earlier "trust the count" assumption, which the server disproves.

* 🧹 chore: Route useResumableSSE diagnostics through the frontend logger

Convert the [ResumableSSE]/[Debug] console.log and console.error diagnostics
to the gated frontend `logger` (client/src/utils/logger), splitting the tag
from the message so object arguments are passed through as real args (logged
expandably, not stringified) and the logs stay tag-filterable and off the
production console unless explicitly enabled. All log statements preserved;
nothing removed.

* 🩹 fix: Prefer content over text when estimating count-less messages

A stopped agent response is saved with both a `text` field and a structured
`content` array, and the send path formats from content. messageChars
early-returned on `text`, dropping the content array (and the tool-call tokens
it carries) from the snapshot-less estimate — also making the tool_call
handling dead for such messages. Prefer content when present, fall back to text.
2026-06-25 15:29:31 -04:00
Danny Avila
5c5ef37e30
⬆️ chore: Migrate off deprecated @ariakit/react-core to @ariakit/react-components (#13940)
* ⬆️ chore: Migrate off deprecated @ariakit/react-core to @ariakit/react-components

@ariakit/react-core and its dependency @ariakit/core are deprecated (split into successor packages) and emit install-time warnings. @ariakit/react already ships the non-deprecated @ariakit/react-components transitively; the only direct use of react-core was the SelectRenderer deep import in ControlCombobox, which is now sourced from @ariakit/react-components/select/select-renderer (identical symbol and subpath). Both deprecated packages drop out of the lockfile and react-components dedupes to the single version @ariakit/react pins.

*  test: Resolve ESM-only @ariakit split packages in jest

@ariakit/react-components and its peers are ESM-only (type: module) and declare only an import export condition, so jest's CJS resolver can't load them when @librechat/client's CJS build requires SelectRenderer. Add a custom jest resolver that resolves these @ariakit/* split packages with the import condition, and extend transformIgnorePatterns so babel transpiles them to CJS. Applied to both the client and packages/client jest configs.
2026-06-24 23:13:57 -04:00
Danny Avila
397ddc5366
🧠 feat: Add Memory as an Agent Capability with Inline Tools and Ephemeral Badge (#13869)
* 🧠 feat: Memory Agent Capability with Inline Tools and Ephemeral Badge

Add `AgentCapabilities.memory`, which expands into the inline set_memory/delete_memory tool pair (mirroring the execute_code expansion via registerMemoryTools) when a run-level memoryAvailable gate holds: capability enabled, memory configured, MEMORIES.USE permission, and personalization not opted out. Surfaces the memory artifact as an attachment in the agents tool-end callback.

Adds the ephemeral path (TEphemeralAgent.memory, load/added agent tool injection), a fully-gated memory badge plus tools-dropdown entry, the agent-builder Memory toggle with form round-trip, and a mock e2e test asserting the badge reaches the request payload. Additive to and independent of the existing post-turn memory extraction agent.

* 🩹 fix: Address Codex review on memory capability (gating, validKeys, usage guard)

- Strip the memory capability from the served agents capabilities when memory is not configured/enabled, so the badge, tools dropdown, agent-builder toggle, and backend capability gate stay consistent instead of exposing an inert toggle on default installs (where MEMORIES.USE defaults true).
- Surface configured memory.validKeys in the inline tool definitions so the model is told the allowed keys up front, matching the runtime createMemoryTool schema.
- Append a strict explicit-request usage guard to the agent instructions when inline memory tools are registered, preserving the memory-agent's privacy behavior.
- Add AppService tests covering memory-capability stripping.

*  test: Update AppService capability snapshots for memory strip

AppService now strips the memory capability from the served agents defaults when no memory block is configured; update the spec's expected capability lists to defaultAgentCapabilitiesWithoutMemory for the no-memory-config cases.

* 🛡️ fix: Address Codex re-review on memory capability (round 2)

- Strip the memory capability from the FINAL served agents config, not just defaults; loadEndpoints reparses any endpoints.agents block, so memory was still exposed in that common shape (packages/data-schemas/src/app/service.ts) + regression test.
- Re-check the full memory gate (config, opt-out, MEMORIES.USE) inside handleTools before constructing set_memory/delete_memory, so an unsolicited tool call from a model/custom endpoint can't bypass the runtime gates (api/app/clients/tools/util/handleTools.js).
- Restore the persisted memory toggle for model-spec conversations via applyModelSpecEphemeralAgent (client/src/utils/endpoints.ts).
- Clear LAST_MEMORY_TOGGLE_ on logout and clear-all-chats so a stale memory preference can't leak across users on a shared browser (client/src/utils/localStorage.ts).

* 🧠 fix: Address Codex re-review on memory capability (round 3)

- Serialize set_memory writes and advance a running token total inside createMemoryTool, so parallel batched calls in one event-driven turn can't each pass the limit check against a stale total and collectively exceed memory.tokenLimit (packages/api/src/agents/memory.ts) + tests.
- Inject the keyed memory context (withKeys) instead of withoutKeys when the running agent has the inline memory capability, so delete_memory has a visible key to target (api/server/controllers/agents/client.js).

* 🔐 fix: Address Codex re-review on memory capability (round 4)

- Detect inline memory by tool NAME (set_memory/delete_memory) across an initialized agent's tools + toolDefinitions, since the 'memory' marker is expanded at init and the prior string check never matched; inject the keyed memory context for any primary OR sub-agent that carries the inline memory tools (api/server/controllers/agents/client.js).
- Enforce memory WRITE permissions in the inline tool gate: set_memory requires CREATE+UPDATE and delete_memory requires UPDATE (matching the REST memory routes), so a USE-only role can't mutate/delete memories via agent tool calls (api/app/clients/tools/util/handleTools.js).

* 🔒 fix: Address Codex re-review on memory capability (round 5)

- Gate inline memory registration (memoryAvailable) on the memory WRITE permissions (USE+CREATE+UPDATE), so a read-only-memory role no longer has set_memory/delete_memory shown to the model only for the runtime loader to refuse them (api/server/services/Endpoints/agents/initialize.js).
- Enforce the per-agent memory opt-in at execution: handleTools now refuses to construct set_memory/delete_memory unless the agent actually declared them (toolDefinitions/tools), blocking hallucinated/undeclared memory tool calls from mutating memory.
- Fail closed when getFormattedMemories errors with a configured tokenLimit, instead of writing as if storage were empty and bypassing the cap (api/app/clients/tools/util/handleTools.js).

* 🩹 fix: Address Codex re-review on memory capability (round 6)

- Fix a P1 regression from the prior round: the execution-context agent keeps the raw 'memory' capability marker (not the expanded set_memory/delete_memory names), so the opt-in check now matches the marker. This restores memory writes/deletes AND avoids hijacking an MCP tool that merely shares the set_memory/delete_memory name (api/app/clients/tools/util/handleTools.js).
- Count repeated set_memory writes to the same key as replacements, not additions, against tokenLimit — set_memory upserts, so a same-key rewrite swaps its prior token contribution instead of double-counting (packages/api/src/agents/memory.ts) + test.
- Gate the memory badge, tools dropdown, and agent-builder toggle on the full memory write permissions (USE+CREATE+UPDATE) via a shared useHasMemoryAccess hook, so a read-only-memory role no longer sees an enabled Memory control the backend would refuse to wire up.

* 🧷 fix: Address Codex re-review on memory capability (round 7)

- Recognize inline memory across both execution-context agent shapes: initializeAgent now sets a LibreChat-only memoryToolsRegistered flag on the InitializedAgent, and the opt-in/detection checks accept that flag OR the raw 'memory' marker. Fixes memory failing for processAddedConvo agents (which store the initialized config, marker already expanded) while staying MCP-name-collision-safe (api/app/clients/tools/util/handleTools.js, packages/api/src/agents/initialize.ts, api/server/controllers/agents/client.js).
- Scope keyed memory context to memory-enabled agents only: useMemory now returns both keyed and unkeyed contexts, and buildMessages injects the keyed one (memory keys + token metadata) only to agents that can call delete_memory, while the primary/post-turn path keeps the unkeyed values — so a primary without memory tools no longer sees memory keys it doesn't need.

* 🔏 fix: Address Codex re-review on memory capability (round 8)

- Enforce memory size limits on inline writes: createMemoryTool now rejects keys over 1000 chars and values over memory.charLimit, matching the REST memory routes, so an inline-memory agent can't persist blobs the memory UI/API would reject (packages/api/src/agents/memory.ts, api/app/clients/tools/util/handleTools.js) + test.
- Recheck the agents 'memory' endpoint capability at execution time, so a stale/hallucinated set_memory/delete_memory call can't mutate memory after an admin removes the capability while the agent document still carries the marker (api/app/clients/tools/util/handleTools.js).

* ♻️ refactor: Move inline-memory backend logic into packages/api + share memory load

Workspace boundary: the inline-memory gating/detection logic that had crept into /api now lives in packages/api/src/agents/memory.ts (TS), with /api kept as thin wrappers.

- Add agentHasInlineMemoryTools, isMemoryToolAllowed, and buildInlineMemoryTool to packages/api; handleTools.js now calls buildInlineMemoryTool instead of constructing/gating the tools inline, and client.js imports agentHasInlineMemoryTools instead of redefining it.
- Optimize repeated memory loads: getRequestMemories memoizes getFormattedMemories per request (WeakMap keyed by req), so the run's memory-context load and every memory-enabled agent's set_memory token-usage load share a single DB fetch instead of one per agent.

* 🧠 fix: Invalidate request memory cache after inline writes

Inline set_memory/delete_memory now invalidate the request-scoped
getFormattedMemories cache on a successful write, so a later tool round
in the same response is seeded with the post-write usage total instead
of the stale pre-write one (multi-round writes no longer collectively
exceed tokenLimit, and a set after a delete is not over-counted). The
within-round sharing across multiple memory-enabled agents is preserved.

* 🧠 fix: Persist memory capability on saved agents; honor registration flag

- Add Tools.memory to the v1 systemTools allowlist so filterAuthorizedTools
  no longer silently drops the memory marker when an agent with the Memory
  capability is created/updated/duplicated through the builder (previously
  the capability only worked for ephemeral chats, not persisted agents).
- agentHasInlineMemoryTools now honors an explicit memoryToolsRegistered
  boolean before falling back to the raw `memory` marker, so an initialized
  config whose registration was denied (memoryAvailable false) is not given
  keyed memory context just because the marker survives in tools.

* 🧩 fix: Bring memory tool to parity with other ephemeral tools

- Add `memory` to the model-spec schema/type and honor `modelSpec.memory`
  in both ephemeral paths (load.ts, added.ts) and the frontend spec
  application, so admins can pre-enable Memory from a model spec exactly
  like webSearch/fileSearch/executeCode.
- Add LAST_MEMORY_TOGGLE_ to the timestamped-storage cleanup list so stale
  per-conversation memory toggles are purged on startup like the others.
- Hide the agent-builder Memory toggle for users who disabled memory in
  personalization (memories === false), mirroring the chat badge's opt-out
  gate, so the setting isn't shown as inert/misleading.

*  test: Cover memory in applyModelSpecEphemeralAgent spec defaults

Update the exact-object assertions to include the new `memory` field and
add positive coverage that `modelSpec.memory` maps to the ephemeral
agent's `memory` flag. Fixes the shard 2/4 failure from 672a03b05.
2026-06-24 17:14:13 -04:00
Marco Beretta
61016e328a
🔄 feat: Continue Shared Conversations as Personal Copies (#13714)
Adds a "Continue this chat" button to the shared conversation view that forks
the shared conversation into a new conversation owned by the viewer and opens it
to continue (issue #13001).

- POST /api/share/:shareId/fork, gated by requireJwtAuth, the fork rate
  limiters, and the canAccessSharedLink ACL (view access = fork access).
- forkSharedConversation clones from the anonymized getSharedMessages payload,
  so only share-visible data is copied.
- Strips file ids from cloned files/attachments so a fork grants no more file
  access than viewing the read-only share, and honors the global shared-file
  kill switch via the snapshotFiles option.
- Reduces the clone to the viewer's active branch, located by its index in the
  shared payload (shared ids are re-anonymized per request and createdAt can
  collide, while the payload order is stable).
- Resolves config/retention, persists, and reads back under the requesting
  user's tenant, not the share owner's; canAccessSharedLink also falls back to
  a system-wide share lookup so cross-tenant public shares resolve (ACL still
  enforced under the share's own tenant).
- Resolves a usable endpoint/model from the viewer's models config instead of
  hard-coding OpenAI, so deployments without OpenAI can send the first message.
- Routes the fork's 401s (logged-out or cold-loaded viewers) through login,
  including when the refresh itself is rejected for a stale session.
- Hides the Temporary Chat toggle once a conversation has a real id, and
  portals the share-settings theme/language dropdowns above the dialog.

Rebased onto dev; collapses the share-fork feature and its review fixes into a
single commit.
2026-06-24 16:27:01 -04:00
Marco Beretta
b84e26671e
🕒 feat: Track Terms Acceptance Timestamp (#10810)
* feat: add terms acceptance timestamp tracking and migration script

* feat: update migration script to use countUsers method for user count

* Update config/migrate-terms-timestamp.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: enhance terms acceptance response to include acceptance timestamp

* fix: make terms acceptance idempotent and fail migration on partial errors

Preserve the original termsAcceptedAt on repeat accepts within a terms
cycle so retried or duplicate requests no longer overwrite the first
acceptance time. Exit the migration script with a non-zero status when
any per-user update fails so partial failures are not reported as
successful.

* style: fix import ordering in data-provider mutations

* refactor: record terms acceptance atomically to preserve first-accept time

Replace the read-then-write in acceptTermsController with a single
atomic acceptTerms method that conditionally stamps termsAcceptedAt via
an $ifNull aggregation update. This removes the TOCTOU window where two
concurrent first-time accepts could overwrite the earlier acceptance
timestamp, while still preserving an existing timestamp and backfilling
legacy accepted users.

* fix: run terms timestamp migration under system tenant context

Wrap the count, cursor scan, and per-user updates in runAsSystem so the
tenant isolation plugin does not throw under TENANT_ISOLATION_STRICT or
scope the cross-tenant migration to a non-existent tenant, matching the
other maintenance migrations.

* fix: guard terms backfill against concurrent acceptances

Add the missing-timestamp predicate to the per-user updateOne filter so
a user who accepts through the API between the cursor read and the write
keeps their real acceptance time instead of being overwritten with
createdAt. Track modified vs skipped so the summary reflects skips.

* fix: scope terms backfill to still-accepted users

Add termsAccepted: true to the per-user updateOne filter so a reset that
clears acceptance between the cursor read and the write is not re-stamped
with createdAt, which would otherwise poison the next acceptance cycle
through the $ifNull preserve in acceptTerms.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-24 16:26:42 -04:00
Danny Avila
9e74cc0e57
v0.8.7 (#13907)
Some checks failed
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
2026-06-24 14:49:32 -04:00
Danny Avila
189cb245c2
🫥 fix: Hide Quote Popup When Selection Collapses Silently (#13936)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
The "Add to chat" popup lingered over an empty caret after a selection collapsed through a path that fires no mouse/key event — most often a streaming markdown re-render replacing the selected text node. The selection state only updated on mouseup/dblclick/keyup/scroll/resize, so a silent collapse left the button stranded ("showing up with nothing selected").

Add a `selectionchange` listener that hides the popup the instant the selection collapses or empties. It only hides, never shows, so an in-progress drag-select still won't flicker the popup.

Adds an e2e that collapses the selection without a mouse event and asserts the popup disappears.
2026-06-24 11:24:42 -04:00
Danny Avila
ef1ee6ee16
🪤 fix: Guard Prompts Popover Against Empty Result Keyboard Navigation (#13931)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🛡️ fix: Guard Prompts and Mention popovers against empty-result navigation

* 🛡️ fix: Prevent Tab default and clear stale filter on empty popover close
2026-06-23 23:14:24 -04:00
Danny Avila
1662adc581
📺 feat: Google URL Context Param with Native YouTube Video Understanding (#13924)
*  feat: Add Google url_context Param with Native YouTube Video Understanding

Mirror the web_search grounding wiring for a new Google/Gemini `url_context`
model param (resolves to the native `urlContext` tool). When enabled, YouTube
URLs in the latest user message are injected as Gemini video parts (fileData),
since the URL Context tool does not support YouTube.

* 🎞️ fix: Provider-aware YouTube injection limits for url_context

Address Codex review on the YouTube video-understanding path:
- Cap injected YouTube parts per request by provider/model (Vertex: 1; Gemini
  Developer API: 10 on 2.5+, 1 on earlier models) so multi-link messages cannot
  exceed the provider limit and get rejected.
- Set a video/mp4 mimeType on Vertex YouTube fileData (matching Vertex samples);
  the Developer API still omits it.

* 🧩 fix: Round-trip url_context for Google-compatible custom endpoints

Add url_context to openAIBaseSchema so the per-chat value persists for custom
endpoints configured with customParams.defaultParamsEndpoint: 'google', matching
how web_search is already picked there.

* 🚦 fix: Gate url_context tool to Gemini 2.5+ models

Per Google's URL Context supported-models list (2.5+/3.x only), skip the native
urlContext tool on earlier models (debug-log + no-op) instead of sending it and
triggering a provider 400. This also gates the coupled YouTube video-understanding
injection to 2.5+, since it keys off the resolved urlContext tool.

* ✂️ fix: Strip YouTube URLs from urlContext text; keep url_context out of OpenAI schema

- Remove url_context from the shared openAIBaseSchema (revert): it is Google-only
  and would otherwise leak as an unsupported param to OpenAI/Azure/OpenRouter
  requests. On Google-compatible custom endpoints url_context is enabled via admin
  addParams/defaultParams, same as web_search.
- When injecting YouTube video parts, strip the matched YouTube URLs from the prompt
  text so the urlContext tool (which reads URLs from text and cannot fetch YouTube)
  does not consume its URL budget on them. Non-YouTube URLs are left intact.

* 🎯 fix: Refine url_context model gating and YouTube injection edges

Address Codex round 4:
- Exclude non-text modality variants (image/live/tts) from URL Context support,
  mirroring the Google tool-combination modality exclusion.
- Use the resolved run model (model_parameters.model) for YouTube injection limits
  instead of the saved base model.
- Strip only the YouTube links actually routed to video (id-aware); keep over-limit
  links in the text so the model can still reason about them.
- Keep timestamped YouTube links (?t=/&start=) in the text so the moment cue survives.
- Recognize youtube-nocookie.com/embed links.

* 🎚️ fix: Exclude audio Gemini variants + preserve pre-id YouTube timestamps

Address Codex round 5:
- Add `audio` to the url_context modality exclusion so audio-only Gemini variants
  (e.g. gemini-2.5-flash-preview-native-audio-dialog) skip the tool instead of 400ing.
- Detect YouTube timestamps anywhere in the matched URL (incl. before `v=`, e.g.
  watch?t=90&v=<id>), so timestamped links are kept in the prompt text as intended.
2026-06-23 22:42:06 -04:00
Oliver777int
6934d07066
🌵 fix: Align Mention Empty Result Behavior With Skills Command (#13928)
Co-authored-by: oliver.olsson <oliver.olsson@zeekrtech.eu>
2026-06-23 21:10:06 -04:00
Dan Lew
f09a1ad7fc
🧹 fix: Order Enabled Guard After Config Expansion in useGetAgentByIdQuery (#13927)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Otherwise, it's possible for a config to override the `isValidAgentId` check.

Without that check, it's possible to query `getAgentById()` with a blank `agent_id`,
which can result in polluting the `QueryKeys.agent` cache with a full list of agents
(instead of just a single agent result).
2026-06-23 17:15:44 -04:00
Danny Avila
562bd8ec5f
🐛 fix: Prevent Infinite Render Loop on Code-Execution File Preview (#13922)
* 🐛 fix: Prevent Infinite Render Loop on Code-Execution File Preview

Loading a conversation that contains a large (>1MB) code-execution
office file crashed the whole app with React error #185 ("Maximum
update depth exceeded") on hard refresh.

Root cause (client-only): the terminal-write effect in
useAttachmentPreviewSync writes the resolved preview record back into
messageAttachmentsMap with a fresh object identity on every run, and
`attachment` is in the effect's dependency array. useAttachments
re-derives `attachment` ({...db, ...liveEntry}) with a new identity on
every map write, so once polling resolves (pending -> ready on a loaded
conversation) the effect ping-pongs forever:
setAttachmentsMap -> re-derive -> effect -> setAttachmentsMap.

Only files large/slow enough to defer extraction are persisted at
status: 'pending', which is why small documents never triggered it.

Fix: an idempotency gate that bails before setAttachmentsMap when the
merged attachment already carries the resolved status/text/textFormat/
previewError. The write happens once and then settles.

Tests:
- useAttachmentPreviewSync.loop.spec.tsx wires the real
  useAttachments -> hook feedback to reproduce the loop (verified to
  throw #185 without the gate, settle with it).
- e2e/specs/mock/attachment-preview-loop.spec.ts loads a conversation
  with a pending code-exec attachment whose preview resolves ready and
  asserts the app does not crash.

Closes #13916

* 🔧 feat: Make Office Preview Extraction Cap Configurable (default 2MB)

The inline code-execution preview extraction ceiling was a hardcoded 1MB
constant (MAX_TEXT_EXTRACT_BYTES). Office/text artifacts over that skip
the inline preview and resolve to "Preview unavailable" (download-only).

Make it configurable via FILE_PREVIEW_MAX_EXTRACT_BYTES and raise the
default to 2MB so larger documents get an inline preview out of the box.
The rendered HTML remains independently capped at MAX_TEXT_CACHE_BYTES
(512KB), so image-heavy files over that still fall back to the existing
"preview too large" banner rather than rendering unbounded output.

- resolveMaxTextExtractBytes(env) parses the override, falling back to
  2MB on missing/non-numeric/non-positive values (warns on invalid).
- Documented in .env.example next to the other file-size limits.
- Unit tests cover default, valid override, fractional flooring, and
  invalid fallback.

* 🐛 fix: Guard sub-byte preview cap from flooring to zero

A fractional FILE_PREVIEW_MAX_EXTRACT_BYTES in (0, 1) passed the
positive-number check then floored to 0, making MAX_TEXT_EXTRACT_BYTES
zero and treating every non-empty artifact as oversized. Floor first,
then require the result to be >= 1 byte before accepting it; otherwise
fall back to the 2 MB default. Adds coverage for the sub-byte case.

*  test: Make exported-ceiling assertion env-independent

The "exported ceiling" assertion compared MAX_TEXT_EXTRACT_BYTES to a
literal 2 MB, but that const is initialized from
FILE_PREVIEW_MAX_EXTRACT_BYTES at module load — so the suite would
falsely fail when run with the override set. Assert the export tracks
resolveMaxTextExtractBytes(env) for the current environment instead; the
undefined-case test continues to pin the 2 MB default.
2026-06-23 16:34:43 -04:00
Danny Avila
f616a58fb7
🖱️ fix: Summon Quote Popup on Double-Click Word Selection (#13923)
* 🖱️ fix: Summon Quote Popup on Double-Click Word Selection

Chromium commits a double-click word selection on the `dblclick` event, after `mouseup` has already read a still-collapsed range, so the "Add to chat" popup never appeared for double-click selections. Listen for `dblclick` in addition to `mouseup`/`keyup`.

Adds an e2e covering a native double-click word selection (measured-coordinate dblclick exercises the real browser path, unlike the programmatic-Range helper).

* 🎯 test: Target Reply Text Node in Double-Click Quote E2E

Walk to the text node containing the needle (not the first text node in .message-render, which may be a select-none screen-reader/model-label header) and measure the needle's first character, so the native double-click lands on the reply word rather than metadata.
2026-06-23 15:52:34 -04:00
Danny Avila
f14309e087
🪶 refactor: Ground Default Model Spec Selection in Conversation Recency (#13915)
Resolve the new-chat default spec from the most recent conversation setup
(LAST_CONVO_SETUP_0) instead of reconstructing intent from accumulated
cross-endpoint history. Removes hasStoredModelValue, hasStoredPrefixValue,
hasStoredModelSelection, the sticky LAST_SPEC read, the nested
resolveSoftDefault closure, and the duplicated prioritize/modelSelect branches.

Fixes the soft default being dropped on New Chat ("Select a model") when its
preset endpoint sits outside modelSpecs.addedEndpoints alongside a custom
endpoint: a model lingering in LAST_MODEL for that endpoint no longer
suppresses the soft default.

Clear All Chats now also clears LAST_SPEC/LAST_MODEL/LAST_TOOLS so a new chat
afterward cleanly returns to the soft default. Adds the cross-endpoint unit
case, a clearAllConversationStorage test, and a cold-load e2e regression test.
2026-06-23 15:49:04 -04:00
Danny Avila
33d7b0070c
🌍 i18n: Update translation.json with latest translations (#13914) 2026-06-23 11:14:38 -04:00
Danny Avila
e807c63d5d
🔐 fix: Gate Shared Startup Config By Link Access (#13897)
* fix: gate shared startup config by link access

* fix: satisfy shared config CI checks

* fix: align shared config client types

* fix: reject expired shared link access
2026-06-23 08:28:37 -04:00
Marco Beretta
ebb4f15dbe
⌨️ feat: Keyboard Shortcuts (#12425)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* feat: add useKeyboardShortcuts hook and showShortcutsDialog atom

Implements the core keyboard shortcuts hook with 11 shortcuts:
- General: new chat, focus input, copy last response
- Navigation: toggle sidebar, model selector, search, settings
- Chat: stop generating, scroll to bottom, temporary chat, copy code

Also adds the showShortcutsDialog atom to control dialog visibility.

Closes #3664

* feat: add KeyboardShortcutsDialog component

Renders a modal dialog listing all available keyboard shortcuts
grouped by category (General, Navigation, Chat). Features:
- Platform-aware key labels (⌘ on Mac, Ctrl on others)
- Clean kbd-style key badges with subtle shadows
- Grouped sections with separators
- Sticky footer with shortcut to open the dialog itself
- Single close button, Escape to dismiss

* feat: integrate keyboard shortcuts into Root layout and account menu

- Mount useKeyboardShortcuts and KeyboardShortcutsDialog in Root.tsx
  via a KeyboardShortcutsProvider wrapper (only renders post-auth)
- Add 'Keyboard Shortcuts' menu item with Keyboard icon to the
  account settings popover for discoverability

* chore: add data-testid to model selector button

Adds data-testid="model-selector-button" to the model selector
trigger for reliable DOM targeting by keyboard shortcuts and tests.

* i18n: add keyboard shortcuts localization keys

Adds 12 new com_shortcut_* translation keys for the keyboard
shortcuts feature: group labels, action labels, and dialog title.

* style: fix keyboard shortcuts dialog dark mode

Replace token-based dark mode styling with explicit white-alpha
values for kbd badges, borders, and separators:
- Kbd: dark:bg-white/[0.06] dark:border-white/[0.08] dark:shadow-none
- Separators: dark:border-white/[0.06]
- Dialog border: dark:border-white/[0.06] dark:shadow-2xl

Ensures the key badges blend naturally into the dark surface
instead of appearing as harsh bright rectangles.

* feat(shortcuts): add definitions for 8 new keyboard shortcuts

Add shortcut definitions and localization keys for:
- Upload file (Cmd/Ctrl+Shift+U)
- Toggle right sidebar (Cmd/Ctrl+Shift+R)
- Regenerate response (Cmd/Ctrl+Shift+E)
- Edit last message (Cmd/Ctrl+Shift+I)
- Scroll to top (Cmd/Ctrl+Shift+↑)
- Archive conversation (Cmd/Ctrl+Shift+A)
- Delete conversation (Cmd/Ctrl+Shift+Backspace)

Addresses #3664

* feat(shortcuts): implement handlers for all new shortcuts

New handlers:
- Upload file: triggers attach-file button click
- Toggle right sidebar: clicks parameters-button
- Regenerate response: clicks regenerate-generation-button
- Edit last message: finds last user-turn and clicks edit button
- Scroll to top: scrolls main[role=main] to top
- Archive conversation: calls archive mutation + navigates to new chat
- Delete conversation: calls delete mutation + navigates to new chat

Improvements:
- Use getMainScrollContainer() helper targeting main[role=main]
  instead of fragile class-based selectors
- Use data-testid selectors instead of aria-label substring matching
  for stop-generation and model-selector buttons
- Use id-based selectors (button[id^=edit-]) for edit buttons
- Add isEditing guard to skip shortcuts when user is typing in
  inputs, textareas, or contentEditable elements
- Refactor handler from if/return chain to switch statement for
  cleaner flow control

* fix(shortcuts): increase dialog scroll height for expanded shortcut list

With 20 shortcuts across 3 groups, the previous 480px max was tight.
Increase to 560px / 70vh so all shortcuts are visible without
excessive scrolling.

* refactor(shortcuts): use data-testid selectors for reliable targeting

Add data-testid="nav-settings" to the Settings menu item in
AccountSettings so the open-settings shortcut no longer relies on
fragile text-content matching ('Settings' but not 'Keyboard').

* refactor(shortcuts): two-column layout for shortcuts dialog

Split the shortcuts dialog into a two-column grid layout:
- Left column: General + Navigation groups
- Right column: Chat group (which has the most shortcuts)

Reduces vertical height so the full list is visible without scrolling.
Widen dialog to max-w-4xl (w-11/12) to accommodate both columns.
Simplify Kbd/group styling for cleaner visual density.

* refactor(shortcuts): adjust padding in KeyboardShortcutsDialog content

* feat(shortcuts): customizable keyboard shortcuts with recorder UI

Add per-shortcut overrides stored in localStorage, a recorder component
for capturing new key combos with conflict detection, and a per-row
edit/reset affordance in the shortcuts dialog.

* test(shortcuts): fix specs broken by keyboard shortcut hooks

- ExpandedPanel: add customShortcuts atom to the store mock so
  useShortcutDisplay/useShortcutAriaKey can read state
- AttachFileMenu: update queries to the new 'Attach Files' aria-label
- Button (Generations): wrap renders in RecoilRoot now that the
  component reads shortcut state

* feat(shortcuts): add panel/submit/bookmark/continue/read-aloud shortcuts

- Wire stop, regenerate, continue, and read-aloud handlers to existing
  buttons via data-testid, fixing handlers that previously queried
  selectors with no matching DOM nodes.
- Add data-testid='nav-panel-${id}' to expanded sidebar nav buttons so
  the panel-opener shortcuts can target them.
- Add new shortcut definitions and handlers: submitMessage,
  bookmarkConversation, continueResponse, readAloudLastResponse, and
  the open* panel openers (assistants, agents, prompts, memories,
  parameters, files, bookmarks, MCP).
- Drop the toggleRightSidebar shortcut — there is no right sidebar to
  toggle in this codebase.
- Refresh the KeyboardShortcutsDialog layout and ShortcutRecorder for
  the new groups, tighten ShortcutKeyCombo styling, and surface the
  shortcuts hint chips in the account menu.

* chore(shortcuts): remove unused translation keys

Drop com_shortcut_dialog_subtitle, com_shortcut_not_set, and
com_shortcut_reset_aria — no remaining references in the codebase.

* fix(shortcuts): resolve keyboard shortcut and footer regressions

- Guard the temporary-chat toggle so the shortcut mirrors the UI, only
  toggling when the conversation has no messages and is not submitting.
- Stop Ctrl/Cmd+Enter from double-submitting: the main chat textarea
  already submits via its own handler, and submit is blocked from
  unrelated inputs while still working in the chat box.
- Ignore repeated keydown events (e.repeat) so held keys no longer
  re-run toggles or destructive actions.
- Scope archive/delete shortcuts to the conversation in the active
  route using useMatch, preventing mutations of a stale background
  conversation on non-chat routes.
- Keep the recorder conflict controls clickable by including the whole
  editing row in the outside-click containment check.
- Restore privacy policy and terms of service links on public share
  pages via an opt-in Footer prop.
- Expand the sidebar before activating panel shortcuts so they are
  visible on mobile, and avoid toggling an already-active panel.

* fix(shortcuts): reject bare non-printable shortcut bindings

A recorded non-printable key (Tab, Enter, Backspace, Delete, arrows,
Space) with no Cmd/Ctrl/Alt was treated as valid, so it could be saved
and then hijack navigation or fire destructive actions since the global
handler preventDefaults it outside text inputs. Require Shift at minimum
for these keys, which keeps Shift+Escape (focusChat) valid while
rejecting bare single-key bindings.

* style: fix import order drift across keyboard shortcut files

* fix(shortcuts): guard actions behind dialog and resolve reset conflicts

- Ignore global shortcut actions while the shortcuts dialog is open
  (except the toggle that closes it), so a combo like delete/archive
  can no longer fire on the conversation behind the modal.
- When resetting a shortcut to its default, unbind any other action
  whose custom binding collides with that restored default, so Reset
  after a Replace can't leave two rows sharing one binding with one
  action unreachable.

* fix(shortcuts): keep attach menu button accessible name stable

The shortcut pass changed the attach menu button's aria-label from the hardcoded "Attach File Options" to localize('com_sidepanel_attach_files') ("Attach Files"), which changed its accessible name and broke the provider-file e2e specs that locate it by name. Restore the original label and keep only the added aria-keyshortcuts.

* fix(shortcuts): gate temporary chat toggle to chat routes

The Root-level listener runs on non-chat routes (search, settings, panels) where the last loaded conversation may be empty, so Ctrl/Cmd+Shift+T could flip the hidden isTemporary state without the TemporaryChat control being visible. Require an active chat route (routeConvoId) before toggling.

* test(shortcuts): align attach menu spec with button accessible name

The attach menu button's aria-label was restored to "Attach File Options" (matching dev and the provider-file e2e specs), so update the unit test's button queries from /attach files/i to /attach file options/i. All 26 cases pass.

* fix(shortcuts): target conversation bookmark and reveal search panel

- Bookmark: query the unique #bookmark-menu-button so the shortcut
  bookmarks the current conversation. The previous
  querySelector('[data-testid="bookmark-menu"]') matched the sidebar
  tag-filter button first (same testid, earlier in the DOM), toggling
  the filter instead of bookmarking.
- Focus search: activate the conversations panel before focusing, since
  the search input only mounts there and the sidebar renders just the
  active panel. Route through the nav-panel-conversations button (the
  listener is outside ActivePanelProvider) and settle before focusing,
  so Ctrl/Cmd+/ works from any panel.

* fix(shortcuts): preserve footer links, cross-platform bindings, modal guard

- restore unconditional legal footer links (drop showLegalLinks gate)
- keep untouched platform's default when customizing a binding
- round-trip bindings whose key is the plus character
- suppress global shortcuts while any modal dialog is open
- tag read-aloud test id only on assistant turns

* fix(shortcuts): include non-Radix dialogs in the modal guard

The guard only matched Radix dialogs via data-state="open", missing
Headless UI dialogs (e.g. the redesigned Settings modal) that render
role="dialog" without data-state. Iterate all dialog/alertdialog nodes
and treat one as open unless it is inert or data-state="closed", which
also avoids false positives from always-mounted inert panels.

* fix(shortcuts): gate temporary chat toggle behind TEMPORARY_CHAT permission

* fix(shortcuts): only prevent native key event when shortcut action runs

* fix(shortcuts): rebind temporary chat, open settings without toggling menu, release no-op keys

* fix(shortcuts): confirm conversation delete, use clipboard fallback, add tests

* fix(shortcuts): navigate to new chat after keyboard-confirmed delete

* fix(shortcuts): copy last response via message button, guard unavailable controls

* fix(shortcuts): keep custom Enter-based submit bindings working in the composer

* fix(shortcuts): restrict shift-only bindings to safe keys

* fix(shortcuts): submit custom Enter chords in the composer without inserting a newline

* fix(shortcuts): block global shortcuts while a menu overlay is focused

* fix(shortcuts): rebind archive off the browser-reserved Ctrl+Shift+A

* fix(shortcuts): honor submitMessage overrides in the composer
2026-06-22 17:02:46 -04:00
Danny Avila
77983dbd42
🐳 feat: Bundle Admin Panel in Docker Compose Stacks (#13876)
* 🐳 feat: Bundle ClickHouse Admin Panel in Docker Compose Stacks

* chore: route admin-panel image through Scarf gateway

* chore: route admin-panel image through Scarf gateway (deploy-compose)

* 📝 docs: Add Admin Panel to README Features

* 🏷️ chore: Rename Admin Panel Container to admin-panel

* 🔐 fix: Fall Back Admin Panel SESSION_SECRET to CREDS_KEY

* 📝 docs: Reference Open-Source code-interpreter Repo in README
2026-06-22 16:59:08 -04:00