mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-01 19:41:32 +00:00
4998 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
06bf324cf0
|
🛤️ feat: Per-Agent Code Execution Routing With Stateful Session Scopes (#14848)
* feat: route code execution per agent profile * chore: sort execution profile imports * test: preserve stateful environment literal types * fix: isolate stateful code environments by user * fix: preserve per-agent code routing end to end * fix: route code priming by execution profile * fix: isolate code profile lifecycle state * fix: preserve mixed-profile code resources * fix: complete stateful skill routing |
||
|
|
d411512a98
|
⬆️ chore: Bump @librechat/agents to v3.6.0 (#14890)
* ⬆️ chore: Bump `@librechat/agents` to v3.6.0 Bumps the pin in `api` and `packages/api` from `^3.5.1` to `^3.6.0`. The caret on `^3.5.1` cannot cross the minor, so both manifests and the lockfile need the explicit bump. v3.6.0 contains three changes over v3.5.1, all additive: - `fix: Close Subagent Child-Graph Run Steps` — subagent child graphs run via `workflow.invoke()` outside `Run.processStream`, so the terminal sweep never reached their steps. They now close on both the success and error paths, which is what makes `on_run_step_closed` reliable for subagent tool cards. - `fix: Restore Run Steps Across Process Resumes` — open run-step lifecycle state is now persisted in LangGraph checkpoints, so a step opened by one process closes correctly after a resume on another. - `feat: route code execution per agent profile` — new optional `codeSessionKey` partition for code-session ids and file refs. No breaking changes: every new field on the public type surface is optional, and the package's own dependency set is unchanged between the two versions (verified against the registry), so the lockfile diff is limited to the `@librechat/agents` entry itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🔒 chore: Sync `bun.lock` with the agents v3.6.0 bump `bun.lock` still recorded both workspace requirements and the resolved package as `@librechat/agents@3.5.1`, which no longer satisfies `^3.6.0`, so `bun install --frozen-lockfile` would reject the committed state. `bun install --lockfile-only` cannot run in this environment: bun stores no integrity for the `xlsx` URL dependency and therefore re-fetches `cdn.sheetjs.com`, which the sandbox network policy denies (403 on CONNECT). The entry was updated directly instead, which is exact here because the package's dependency graph does not move between the two versions: its `dependencies`, `peerDependencies` and `optionalPeers` at 3.6.0 are identical to 3.5.1 (checked against the registry), so only the version, the resolution id and the integrity hash change. The integrity matches the one npm resolved into `package-lock.json`, and the two existing `@librechat/agents/*` hoisting overrides stay valid because the dependency set they resolve is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
cf30661d20
|
🗂️ feat: Display Chat Title in Tab Setting (#14881)
* Add setting to toggle chat title in browser tab Adds a General > Layout toggle controlling whether the browser tab shows the conversation title or the app title, defaulting to on so existing behaviour is unchanged. The tab title had no single owner: several call sites assigned document.title directly. Route the chat-title writers through a shared setDocumentTitle helper so the setting applies consistently to sidebar navigation, search results, SSE title generation, title polling, and the share view. * test: cover document title settings * fix: address chat title review findings * fix: keep app title for new chats * fix: distinguish new chat title placeholder |
||
|
|
db675209e8
|
🧩 refactor: Extend Step Status To Remaining Cards; Separate Cancelled From Failed (#14873)
* 🧩 fix: Extend Explicit Step Status To Specialized Tool Cards Follow-up to #14871, which covered the generic tool card and the five sharing `useToolCallState` but left the cards carrying their own cancellation logic on the whole-message heuristic. Each needed its own treatment rather than a forwarded prop: - `RetrievalCall` is the exact analog of the reviewed shape. - `WebSearch` feeds `effectiveProgress` into `finalizing` and `complete` as well, so forcing it terminal naively would strand a cancelled final search as "finalizing" forever. A closed step now settles on its own status instead of waiting for the submission to end. - `OpenAIImageGen` resolves through `computeCancelled`, which now short-circuits on explicit status ahead of both the agent and legacy paths — the legacy path has no submitting signal at all, so this is the first real stop signal it has ever had. In all three the status is authoritative on its own terms: never gated on the output-parsing error check, a closed step forces progress complete so it cannot keep animating, and `failed` reports as an error even when the output text looks benign. The prior heuristic remains the fallback for messages saved before `on_run_step_closed`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧩 fix: Resolve Subagent Card State From Closed Step Status `SubagentCall` uses a tri-state (`running`/`cancelled`/`finished`) built from the subagent's own phase envelopes plus `!isSubmitting`, so it could not distinguish "this subagent was stopped" from "the parent stream ended for some other reason" — the distinction `on_run_step_closed` exists to make. A closed step now resolves the tri-state directly: `cancelled` maps to the cancelled state, `completed` and `failed` both count as finished, and `failed` additionally reports as an error. The phase-and-isSubmitting inference remains the fallback for messages predating the event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🩹 fix: Give `failed` Its Own Terminal Path In Each Card Codex found the same mistake in three places: I resolved `cancelled` carefully and let `failed` fall through into a success or in-progress path. Forcing a closed step's progress to 1 made that visible rather than latent. - `OpenAIImageGen`: `hasError` did not account for the status, so a failed generation rendered and announced as a finished image. Updated to match every other card. - `WebSearch`: a failed close left `complete` false and dropped the card into the streaming branch, shimmering forever. `error` now folds into the `cancelled` early-return, which is where an errored search has always gone — rather than inventing a failure UI this component has never had. - `RetrievalCall`: the live region did not consult `errorState`, so a failed retrieval announced "retrieved files" while the card showed a failure. Announces the failure first now, mirroring the same fix made to `ToolCall` in #14871. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧯 fix: Separate Cancelled From Failed, Stop Terminal-Step Timers Codex round 2. Two distinct classes, plus the same leak in already-merged code. Rendering — cancellation and failure were collapsed: - `OpenAIImageGen` fed `cancelled` into `ProgressText`'s `error` prop, so a user-stopped generation read "image generation failed" visually while the live region announced "cancelled". Meanwhile a `failed` close reached neither consumer, since `computeCancelled` returns false for it — adding the status to `hasError` alone changed nothing. `ProgressText` now takes `cancelled` alongside `error`, and both the card and the live region resolve the two states independently. Timers — masking a hook's output does not stop it: - `useProgress` keeps a 200ms interval alive whenever its input is below 1, and a closed step usually never receives the completion that would raise it. Every site that masked the result now passes the terminal value in instead, so a closed card schedules nothing. - The agent-style image ticker had the same problem one layer up: its interval effect ignored the close entirely and its cleanup keyed on `cancelled`, so a step closed as `failed` mid-submission kept rerendering for up to ~50s. Both effects now observe the close. - `ToolCall` and `useToolCallState` carried the identical masking from #14871; fixed here rather than left as a known leak in merged code. Also dropped a JSDoc line that narrated its own assignments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * ✅ fix: Failure Outranks Cancellation; Update The Test That Encoded The Conflation Codex round 3, plus the CI failure it explains. - Failure now takes precedence over cancellation in both `ProgressText` and the image-gen live region. The legacy inference folds `hasError` into its cancellation signal, so checking `cancelled` first relabelled a genuine failure on an older saved message as a user stop — a regression introduced by the previous commit. - `RetrievalCall` passed `finishedText={intent ?? 'Retrieved files'}` regardless of state, so a cancelled retrieval read "Retrieved files" beside a cancellation icon while the live region announced "Cancelled". The finished label is now cancellation-aware. - `OpenAIImageGen.test.tsx` asserted `data-error === 'true'` for a heuristically cancelled step — the exact conflation this work removes. Updated to the new contract and extended with cases for explicit cancellation, explicit failure with benign output, and failure precedence under the legacy inference. Verified locally for the first time in this work stream: building the `@librechat/client` and `data-provider` workspaces made the client suite runnable here. 15/15 in the image-gen spec, 851/851 across `Content` and `hooks/SSE`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧹 style: Drop Narrating Comment From Cancellation Test The test name and the `data-cancelled` / `data-error` expectations state the contract on their own; the JSDoc above them only restated it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * ⚖️ fix: Explicit Cancellation Outranks Parsed Errors; Force Progress Synchronously Codex round 5, both findings consequences of round 3's fixes. - Failure precedence was applied unconditionally, so a step explicitly closed as `cancelled` whose output happens to be error-formatted — aborting a tool can itself produce one — reported failure despite an authoritative status saying otherwise. Precedence is now scoped to the legacy inference, which is the only path that folds `hasError` into its own cancellation signal. Explicit cancellation wins. - Passing 1 into `useProgress` stops its interval but does not make the returned value 1 on that render: the hook settles through 0.99 and a 200ms timeout. For a step closing while mounted, that window rendered a failed retrieval as "Searching files" and left a completed one shimmering. Both halves are needed — pass 1 in to stop the timer, mask the result so the terminal value is observable immediately — in `RetrievalCall`, `ToolCall`, `useToolCallState` and `OpenAIImageGen`. Regression test added for the cancellation-outranks-error case. 16/16 in the image-gen spec, 851/851 across Content and hooks/SSE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🛑 fix: Explicit Subagent Cancellation Outranks Child Error Phase Codex round 6. - `SubagentCall` checked `hasError` before cancellation, so a subagent explicitly closed as `cancelled` whose last live envelope carried the `error` phase — which aborting a child can produce — rendered "Agent errored" instead of cancelled. Scoped the same way as the image path: the live error phase is suppressed when the authoritative close says cancelled. - Removed a narrating comment in `WebSearch`; `isClosed`, `effectiveProgress`, `finalizing` and `complete` name the flow. 852/852 across Content and hooks/SSE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧽 style: Sweep The Narrating Failure Comments Codex flagged two; the same one-line paraphrase of `runStepStatus === 'failed'` had been copied into five files, so all five are removed rather than leaving three to surface next round. The named `hasError` / `errorState` / `error` booleans carry it. The remaining comments in these files explain non-obvious behavior rather than restating code — why `useProgress` needs both the terminal argument and the mask, why an errored web search renders as nothing, and which precedence applies to the legacy inference versus an explicit close. 852/852 across Content and hooks/SSE. Typecheck baseline is now 1 error (`useRum.ts`, unrelated) rather than 487, since building the `@librechat/client` workspace resolved the module errors that were masking it — identical with this diff and at the branch point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a23ab9d16e
|
💄 style: Align the Overflow Menu's Icons and Ease the Assistant Header (#14888)
`DropdownPopup` already wraps every item icon in `mr-2 size-4`, but the header menu's icons carried `icon-md mr-2` of their own — so the margin was applied twice and an 18px icon sat in a 16px box, leaving the column ragged between items whose icons happened to render at different intrinsic sizes. The icons now match the box they are given and let the wrapper own spacing. Separately, the assistant name sat directly on the first line of its own response. A small bottom margin separates the two without opening a gap. |
||
|
|
e7fa54dacf
|
📱 feat: Give the Mobile Nav the Whole Screen (#14849)
* 📱 feat: Give the Mobile Nav the Whole Screen The drawer was `min(85vw, 380px)` with the 52px icon rail inside it, so the conversation list got well under half the screen while ten unlabelled glyphs held a permanent column. The drawer now takes the viewport. Neither side needs a width literal any more: the panel is `fixed`, so `w-full` is the initial containing block, and the chat pane's `translateX(100%)` is self-referential and survives rotation. The shared transition moves to a constant — the two elements must stay frame-locked or the seam shows mid-animation. Drop the rail on mobile by not rendering `ExpandedPanel` rather than branching inside it, so desktop keeps an untouched file. Its four jobs move to a drawer header (panel switcher, account, close) and a bottom bar. The switcher doubles as the drawer title, answering "where am I" and "take me elsewhere" with one control, and lists panels as labelled rows. Search and new chat were both in the top corner — the two most frequent actions in the hardest place to reach one-handed. They move to a bottom bar built as a flex footer, not an overlay, so the virtualized list shrinks around it and can never be occluded. The backdrop is gone: at full width it can never be tapped, and `Root` already marks the covered pane `inert`. That makes the header's close button the primary dismissal, so it keeps `CLOSE_SIDEBAR_ID`, which `OpenSidebar` focuses after opening. Reset the drawer closed once per mobile mount. `sidebarExpanded` persists, and at full width a stale open state would launch into the nav rather than the conversation. Conversation rows revealed their overflow menu on hover, which touch does not have, leaving it reachable only on the active row. Touch now gets a cheap always-visible trigger that mounts the real menu already open, rather than mounting six mutations per overscanned row. * 🩹 fix: Address Codex Findings on the Full-Width Mobile Nav The panel switcher was unreachable. `DropdownPopup` portals to `document.body`, where `usePopoverZIndex()` hands it 50 outside a dialog — behind the opaque full-screen drawer at 110 — so none of its destinations could be selected. Render it inside the drawer instead; nothing between the trigger and the drawer root clips overflow. The drawer's z-index moves to a named constant carrying that reasoning. Panel keyboard shortcuts stopped working on mobile. They locate a panel by its rail button, read `aria-pressed`, then click it, and the rail no longer exists — so Agents, Prompts, Memories and the rest silently no-opped on a narrow window or a tablet with a keyboard. Hidden persistent targets keep that contract without reviving the rail. Routing the shortcuts through `useActivePanel` instead would mean hoisting `ActivePanelProvider` above `SidebarChatProvider`, which exists to keep panel changes from re-running `useChatHelpers`. Only available links render, so a shortcut for a panel this endpoint lacks still correctly does nothing. The persisted-drawer reset ran after the first paint, so a reload with the drawer open showed the nav covering the app and then animated it shut — the exact state it was meant to prevent. `atomWithLocalStorage` already accepts a normalizer, so the value is corrected during atom initialization and the closed state reaches the first paint. Drops the effect entirely. Note the normalizer also rewrites the stored value, so opening the drawer on a phone leaves that browser's desktop sidebar collapsed until toggled. * 🩹 fix: Address the Second Codex Round on the Full-Width Nav The conversation row's overflow menu was unreachable on mobile for the same reason the panel switcher was: `ConvoOptions` portals to `document.body`, where `usePopoverZIndex()` gives it 50, behind the drawer at 110. It now portals only off mobile — on desktop the sidebar is in normal flow, so portaling still buys escape from the list's clipping. The touch trigger also lost its own first tap. Touch browsers focus a button mid-tap, and the row's `onFocus` sets `hasInteracted`, which swaps the trigger for `ConvoOptions` before the click can land. Moving to `pointerdown` runs the handler before the swap. Crossing into the mobile breakpoint left the drawer open. The persisted value is normalized when the atom initializes, which covers loading on a phone, but narrowing a window or rotating a tablet has no such moment and an expanded desktop sidebar became a drawer covering the app. Collapse on the transition specifically, so the initial mobile paint still comes from the normalizer rather than an effect. The new spec pins the tap contract: it fires only `pointerdown`, so a click-based handler fails it. * 🩹 fix: Address the Third Codex Round on the Full-Width Nav The touch options trigger handled only `pointerdown`, so assistive tech, voice control and keyboard activation — which dispatch `click` with no preceding pointer event — did not reach it, and the click bubbled to the row and navigated away instead. It now handles pointer, click and Enter or Space through one handler. The two paths cannot double-fire, since `pointerdown` removes the button before a click could follow. The breakpoint reset still animated. Correcting it in an effect meant the first render after crossing into mobile painted the drawer open with the conversation translated fully offscreen, then moved both back over 300ms. The closed state is now derived during the transition render itself, and the effect only commits it. That derivation has to be shared: `UnifiedSidebar` draws the drawer while `Root` translates the pane, and both read the atom independently, so either one deciding alone would disagree with the other for that frame. Both now read through `useSidebarState`. * 🩹 fix: Restore Portaling and Scope the Drawer's Close Identity Revert the mobile menus to `portal={true}`. The premise behind rendering them in place was wrong: the drawer's z-index only ranks it inside `Root`'s `relative z-0` stacking context, so it cannot occlude a popup portaled to `document.body` regardless of the values involved. `ConvoOptions` has always portaled from inside this drawer and has always worked. Rendering in place cost real breakage: the row sits under the nav's `overflow-hidden` and a virtualized list, and the drawer's transform makes it the containing block for fixed descendants, so menus near a list edge were clipped and their rename, archive and delete actions unreachable. Scope the drawer's close button to the open state. It stays mounted while closed so the drawer can slide, and a translated element still counts as visible, so anything probing for `close-sidebar-button` found a control sitting off-viewport — which is what stalled the mobile visual specs. The rail this replaced only published that id while expanded; match it, and keep the closed drawer out of the tab order. * 🩹 fix: Let an Ordinary Click Open the Conversation Menu The trigger committed on `pointerdown`, so beginning a vertical scroll on an ellipsis opened that conversation's menu before the browser could tell a tap from a swipe. That handler only existed to beat a race of our own making: `hasInteracted` is hover- and focus-driven, which is meaningful on a pointer device but not on touch, where focus lands mid-tap — swapping the trigger for `ConvoOptions` while the finger was still down. Key the swap to the menu's own state on touch and the race disappears, so a plain click suffices. The browser already withholds a click until a press resolves as a tap, and synthesises one for keyboard and assistive-technology activation, which the `pointerdown` path had to special-case separately. Also correct the drawer z-index comment, which described the opposite of the layering the code settled on and would have led the next caller back into the clipping bug, and restore `aria-keyshortcuts` on the new-chat button so its binding stays discoverable. * 🩹 fix: Let Escape Leave the Menu Before the Drawer Menus opened from the drawer portal out of it, so their Escape still reached the drawer's document listener and collapsed the whole thing rather than the level the user meant to leave. Those menus unmount when closed, so their presence in the document is the signal to stand down. Also restore the toggle binding on the close control. It is the only close affordance while the drawer is open — the header's `OpenSidebar` is inside the inert, translated chat pane — so assistive technology had no way to discover the shortcut from there. * 🩹 fix: Only Treat an Open Menu as Reason to Keep the Drawer The Escape guard matched any `[role="menu"]` in the document, but not every menu unmounts when closed — the account menu stays mounted and merely `hidden`. Once it had lazily loaded, a closed menu would have suppressed Escape for the drawer permanently. Match only menus that are actually open. * ✅ test: Pin the Ariakit Closed-Menu Contract The drawer's Escape guard stands down only for menus that are actually open, which depends on Ariakit keeping a closed menu mounted and marking it `hidden` rather than unmounting it — the account menu behaves this way and would otherwise suppress Escape for the drawer permanently. Exercised against the real library rather than a mock, so a change in that behaviour fails here and points at the guard. * 🩹 fix: Keep the Row's Menu Mounted Once It Has Been Opened Keying the swap to `isPopoverActive` meant dismissing the menu unmounted `ConvoOptions` immediately, destroying Ariakit's own button — its final-focus target — mid-close. The lightweight trigger that took its place is a different node and never received focus, so a keyboard or assistive-technology user was dropped to the document instead of returning to the control they opened. Once a row's menu has been opened, keep the real one. Rows the user never touched still mount nothing, which was the reason for the trigger. * 🩹 fix: Complete the Retained-Menu Path for Touch Rows Three gaps in the retained-menu approach, all reachable. `hasOpenedMenu` was only set by the touch trigger, but the active row already renders the real menu and never passes through it. A row opened while active and later demoted would swap its focused button for a new node and drop focus — the same defect the retention was added to prevent. Recorded on every opening instead. The retained button then stayed invisible: `ConvoOptions` reveals its trigger on hover or focus when the row is neither active nor open, and touch has neither, so an interacted row was left with an invisible hit target. Kept visible on small screens. The touch trigger also restated the shared control's sizing, rounding and text treatment by hand, losing the focus ring, transitions and disabled handling that come with it. Composed from `Button` with only the local sizing retained. * ♻️ refactor: Give the Row's Overflow Control One Owner Five review rounds in this file each fixed something the previous fix introduced — trigger swap, activation path, scroll-versus-tap, focus return, retention completeness. The cause was structural rather than any one mistake: two controls can represent a row's menu, `ConvoOptions` and the cheap placeholder that stands in for it, and the rules they must agree on were spread across four separate expressions and a button, so each repair taught one of them something the other never learned. `ConvoActions` now settles them together — which control renders, when the real one becomes permanent, how it stays visible without hover, and how activation is claimed — with the reasoning for each recorded where the decision is made, including why a plain click is the right event and what breaks if a press is claimed earlier. Behaviour is unchanged; this is the same set of rules in one place. `Convo` keeps the open state, which it needs to suppress row navigation, and now passes a single `onOpenChange` rather than driving the swap itself. * 🩹 fix: Reveal the Real Menu Trigger on Touch and Recheck the Drawer Default The conversation menu has two triggers — the shift-held variant and the Ariakit button used the rest of the time — and only the first was taught to stay visible without hover. The second restated the same class string by hand instead of sharing it, so the earlier fix silently missed the trigger that actually matters. It now composes the shared string, which is why the two could disagree at all. Separately, the sidebar default is captured when the store module is evaluated, and `atomWithLocalStorage` only ran its normalizer when a saved value existed. A first visit that loaded wide and narrowed before the app mounted — a login screen being resized — therefore kept `true` with nothing to correct it, and `useMediaQuery` now resolving on the first render means the breakpoint guard sees no transition either. Normalize the default at initialization as well; callers without a normalizer get the identity function, so nothing else changes. * ✅ test: Cover the Normalized Default in `atomWithLocalStorage` Normalizing the default reaches every atom built with the helper, so the cases worth pinning are the ones where a normalizer exists and could move an untouched default: no normalizer, one that accepts the default — the shape the speech-engine atoms have — one that rejects it, and a persisted value, which must still be normalized as before. * 🩹 fix: Carry the Search Text Across a Breakpoint Change Moving search into the drawer's bottom bar left it mounted in two places — the list on a pointer device, the bottom bar on touch — so crossing the breakpoint mid-search destroys one instance and builds another. The field seeded its text to an empty string and never read the stored query, so the results stayed filtered by a term the box no longer showed, with no clear affordance to undo it. Seeded from the query instead, along with the clear button's state. * 💄 style: Settle the Drawer's Panel Switcher and Bookmark Filter The switcher's chevron trailed the panel name instead of sitting on the edge, so the control read as text with an arrow stuck to it rather than a menu spanning the header. The label now takes the slack. Moving search to the bottom bar also left the bookmark filter alone on a row of its own above the list, with nothing to sit beside. It moves next to the Chats heading, matching the Projects heading that already keeps its actions there, and the row disappears on mobile rather than lingering with one icon in it. `ChatsHeader` gains a trailing slot for that, so section actions have a home instead of a floating row. * 💄 style: Match the Bookmark Filter to the Section Actions The bookmark control was built for the row it used to share with the search field — 36px, `rounded-lg`, a larger icon — so beside a section heading it read as a different kind of control to the Projects actions sitting one row above it. Both now draw from one recipe, at every width rather than only where the move exposed it, so the two headings cannot drift apart in size, radius or hover treatment. * 🩹 fix: Cancel the Search Debounce the Field Leaves Behind The debounced commit writes to shared search state, so a pending timer outlives the instance that scheduled it. Mounting the field in two places made that reachable: crossing the breakpoint mid-keystroke destroys the list's field and builds the bottom bar's, and the departing timer would then reinstate a query the replacement had already edited or cleared. Clearing the field had the same hole within a single instance. Cancelling needs a debounce that is stable for the field's lifetime. A memo rebuilt on dependency changes leaves the previous instance's timer running past the cancel meant to stop it, and cancelling on that rebuild discards live keystrokes instead — so the handlers are read through a ref and the debounce is built once. Renames the spec, since hydrating the arriving instance and silencing the departing one are two halves of the same remount. * 🩹 fix: Hand the Uncommitted Query to the Arriving Search Field Cancelling the departing field's debounce stopped it overwriting a query the replacement had edited, but it also stranded the simpler case: a user who crosses the breakpoint and then just stops typing. The commit that would have published their query died with the instance that scheduled it, so the arriving field showed text the list was not filtered by and `isTyping` was never cleared — the loading state has no other way out while `debouncedQuery` and `query` disagree. The arriving field now takes the handoff, scheduling the commit itself when it mounts with an uncommitted query. Reading that at first render keeps it to the moment of the swap, so a real edit still wins. * 💄 style: Give Section Actions a Home in the Button Recipe The two sidebar headings shared their icon-button appearance through a feature-local class string, which is the shallow wrapper the styling rules warn about: sizing, radius, hover and focus ring are reusable appearance decisions, so they belong to the shared primitive where future theme and accessibility work will reach them. `sectionAction` and an `iconSm` size carry that recipe now, and the call sites keep only their layout. The drawer's panel switcher gets the same treatment for a sharper reason than consistency: its hand-written class string had no focus-visible state at all, so keyboard focus on the drawer's primary navigation control was invisible. Composing the shared ghost recipe restores the ring and transition, leaving only the row-filling layout local. `buttonVariants` returns unmerged recipe output, so every call site wraps it in `cn` — a spec pins that, since forgetting it silently reinstates whichever base utility the variant meant to override. * 🩹 fix: Publish the Search Field's Pending Query When It Leaves Cancelling on unmount assumed a replacement field would always arrive to inherit the query, so the fix grew a second mechanism to hand it over. The bottom bar disproves the assumption: switching panels drops the search entirely, leaving `query` set, `debouncedQuery` stale and `isTyping` on with nothing left to clear it. Flushing replaces both mechanisms. It publishes the pending commit rather than discarding it, so a field that leaves without a successor still settles the state it changed. And because a flush is synchronous with the unmount, it lands before any edit the replacement makes — which is what the cancel was for, so nothing is given up. Also normalizes the default on the parse-error path in `atomWithLocalStorage`: unparseable storage falls back to the same module-time default as a missing key, and only the missing-key path was re-checking it against the current viewport. |
||
|
|
edc6cf5936
|
🩹 fix: Stop Archived and Shared Chats Dialogs Crashing on Open (#14886)
* fix: stop the virtualized data table looping on render Opening Archived chats or Shared chats with 50 or more rows threw "Too many re-renders". DataTable passed an inline getItemKey to useVirtualizer, and virtual-core lists that option among the deps of its getMeasurementOptions memo, whose onChange notifies. getVirtualItems() is read during render, so every render built a new closure, notified, and dispatched a render-phase update on the component that was still rendering, until React gave up at 25 passes. It only fired past the 50-row virtualization threshold, which is why both dialogs looked fine while empty. Memoize getItemKey and estimateSize so their identity tracks their inputs. DataTable.spec had mocked @tanstack/react-virtual away, attributing the same error to jsdom, which hid this from CI. Keep that mock, since its row assertions need every row rendered, and add a spec that drives the real virtualizer and fails without the fix. Also restyle both dialogs, which is what made them look unfinished: - add the 19 keys these components pull from @librechat/client but the app locale never defined, so the empty state rendered com_ui_no_data verbatim - rename Shared links to Shared chats, matching the sibling Archived chats - transparent table with a rounded hover highlight painted on the cells, since border-radius does not apply to a table row, which needs separated borders - row height 56 to 40, dividers dropped, skeletons follow the same height - row hover uses surface-secondary-alt: plain surface-secondary is 247 against a 255 dialog in light mode and reads as nothing - row action buttons use surface-hover-alt, because surface-hover is also 227 in light and would vanish against the row highlight - drop the focus ring from the dialog containers and stop Shared chats seating focus in its search field, so neither flashes an outline on open - narrow both dialogs and let the table height follow its content * Fix compact row actions and selection count * fix: update selected count translation test to match interpolated output |
||
|
|
0a2f59ab86
|
📋 fix: Stop the Copy Button From Clobbering the Clipboard (#14876)
* fix: keep the copy button from clobbering the clipboard Join only the non-empty TEXT parts instead of deciding the separator from the raw content index, so a tool call or error after the last visible text part can no longer append a stray trailing newline. Pasting that into a terminal submitted the command on its own. Skip the copy entirely when a message serializes to nothing, such as an error-only response, rather than overwriting the clipboard with an empty string and flipping the button to its copied state. * fix: stop copy controls from reporting a copy that never happened The copy hook now reports whether it wrote to the clipboard. The shared link announcement and the redirect URI and resource URL toasts fire on that result instead of on every click, so a click that copies nothing no longer tells the user, or a screen reader, that the value was copied. Give CopyButton a disabled prop and use it for the shared link and the MCP redirect URI, whose values are empty for a beat while the share id and the created server id resolve, so the control is not a dead target. * fix: honor clipboard write failures and disable copy with nothing to copy copy-to-clipboard reports whether the write landed. Return that instead of assuming success, so a browser that refuses the fallback no longer produces a copied checkmark, an announcement, or a toast. Export hasCopyableText and use it to disable the copy action in HoverButtons and MinimalHoverButtons. An error-only or tool-only response rendered an enabled control that could not do anything, which keyboard users had no way to tell apart from a working one. Reveal's copy test relied on the old always-succeed path, since jsdom has no execCommand for the real module to call. It now mocks the write and covers both outcomes. * fix: derive copy availability from the text the hook would copy hasCopyableText checked raw text parts while the hook copies the cleaned value, so a part holding only citation markup counted as copyable and then declined the copy, leaving the enabled-but-dead control this was meant to remove. Both now run buildClipboardText, so the predicate cannot drift from the guard. That needs search results to stay accurate, which HoverButtons does not have, so copyability is decided in useMessageActions and useMessageHelpers next to the hook call that owns those inputs, and passed down. * perf: stop scanning a response that is still streaming for copyability The copyability memo invalidated on every streamed chunk and rebuilt the clipboard text from the whole accumulated response, so a long answer paid quadratic cumulative work for a copy button that stays hidden until the stream ends. The hooks now hand down a getter and HoverButtons short-circuits it while isActiveStreamingMessage is true, so nothing is scanned before the control can be used, and the result is still memoized once the response settles. * style: drop the narrating comment on the copyability gate * chore: sort client imports |
||
|
|
b2016898a5
|
🎙️ fix: Prefer Compatible Formats for External STT Recording (#14864)
* feat(stt): Prefer ogg over mp4 for openai compatible providers and firefox * test(stt): cover recording MIME priority --------- Co-authored-by: Pascal Garber <pascal@artandcode.studio> |
||
|
|
d513e55a62
|
📥 feat: Download Code Blocks as Files (#13715)
* ✨ feat: Add Download button for code blocks Adds a Download button next to Copy code in both the code block header bar and the floating bar, saving the block content as a file whose extension is inferred from the fenced-block language hint. - getCodeBlockFilename() maps language names to extensions (python → code.py), passes extension-like hints through unchanged (tsx → code.tsx), and falls back to code.txt; reuses the existing triggerDownload() util for the blob download and URL revocation. - useDownloadCode mirrors useCopyCode (focus restore, 3s reset timer). - DownloadButton mirrors CopyButton (icon animation, tooltip in icon-only mode, aria-label reflecting current state). Fixes #13470 * Address review: alias extensions + hide floating download on error - Map alphanumeric language aliases (python3, nodejs, node, golang) to their real extension so they download as code.py/.js/.go instead of code.python3 etc. — the bare-hint fallback previously passed them through verbatim. - Gate the floating-bar Download button behind error !== true, matching CodeBar, so error blocks (e.g. token-balance JSON) don't expose a download action once the header scrolls out of view. * style: show only Run Code with a label in the code block header Download and Copy code now render as icon-only buttons with tooltips, so the header reads as one labelled action plus two secondary icons. Below the md breakpoint Run Code drops its label too, leaving all three as icons on mobile. * refactor: extract the shared code bar action button CopyButton and DownloadButton were identical apart from the icon and their default labels. Both now wrap a single ActionButton that owns the class recipe, the icon swap, the label swap and the icon-only tooltip, so the two cannot drift apart as the bar evolves. The public props of both components are unchanged. * fix: keep the Run Code tooltip when its label is hidden Below the md breakpoint Run Code renders as a bare terminal icon, so it needs the same hover description Download and Copy already carry. The button is now always wrapped in the tooltip anchor rather than only in the explicit iconOnly mode. * docs: drop the em dash from the extension map comment --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> |
||
|
|
1d789c41a5
|
🧩 fix: Normalize MCP UI Resource Rendering (#14868)
* fix: normalize MCP UI resource rendering * fix: filter unsupported MCP UI resources * fix: preserve MCP UI marker examples * fix: handle MCP UI resource edge cases * fix: harden MCP UI marker sanitization * fix: scope MCP UI marker sanitization * fix: parse MCP UI marker contexts * fix: align MCP UI sanitizer parsing * fix: match MCP UI renderer syntax * fix: align blockquote marker spans * fix: decode MCP UI text node sources * fix: sanitize nested subagent markers * fix: bound MCP UI sanitizer traversal * fix: keep MCP UI marker mapping linear * style: sort security patch imports * fix: harden nested MCP UI sanitization * fix: mirror citation cleanup for MCP UI markers * fix: clean decoded citation markers * fix: clean assembled citation markers * fix: align MCP marker sanitization with rendering * fix: match persisted MCP marker render paths * fix: preserve highlighted citation boundaries * fix: align MCP markers across content renderers * fix: preserve citation renderer boundaries * fix: match legacy thinking trim semantics |
||
|
|
eb3b353712
|
📡 fix: Publish App-Level MCP Tool Catalogs Without a Reserved Revision (#14858)
* 📡 fix: Publish App-Level MCP Tool Catalogs Without a Reserved Revision Shared MCP servers advertised no tools to agents, so every turn failed with "configured to use MCP tools, but none are available" (#14857). `replaceAppServerTools` returned false whenever a publication carried no `publicationRevision`, but only `refreshChangedTools` reserves one. Every other app-level publisher — the first-connect snapshot, reinitialization, on-demand catalog reads, the retained-catalog restore — was silently dropped. The agent path fails closed on that drop: the skipped write returns null, so reinitialize yields no tools and the turn 503s. Startup hid it. `connectAppServers()` defers the initial refresh and calls `refreshToolList()` itself, which does reserve, so a boot that reaches its MCP servers looks healthy. Only a lazily created app connection — the server not yet up when LibreChat boots, a dropped connection, a cold cache — takes the unreserved path. `ConnectionsRepository` now reserves before its own `tools/list`, matching the list_changed path; a failed reservation publishes unordered rather than failing the connection. Publishers with no pre-fetch reservation point have already fetched by the time they reach the cache, so they take the next revision at write time instead of being discarded. `mergeAppTools` still publishes at revision 0 and stays deferential to a live catalog. * 📡 fix: Bind App Catalog Ordering to the Fetch That Produced It Addresses review feedback on the previous commit: allocating a revision at publish time lets a slow `tools/list` of an old catalog outrank a newer one that reserved after it started, and it would let the retained-catalog restore — which republishes deliberately pre-mutation data — outrank a live catalog. Ordering now travels with the data. `fetchToolsSnapshot` reserves before its first page and returns the ticket on the snapshot, so every app-level publisher reads the revision belonging to the read it is publishing rather than one allocated at an unrelated moment. `fetchOrderedToolsSnapshot` carries the refresh's revision when it defers to one, since that is whose catalog it returns. With the reservation at the single point where app-level tools are read, no publisher can forget it, so `replaceAppServerTools` goes back to refusing an unordered write: a publication that lost its ticket fetched at an unknown time and cannot be ordered. A failed reservation is reported as `orderingUnavailable` rather than swallowed, which keeps the list_changed path retrying instead of publishing a catalog that would be silently dropped, and leaves inspection unaffected by a transient cache outage. `MCPServerInspector.getToolFunctions` becomes `getToolCatalog` and returns the revision with the tools, so there is no variant that quietly discards ordering. * 📡 fix: Retry an Empty App Catalog That Could Not Reserve Ordering Review follow-up. The no-tools-capability branch destructured the reservation result and dropped `orderingUnavailable`, publishing without a revision when the revision store was transiently unavailable. That write is rejected in silence, and unlike the snapshot branch this one returned without reaching `refreshToolList()`, so whatever the server last advertised stayed in place until the connection was recreated or the cache expired. Both branches now route an unreservable catalog through the same retry path. * 📡 fix: Serve Tools Whose Shared Catalog Write Could Not Be Ordered Review follow-up. Only the shared catalog write needs ordering; the tools themselves were just read from the server and are correct to serve. Discarding them because the write could not be ordered is what turns a cache failure into a server that appears to have no tools at all, which is the reported symptom. `updateMCPServerTools` now returns the tools it built when the publication has no reserved revision, instead of null. A superseded write still discards — there another replica holds something newer. Reinitialization also asks the connection to republish under backoff when its snapshot could not reserve ordering, so the shared catalog does not stay cold until something else triggers a refresh. * 📡 fix: Surface a Discarded App Catalog Instead of Debug-Logging It #14857 went a release without a diagnostic because the only trace of a dropped app-level catalog was a debug line no deployment runs. Operators saw agents fail every turn with nothing in the logs to explain it, and the reporter had to read the source to find the cause. A publication discarded because it cannot be addressed or ordered means this server's tools are unavailable to every agent that selected them, and serving an unpublished catalog means every request re-fetches it. Both are warnings now. A superseded write stays at debug: concurrent replicas produce it routinely and the winner already holds newer tools. Tests pin the level, so a later refactor cannot quietly make the failure silent again. * 🧪 test: Pin the Reinitialize Path's Catalog Ordering Reinitialization is the path an agent falls back to when the shared catalog is cold, so it is where #14857 surfaced as "configured to use MCP tools, but none are available". Nothing pinned that it forwards the ordering its snapshot was fetched with, nor that it asks the connection to republish a catalog it could not order. Both assertions fail against the pre-fix source. |
||
|
|
a2ad0aa0c8
|
🤐 feat: Allow Promptless Sends When Files Are Attached (#13717)
* ✨ feat: Allow sending file attachments without a text message When an agent asks the user to upload a document, the user could attach the file but still had to type a placeholder message ("OK", "Here is the file") before the send button enabled and the submit guard let the message through. Attachments now count as submittable content: - New isSubmittableMessage(text, fileCount) util: non-whitespace text OR at least one attached file. - ask() in useChatFunctions uses it instead of bailing on empty text, so an empty draft with attached files submits. - SendButton receives the attached file count and enables accordingly. - ChatForm only marks the text field as required when no files are attached, so react-hook-form validation no longer blocks handleSubmit. Submitting an empty draft with no attachments is still rejected at all three layers. Fixes #13646 * Address review: support replayed file-only turns + drop empty vision text - ask(): count replayed attachments (overrideFiles) in the submittable check and skip it entirely for regenerate, so a file-only message can be regenerated or saved-and-resubmitted instead of being rejected as empty. - formatVisionMessage(): omit the text content part when the message text is empty. Anthropic rejects empty text content blocks with HTTP 400, and an empty block adds nothing for other providers; image-only sends now format cleanly. Added formatMessages tests for with-text and image-only (Anthropic + other) cases. * Address review: keep attachment-only turns valid for providers, answer mode, and titles - formatMessage: substitute minimal text when a user turn carries files but no inline content, so Anthropic does not reject an empty user message for RAG or code-environment attachments. - assistants chatV1: send the same stand-in for attachment-only Threads messages, which reject an empty body. The persisted message keeps empty text. - ChatForm: attachments no longer make an empty draft submittable in answer mode, where submitText consumes the click without answering or sending. - agents request: seed title generation from attachment filenames when the turn has no text, so immediate-mode titles are not invented from an empty string. - useChatFunctions.regenerate.spec: mock the utils barrel over the real module so new exports resolve. * Cover the agents path for attachment-only turns AgentClient formats its payload with the SDK's formatMessage, not the local one, so the earlier guard missed the endpoint the feature actually targets: an attachment-only turn still reached Anthropic as an empty user message. Apply the same stand-in after the file-context and quote merges, so a turn that already gained inline content is untouched. * Carry filenames on freshly attached files The fresh-file submission mapping copied only file_id, filepath, type, and dimensions, so the attachment-only title fallback read an undefined filename and produced nothing. Include filename, and cover it with a test that submits an empty draft with one attachment. * Address review: cover assistants v2, fresh agent attachments, editor, and title fallback - agents client: the current turn has no files during buildMessages, so read the resolved attachments from message_file_map instead. The previous guard only ever fired for persisted historical turns. - assistants chatV2: the default assistants endpoint routes here, so it needs the same stand-in body chatV1 got. - assistants title: fall back to filenames, then the response, and keep the default title rather than saving an empty one. - EditMessage: retained attachments make an empty edit submittable, matching the composer, so the overrideFiles replay path is reachable from the UI. --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> |
||
|
|
e1ac7d2bda
|
⚓ ci: Settle the E2E Reply Before the Double-Click Quote (#14840)
* test(e2e): grant MULTI_CONVO.USE in the mock e2e config
`agent-skills-added.spec.ts` drives the composer's `+` command, which opens the
added-model popover. That path is gated on MULTI_CONVO.USE:
if (!hasMultiConvoAccess || !plusCommandEnabled || isAssistantsEndpoint(endpoint)) return;
The mock config never sets `interface.multiConvo`, so the permission falls
through to the seeded role default and `handlePlusCommand` returns before
opening the popover. The spec then fails on a popover that is absent from the
DOM entirely, which reads as a selector or timing problem rather than a missing
permission.
Set it explicitly, the same way `contextCost` is set just above for the usage
gauge — the mock config's job is to make each exercised feature's gate explicit
rather than inherit a default.
* test(e2e): wait for the reply to settle before the double-click quote
`quotes.spec.ts` › 'summons the popup from a native double-click word
selection' double-clicks a word as soon as `mockReply` becomes visible. But
`sendMessage` resolves on the stream *response*, not on the final render, so the
reply can still be re-rendering.
A streaming markdown re-render swaps out the text node the selection points at,
which collapses the selection — the same mechanism the sibling
`selectionchange` test documents deliberately. A double-click landing mid-stream
therefore loses its selection before the popup can be clicked, and because the
whole gesture is wrapped in `toPass`, every retry re-runs into the same
still-streaming reply rather than recovering from a one-off.
This is a different race from the one #14777 fixed. That one is the *selection*
still settling (touch long-press, native handle drags, block-granularity
gestures) and is handled inside QuoteButton. This one is the *reply* still
streaming, which no amount of component-side settling can absorb.
Observed on a downstream fork running this suite on slower hardware: the test
fails all three attempts, deterministically on the in-memory stream store while
the Redis lane passes the same shard — the in-memory store's final re-renders
land late enough to outlive the gesture. Four separate runs, same split.
Wait for the reply text to hold steady before selecting.
* ci(e2e): install ffmpeg so first-retry video actually records (#14841)
`playwright.config.mock.ts` sets `video: 'on-first-retry'`, but the runner only
installs `install-deps chrome`, which does not include ffmpeg. Without it the
first retry fails inside `browserContext.newPage` while setting up video
recording — before the test body runs.
The cost is the retry itself: a genuinely flaky test loses the attempt that
would have recovered it, and the reported failure is a video-setup error rather
than the original symptom.
Bounded and non-fatal on purpose. The CLI has been observed hanging after the
download completes on these runners, so the step is wrapped in `timeout` and
its failure is swallowed — if ffmpeg cannot be installed the job proceeds
exactly as it does today, and no lane is blocked on it.
Applied to both jobs that run Playwright (`e2e_shards` and
`mcp_tool_list_changed`), since both configure retries.
|
||
|
|
fe71ffdf42
|
🛂 ci: Grant Multi-Convo Permission in E2E Mock Config (#14839)
`agent-skills-added.spec.ts` drives the composer's `+` command, which opens the
added-model popover. That path is gated on MULTI_CONVO.USE:
if (!hasMultiConvoAccess || !plusCommandEnabled || isAssistantsEndpoint(endpoint)) return;
The mock config never sets `interface.multiConvo`, so the permission falls
through to the seeded role default and `handlePlusCommand` returns before
opening the popover. The spec then fails on a popover that is absent from the
DOM entirely, which reads as a selector or timing problem rather than a missing
permission.
Set it explicitly, the same way `contextCost` is set just above for the usage
gauge — the mock config's job is to make each exercised feature's gate explicit
rather than inherit a default.
|
||
|
|
88747f0ad8
|
🩺 fix: Render Stopped Run Steps From Explicit Status (#14871)
* 🩺 fix: Render Stopped Run Steps From Explicit Status Tool calls decided "still running" vs "stopped" with a whole-message heuristic: const cancelled = !isSubmitting && progress < 1 && !hasError; That inference cannot tell which step actually stopped. An aborted step keeps spinning while `isSubmitting` is still true, and when submitting ends, every unfinished part flips to "Cancelled" at once regardless of which one died. `@librechat/agents` v3.4.6+ emits `on_run_step_closed`, a terminal per-step signal carrying `status` and timestamps — including for steps swept at end-of-run because the caller aborted. The pinned 3.5.1 already ships it; nothing consumed it. - `StepEvents.ON_RUN_STEP_CLOSED` plus `RunStepClosedEvent` / `RunStepStatus` types mirroring the SDK payload. - `PartMetadata.runStepStatus` — a dedicated field, since `status` is already claimed by activity-label and question-form parts. - Server handler forwards the event without the visibility gating the other step handlers apply: a step whose open reached the client must get its close, or the client is left inferring again. - `useStepHandler` writes the terminal status onto the tool call part. - Both decision points (`ToolCall`, the shared `useToolCallState`) prefer explicit status, keeping the heuristic as fallback for messages saved before this and endpoints that do not emit the event. Threaded through the five cards sharing `useToolCallState`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🩹 fix: Address Codex Review On Run Step Closure Rendering - Persist the terminal status server-side. The handler emitted the closure without folding it into `contentParts`, so the status existed only on the live React message: a reload or resumable reconnect dropped it and fell back to the very heuristic this fixes. Now stamped onto the aggregated tool-call part (via `stepMap`, falling back to the event's own index) before forwarding. - Honor terminal status independently of output parsing. Gating on `hasError` meant a `failed` step with unparseable output rendered as "cancelled", while a `failed`/`cancelled` step whose output did parse as an error was not terminal at all and shimmered indefinitely when no completion event arrived. A closed step now forces progress complete and reports `failed` as an error state on its own authority. - Pass the status to the second `BashCall` branch, which rendered the same updated component without it. - Reuse `Agents.RunStepClosedStatus` in `PartMetadata` instead of redeclaring the union, so a future SDK status cannot diverge between the event and the persisted part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * ♿ fix: Replay Closed Status On Redis Resume, Announce Failures - Apply closure events during Redis reconstruction. The stamp added in the previous commit mutates only the originating process's in-memory `contentParts`; a resumable reconnect landing on another replica rebuilds from `RedisJobStore.getContentParts`, whose allowlist omits `on_run_step_closed`. The status was therefore absent from the sync snapshot and, being snapshot-covered, never redelivered as pending — so multi-replica resume fell back to the whole-message heuristic. Handled as a host-authored event alongside `on_steer_applied` and `on_activity_label`, since the SDK aggregator has no notion of it. - Announce terminal failures in the live region. Forcing terminal progress for a closed step meant a `failed` tool reached the `aria-live` region through `getFinishedText()`, which only special- cased cancellation and otherwise announced "completed function" — telling screen-reader users the opposite of what the card showed. A regression introduced by the previous commit; error states now announce failure before any completion string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🎯 fix: Resolve Closed Steps By ID, Never By Index The steer and HITL offset wrappers clone and shift only `ON_RUN_STEP` and `ON_AGENT_UPDATE`; every other event passes through untouched. A stored `on_run_step_closed` therefore carries the SDK's unshifted index, while the part it belongs to was rebuilt at the shifted one. Any run containing a steer insertion or HITL resume would stamp the status onto an earlier tool card, or none — leaving the real card on the fallback heuristic while mislabeling a different one. - Redis reconstruction builds a step ID -> index map from the replayed `on_run_step` payloads (which carry the shifted index) and resolves closures against it, mirroring what the live callback does via `stepMap`. - The live handler drops its `?? data.index` fallback for the same reason. Skipping is the safe failure: a missing status degrades to the old heuristic, whereas a misplaced one actively mislabels the wrong card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
cd4511038d
|
🚏 feat: Central Trace Destination Opt-Out for Langfuse (#14838)
* feat(langfuse): let callers opt out of the central trace destination Adds `centralTraceExportEnabled` to the score-destination options and threads it through `getScoreDestinations`, `getLangfuseTraceDestinationIds` and `getLangfuseTraceMessageFields`. Deployments that route traces per tenant may want a given turn's spans to reach only the tenant destination — for example when central export is a per-tenant setting rather than a deployment-wide one. Today the central project is included whenever env credentials exist, with no way for a caller to decline it for a single trace. Defaults to `true` everywhere, so existing callers are unaffected: the option is additive and every current call site resolves exactly as before. The three public helpers gain an optional trailing parameter and nothing else. While here, `getScoreDestinations` destructures its options with defaults instead of repeating `options?.waitForCentralProjectId !== false` at both call sites, which is what made adding a second flag awkward. Verified: no new tsc errors (one pre-existing cacheFactory error is unchanged), 99 langfuse tests pass, ESLint and Prettier clean. * fix(langfuse): keep the central opt-out intact when destinations resolve Addresses two review findings on the new `centralTraceExportEnabled` option. Both are cases where opting out of central export was silently discarded by destination resolution, letting later feedback reach a project the trace never went to. 1. Non-fanout deployments with no central env credentials still returned the configured connection, because only the central-credential branch was gated. `resolveLangfuseExportPlan` reports `disabled` for that shape — without a fanout route there is nowhere for a central-suppressed trace to go — so return no destinations and match it. 2. `getLangfuseTraceDestinationIds` returned `undefined` whenever any destination lacked an id, and `sendFeedbackScore` reads `undefined` as unrestricted. A tenant destination has no id when its optional `projectId` is unset, so a suppressed-central trace could resolve back to the central project at feedback time. Fail closed with an empty list, which stays restricted, instead. Both paths now have regression tests, each verified to fail without the corresponding change: the first returned 3 destination ids, the second returned `undefined`. * fix(langfuse): carry the central opt-out into the feedback path `getLangfuseTraceDestinationIds` returns `undefined` when an eligible destination has no stable id, which a tenant route hits whenever the optional `langfuse.projectId` is unset. `sendFeedbackScore` read that as "unrestricted" and re-resolved destinations with central export enabled, so a suppressed-central trace still drew central feedback. Persisting an empty list instead only traded the leak for a drop: the destination filter rejects every id-less destination, discarding ratings the tenant should receive. The id list restricts feedback to destinations that survived reconfiguration; it cannot also encode deployment policy. Thread `centralTraceExportEnabled` through `sendFeedbackScore` so the policy is evaluated the same way at trace time and feedback time, and restore `undefined` for unidentifiable destinations. |
||
|
|
c4357fc9e3
|
📐 feat: Match the Message Column to the Composer (#14851)
* feat: Match the message column to the composer width Give messages the same max-width and horizontal padding as ChatForm, reserve the scrollbar gutter on the composer wrapper, and drop the 65ch prose cap so the body fills that column. * style: Drop the assistant avatar gutter Keep the icon and provider name on the same left edge as the message body. Mid-message author headers and steer bubbles no longer outdent past a column that no longer exists. * style: Reveal the timestamp on the message header bar Put the icon, provider name, and datetime on one full-width row, and show the time only when the message is hovered or focused. CSS on .message-render wins the hover hide that Tailwind group-hover lost. * style: Align scroll-to-bottom with the chat column Sit the control in the same padded column as the composer, swap the hard-coded disc for a themed outline Button, and fade it in on an 8px rise instead of a scale pop. * feat: Crossfade the provider name to the model on hover Swap the assistant header label to the real model name when the provider is hovered or focused. Skip agent_* document ids so the hover text is only a model name. * fix: Reserve the message column gutter without clipping the composer `scrollbar-gutter: stable` only holds its band back while the element is a scroll container, and a scroll container clips. Wrapping the composer in one put the in-flight steer overlay outside the clip: it is painted above the composer's top edge, so for the whole run a submitted steer was invisible and its cancel unreachable. The scroll-to-bottom wrapper had the same problem in a smaller way, cutting off the button's focus ring. Reserve the same band with padding instead, sized by the width the app already gives its own scrollbars, so both columns still line up with the messages without either becoming a scroll container. Also scopes the header label crossfade to the two labelled spans, and drops its `:focus-within` rules, which no focusable descendant can ever trigger. * fix: Keep document ids out of the header and name the model to screen readers An Assistants-endpoint message keys the assistant map by `assistant.id`, so its `model` field holds an `asst_` id, not a model name. The header label only skipped `agent_`, so it crossfaded the assistant's name into an internal id. Skip both prefixes, and offer `assistant.model` ahead of the message field in the callers that already resolved the assistant. The crossfade itself is pointer-only: the model span is `aria-hidden` and nothing in the label can take focus, so keyboard and screen reader users had no path to the value at all. Carry the model in text that never hides, which puts it in the header's accessible name alongside the author and the time. * refactor: Own the header crossfade in the component The provider-to-model crossfade lived in global CSS even though HeaderLabel is its only consumer. Tailwind expresses the whole effect: a named group for the hover scope, one grid cell shared by both labels, and the existing resize duration and easing variables. Reduced motion now follows the same motion-reduce convention as the rest of the client. * fix: Return a defined model name from the header lookup Array.find over nullable candidates widens the return to include null, which tsc rejects against the declared string | undefined. Narrow with a predicate and sort the imports the pre-commit hook rewrote. * fix: Keep the model reachable by keyboard and the scroll button inert The header crossfade was pointer-only, so a sighted keyboard user never saw the model name; the screen-reader copy covered announcement but not sight. Focusing anything in the message row now swaps the label too, the same hook the timestamp already reveals itself with. The scroll-to-bottom wrapper spans the column and stays inert so it never swallows clicks meant for the thread, which left the button to opt back into pointer events. A descendant that opts in is hit-testable however its parent paints, so the transition classes could not hold the control inert as they claimed: the button took clicks while invisible. Gate the opt-in on the enter transition settling and drop the declarations that never applied. * fix: Measure the scrollbar gutter and keep the scroll button unreachable The spacer assumed the gutter was the `::-webkit-scrollbar` width. Blink and WebKit honour that rule, Firefox ignores it and sizes the band itself, and an overlay scrollbar reserves nothing at all, so on those the composer and the scroll-to-bottom control sat off the messages they are supposed to line up with. Measure what the message column actually holds back and publish it for the spacer to read, leaving the token as the pre-measurement fallback. Gating the scroll button on pointer events alone also left it enabled, so it kept its place in the tab order and answered Enter while invisible. Disable it until the same gate opens, and hold its opacity so being briefly unreachable does not dim it on top of the wrapper's own fade. |
||
|
|
73812adca1
|
📐 fix: Size the Model Picker to Its Content (#14859)
The header trigger was w-full inside a max-w-md wrapper, so it rendered as a fixed 448px pill no matter how short the model name was. Let the wrapper shrink-wrap the button and cap it at 60vw (20rem from sm up), so the pill hugs its label and long names truncate instead of claiming the whole row. |
||
|
|
8d99fd16fc
|
🌗 fix: Keep Code Block Header Visible in User Messages (#14856)
* fix: Keep Code Block Chrome Visible on the User Message Bubble The code bar dropped its background in dark mode and inherited whatever sat behind it. That works on the chat background, but a user message bubble is surface-tertiary, which resolves to the same gray-700 as the code block's border-light outline, so both the header bar and the outline disappeared into the bubble and only the code body showed. Paint the bar with surface-secondary instead. It resolves to the same value as the old pair on the chat background (gray-50 in light, gray-800 in dark, matching the presentation background), so assistant messages are unchanged, while the bar keeps a surface of its own inside the bubble. The execution output panel used the same pattern and gets the same treatment. * fix: Derive the Code Block Surface So Custom Themes Keep Their Colors Painting the code bar with surface-secondary only reproduced the old appearance because the built-in themes happen to give surface-secondary, surface-primary-alt and presentation coinciding values. A custom theme sets those three independently, so the bar could shift on the chat background where nothing was meant to change. Add a surface-code role that derives from whatever the bar used to show: surface-primary-alt in light, where the bar was already opaque, and presentation in dark, which is exactly what the transparent bar inherited. Every theme therefore renders the bar as before on the chat background, while the bubble no longer bleeds through it. Give ResultSwitcher the same surface. It had no background of its own, so once the output panel above it gained one it became a detached band of bubble color, the same defect one element lower. * fix: Scope the Opaque Code Surface to User Message Bubbles CodeBar renders on more than the chat background. The terms dialog and the subagent panel put MarkdownLite on surface-dialog and surface-primary, and dark:bg-transparent was what let the bar sit on any of them. Painting it unconditionally gave those surfaces a header that contrasts with their own background. Restore the original declarations and scope the opaque role with a .user-turn ancestor selector, which MessageRow and SteerPart both already set, so every user bubble is covered without threading context through react-markdown. Outside a user bubble the classes are byte for byte what they were, so no other surface and no custom theme can drift. |
||
|
|
530a935a74
|
🎨 feat: Color the Context Gauge by Category and Collapse its Breakdown (#14855)
* 🎨 feat: Color the Context Gauge by Category and Collapse its Breakdown The context window bar becomes a stacked meter — one hue per category — and the breakdown collapses behind a disclosure so the gauge alone is the default view. The collapse choice persists per user. Adds a categorical series scale (`rgb-series-1`…`rgb-series-7`) to the versioned theme registry, so themes and `REACT_APP_THEME_SERIES_*` can retint it. Hues are anchored on LibreChat's own brand tokens; every step was computed rather than picked, by enumerating slot orderings and snapping each step until all gates passed in both modes: worst adjacent CVD ΔE 12.4 light / 13.0 dark (target 8) worst adjacent normal-vision 19.0 light / 19.0 dark (floor 15) contrast all 14 steps ≥ 3:1 on both the popover surface and the meter track Slot order is the colour-vision-deficiency safety mechanism, not cosmetics. Reserved status colors are never reused for series identity, and the circular composer gauge is deliberately untouched — it answers "how close am I to the limit", which stays a status question. - `SegmentedMeter` + `MeterSwatch` land beside `Progress` in `@librechat/client`, owning the 2px surface gaps, rounded ends, the min-width floor, and the hatch. The category-to-slot mapping stays feature-local: the palette is theme data, the mapping is not. - Every present category gets a 2px floor so a 251-token row cannot render as 0.09px; the shortfall comes out of free space, never another category. - Deferred tools keep their family's hue and add a 135° hatch, so a hue never means two things. Segments are reordered to put each deferred pair beside its parent, which is also the adjacency the palette was validated on. - Messages is drawn as a translucent fill with a solid edge: it is the only category the user grows, and the form difference doubles as secondary encoding. - A row carries a swatch if and only if it is a segment. The estimate path knows the total but not the composition, so it keeps a single unsegmented fill. - Usage totals gain a "Totals" heading, and row text lifts to primary ink on hover/focus. - The popover widens 256px → 288px to absorb the chevron and the legend swatches. Guardrails: the series scale is held to the 3:1 mark floor on both surfaces, the app CSS defaults are held in step with the runtime themes, and each slot is asserted to resolve to a Tailwind utility backed by its CSS variable. * 🐛 fix: Address Codex Review on the Segmented Context Gauge Three P2 findings, all confirmed. **Gaps inflated the fill.** Segment widths were percentages of the whole track while `gap-[2px]` was added on top, so the gaps ate into the free-space remainder instead of living inside the filled region. Measured on the real component: a window at 47.2% painted 55.6% full, and the bar read full at ~94%. Each segment now surrenders its share of the gap budget, so fills plus gaps span exactly the used fraction. Same case now paints 50.2%. The residual 3.0pp is the `SEGMENT_MIN` floor doing its job — five sub-pixel categories rounded up to 2px each. That overshoot is deliberate and bounded, it comes out of free space rather than a neighbouring category, and the doc comment now states the magnitude instead of leaving it implicit. **No reference-theme test.** The suite only exercised the bundled token tables, so it could not detect the shared component becoming coupled to LibreChat's values. Adds a deliberately different reference `ThemeDefinition` and asserts the registry accepts it, the values reach the applied CSS variables, and every rendered mark takes its colour from those variables — no literal colours in the tree. `SegmentedMeter.tsx` also joins the shared-primitive colour guardrail. **Series tokens missing from the public maps.** `IThemeVariables` and `IThemeColors` are exported for downstream consumers to type their CSS-variable and Tailwind maps, and would have rejected the new keys. Adds the series entries to both, plus a compile-time guard in the registry so a slot added to one map and missed in another fails the build. The guard deliberately lives in `registry.ts`, not the spec: `tsconfig.json` excludes `*.spec.ts`, so an assertion there is never checked by the build — verified by removing a key from each map in turn and confirming the error. * ✅ fix: Expand the Breakdown in the Context Gauge e2e Specs `e2e/specs/mock/usage.spec.ts` asserts on rows that now sit behind the disclosure, so four tests failed on the collapsed default. My miss — I updated the component spec and never grepped for e2e coverage. `openBreakdown` now expands the detail after opening, so every caller that reads a row keeps working; the helper is idempotent, since a reload restores an already-expanded preference. The one inline `gauge.click()` that duplicated the helper now uses it. Adds the case the regression should have been caught by, and which only e2e can reach: the popover opens to the gauge alone with no detail mounted, expanding reveals the labelled Totals section, and the choice survives a real reload through localStorage without a second click. `e2e/specs/real/usage.spec.ts` reads the totals the same way. It also hovered rather than clicked, which never opened the popover at all — hover surfaces only the compact snapshot tooltip, as the mock spec asserts. |
||
|
|
58f0ab7f62
|
➡️ style: Flush the In-Flight Steer Stack to the Composer Edge (#14854)
The stack carried its own `mx-auto max-w-3xl` cap, a second width constant beside the composer's. They agree only at `md`. Past that the composer widens to `xl:max-w-4xl` (or to the full pane under maximized chat space) while the stack stays pinned at 768px and centered, so the steer bubble drifts inboard — about 64px short of the composer's right edge at `xl`, and much further when chat space is maximized. `inset-x-0` already spans the composer wrapper, so the cap was never adding a bound the composer didn't already impose; it was overriding one. Dropping it leaves a single source of truth for the column width, and `p-2` then lands the send-now arrow in the same column as the composer's own send button (`mr-2` plus the 1px border). Below `md` nothing moves — the wrapper was already narrower than the cap. This puts the stack in the scroll-to-bottom button's lane at every desktop width, where before `xl` kept them 64px apart by accident. That is what `steerOverlayHeightFamily` is for: the button offsets by the published overlay height, measured from the same edge the stack grows from, so it rests 20px above the top of the stack (#14844). |
||
|
|
8640bf89ef
|
fix: copy only assistant response text (#14853)
Restore the pre-#14770 copy path so the message copy button serializes TEXT parts only and skips tools, thinking, and errors. |
||
|
|
6f05f2427b
|
👷 ci: Stop Optional Playwright Fonts From Failing E2E (#14852)
`npx playwright install-deps chrome` is the third-most-common e2e failure: three of the last twenty-five Playwright runs died on it, taking the whole aggregate gate with them. The step is not installing anything CI needs. The runner's Chrome is an apt package, so apt has already satisfied every library Playwright lists — the log shows each one "already the newest version". All `install-deps` adds are decorative CJK/Thai/Cyrillic font packages, ~21MB pulled from azure.archive.ubuntu.com by seven jobs on every PR. No CI assertion depends on them: the only spec that screenshots gates its comparison behind `E2E_VISUAL_SNAPSHOTS`, which no workflow sets, and no baselines are committed. Keep the install, but demote it. `google-chrome --version` becomes its own fatal step so a genuinely missing browser still fails loudly and immediately, while the font install retries with a per-attempt cap and degrades to a warning. The Redis install in the list_changed job stays fatal — that one is required. |
||
|
|
af7e890b14
|
🐛 fix: Give the Header's Sidebar Toggle Its Own Test Id (#14850)
The header now branches on CSS instead of `useMediaQuery`, so its mobile `OpenSidebar` stays mounted at every breakpoint. The sidebar rail already publishes `open-sidebar-button` for its own collapsed toggle, so both held the id at once and `getByTestId` resolved to two elements. Scope the header's copy to `header-open-sidebar-button` and assert the count in the spec that broke — the click there failed only once the header had mounted, so a count assertion pins the collision deterministically. |
||
|
|
0e160d2ba0
|
📱 style: Consolidate the Mobile Chat Header (#14843)
* ♻️ refactor: Extract `useNewChat` as the Single New-Chat Path The new-chat sequence (clear the outgoing conversation's cached messages, invalidate the messages query, reset the conversation atom) existed in three places: the sidebar's `NewChatButton`, the `newChat` keyboard shortcut, and an unrendered `Nav/NewChat` component. Consolidate into `hooks/Chat/useNewChat`. The panel switch stays an optional `onNewChat` callback rather than living in the hook, because `useActivePanel` throws outside `ActivePanelProvider` and the chat header sits outside it — the upcoming header button needs this seam. `useKeyboardShortcuts` consumes the returned `newConversation` so the file still instantiates `useNewConvo` exactly once. Delete `Nav/NewChat`: it was reachable only through its own barrel export, and carried a stale `max-md:hidden` plus a `data-testid` that collided with the sidebar's button. `handleNewChatClick` now also defers on shift-click, so shift-click opens a new window like any other link. `ExpandedPanel.spec` mocks the new hook — it reaches `useNewConvo` by deep path, which escapes the spec's `~/hooks` barrel mock. * ♻️ refactor: Split Header Action Logic Out of Its Buttons Lift the behaviour behind the compare and temporary-chat header buttons into `useMultiConvo` and `useTemporaryChat`, leaving each component as a thin trigger. The upcoming mobile overflow menu needs the same actions as menu items, and the visibility rules (assistants have their own comparison surface; temporary chat can't be toggled mid-thread) have to stay in one place rather than being restated per surface. Add the header's new-chat button, consuming `useNewChat`. It renders as an anchor to `/c/new` so modified clicks still open a tab, and uses a distinct `data-testid` from the sidebar's button so queries can't match both. `useTemporaryChat` toggles through a functional updater, dropping the `useRecoilCallback` that existed only to close over the current value. No visual change yet — the header layout lands next. * 📱 style: Fold the Mobile Header Into Four Targets The mobile header was a horizontally scrolling strip of up to seven controls, each in its own outlined box, so nothing grouped and nothing receded. The overflow was hidden rather than solved: ModelSelector alone is capped at 70vw (273px) and the side clusters need ~130px, which does not fit a 390px phone. Mobile now reads: sidebar toggle, model selector, new chat, ellipsis. Lift the bookmark and export/share menu items into `useBookmarkItems` and `useExportShare`, each returning the items plus the dialog instance the surface must render. Both menus already built `MenuItemProps[]` internally, so the desktop buttons keep their exact markup and simply consume the hook — the two surfaces cannot drift apart. `HeaderMenu` composes those with the compare and temporary-chat actions. Bookmarks nests through `subItems` rather than flattening every tag to the top level, permission gates decide membership, and the trigger does not render when nothing survives — reachable, since export/share self-hides on a new conversation. Layout is one DOM order serving both breakpoints; hidden items generate no flex gap, so each collapses without reordering. Branching is CSS-only: `useMediaQuery` resolves after paint, and the old `isSmallScreen ? <OpenSidebar/> : null` popped the row a frame late on every mount. `overflow-x-auto` is gone. It hid the overflow instead of fixing it, and it is a horizontal-swipe sink the later edge-swipe work needs removed. Presets stays a visible mobile icon for now: `PresetItems` uses Radix's `Close`, which throws outside a Popover root, so folding it needs a controlled + anchored menu and browser verification. Also drops two stray `console.log` calls carried along from the bookmark mutation handlers. * 🩹 fix: Address Codex Findings on the Mobile Header Menu `separate: true` marks an entry as *being* a divider — `DropdownPopup` returns only a `MenuSeparator` for it and drops the item. Setting the flag on Share and on temporary chat therefore deleted those actions whenever an earlier group existed. Push standalone divider entries instead. The spec missed this because its `DropdownPopup` mock rendered every label regardless of the flag. It now mirrors the real contract — dividers replace items, `show: false` entries are dropped — and asserts both actions survive alongside their dividers. Gate the bookmark tags query on the bookmark permission. `HeaderMenu` mounts unconditionally and called `useBookmarkItems` before the permission result applied, so users without `BOOKMARKS:USE` issued a forbidden request on every chat header mount; the old header dodged this by mounting `BookmarkMenu` only after the check. Restore two states the collapsed trigger had dropped: the shared-link indicator and its active-link label, and a visible checked state for temporary chat, which previously only reached assistive tech through `aria-checked` while the old button switched to `bg-surface-active`. Compose both new controls from the shared `Button` primitive with the same override `OpenSidebar` already uses, rather than restating the bordered icon-button recipe locally. * 🐛 fix: Give the Overflow Menu's Share Indicator Its Own Test Id Restoring the shared-link indicator on the mobile trigger reused the id `ExportAndShareMenu` already owns. Both headers stay mounted and are only hidden by CSS, so `getByTestId('header-shared-link-indicator')` matched two elements and `shared-links.spec.ts` failed on a strict-mode violation. Distinct id, matching the new-chat button, which was already separated from the sidebar's for the same reason. |
||
|
|
f44ce0bb5d
|
⬇️ fix: Keep Scroll-to-Bottom Clear of the In-Flight Steer Stack (#14844)
Both elements claim the same corner. `ScrollToBottom` is `bottom-5`, right-aligned, anchored to the message scroll region. `InFlightSteers` is `bottom-full`, right-aligned, stacking upward from the composer's top edge. They overlap at every breakpoint, and because they sit in different stacking contexts, which one paints on top depends on ancestor DOM order rather than intent. The reservation mechanism already exists: `InFlightSteers` measures itself into `steerOverlayHeightFamily` and `MessagesView` reads it to pad the thread so the newest message clears the overlay. The scroll button was never included. Thread that same height through and offset the button by it — no new state, no second measurement. Also gives the button a mobile gutter. Its width was `md:max-w-3xl` with no base value, so on a phone it escaped the message column and pinned to the viewport edge while the steer bubbles inset by 8px. `px-4 md:px-0` aligns it with the message content and leaves desktop untouched. The overlay is capped at `max-h-[35vh]`, so the button can rise at most a third of the screen. |
||
|
|
b0ed8524d4
|
⚡ fix: avoid modal attachment menu overhead (#14847) | ||
|
|
e1178d3c65
|
🏘️ fix: Scope OpenID User Cache Keys to Signed User Identity (#14837)
* fix(auth): scope OpenID user cache by tenant * fix(auth): preserve pre-auth cache scope * fix(auth): type OpenID reuse secret |
||
|
|
ee21066590
|
🏎️ ci: Focus Redis E2E Coverage (#14842) | ||
|
|
336703fe48
|
🔔 fix: Report Agent Saves That Reuse the Newest Version Entry (#14824)
* fix: report agent saves that reuse the newest version entry An update whose result matches the newest version is written without recording a version entry. The Agent Builder derived its success message from the version count, so every such save reported "No changes were made" while the edit had in fact been persisted. Base the message on whether the submission carried an edit of its own instead, and keep the version count for the version history panel. Also stop suppressing the version entry when the update carries an atomic operator. isDuplicateVersion compares direct updates only, so it cannot speak for the operator half; suppressing there applied a change that no version entry recorded, leaving the document diverged from every entry in its own history. Closes #14809 * fix: count an avatar reset as a persisted edit An avatar upload uses its own endpoint, but a reset rides the update payload as avatar: null, so classifying every avatar-only submission as non-persisted was wrong for resets. Clearing an avatar the newest version never recorded reads as a duplicate to isDuplicateVersion, since it skips a field when both sides are falsy, so the reset landed with the version count unchanged and reported "No changes were made". * fix: skip the version entry when an atomic operator changes nothing An update carrying $push, $pull or $addToSet bypassed duplicate suppression on the operator's mere presence. Re-attaching a resource file an agent already holds makes $addToSet a no-op, so an agent with actions recorded a version entry for a write that never touched the document, and its version count climbed on retries. Resolve the operators against the current document instead. $push always appends and $pull matches arbitrary criteria, so both still count as mutating; $addToSet counts only when some value it adds is missing. Whatever cannot be compared cheaply counts as mutating, since over-reporting costs a redundant version while under-reporting would apply a change no version records. * fix: confirm the submitted edit survived before claiming a save changed anything Treating a dirty form as proof of a persisted edit reports success for a save that stored nothing. The server can normalize a submission straight back to the stored value: an MCP tool the user added is dropped when authorization rejects it, and a skill is pruned when it no longer exists. Neither moves the version count, so the toast claimed the agent was updated when it was untouched. Capture the agent as it stands before the write, since the mutation replaces that cache entry on success, and compare it against the one the server returns across the fields the submission carried. Keep the dirty check alongside it: an agent loaded through the basic projection carries fewer fields than the update endpoint returns, and pairing the two keeps an untouched save honest either way. * fix: compare a save against the expanded agent, not a basic projection The panel falls back to the basic agent query whenever the expanded one has not resolved, and that projection drops instructions, tools, edges, skills and the rest while reducing model_parameters to a single flag. Comparing a submission against it made every one of those fields read as changed, so a rejected MCP tool or a pruned skill still reported success. Compare against the expanded agent, the only projection carrying every field a submission sends. When it is unavailable the comparison reports true and leaves the dirty check to decide, since claiming nothing changed for a save that did is the worse of the two errors. Renamed to say what it now answers. * fix: drop the operator a suppressed update judged a no-op Suppression reads whether $addToSet would add anything from a document fetched before the write, and that reading cannot bind a concurrent one. A $pull landing in between leaves the operator re-adding the value while the version entry has already been suppressed, which is the one outcome this path exists to prevent: a change applied with nothing in the history recording it. Drop what was judged a no-op instead of racing it. Only $addToSet reaches here, and only once every value it adds was found stored, so removing it makes the suppression true by construction rather than true if nothing else writes first. * fix: leave a suppressed update carrying no operator at all Dropping only $addToSet left the invariant resting on which operators callers happen to send. A present but empty $push or $pull counts as no operator when deciding suppression, yet survived into the write, so the suppressed update was operator-free by convention rather than by construction. Drop all three. Reaching suppression already means none of them can change the document, so removing them states that outright and keeps the write consistent with the history it declines to record. |
||
|
|
69ce4b7b00
|
📱 style: Reclaim the Assistant Avatar Gutter on Mobile (#14836)
Assistant content sat 36px from the left edge on mobile (a 24px avatar column plus `gap-3`) against a 16px gutter on the right, costing ~13% of the reading width on every response. Move the avatar into the assistant heading and restore the gutter as `md:pl-9` on the content column, so the column only exists from `md` up. Geometry is unchanged on desktop: 768 - 24 - 12 and 768 - 36 both leave a 732px content box, and an absolutely positioned child resolves against the padding box, so `md:left-0` lands the avatar where the column started. `AuthorHeader` and `SteerPart` hard-coded `-ml-9` to reach back past that column; both are gated to `md` so they no longer outdent off-screen. Pure `md:` variants rather than `useMediaQuery`, which resolves desktop-first after paint and would reflow every row on mobile. Covers all five surfaces sharing `MessageRow`: the three message paths, the shared-conversation view, and search results. |
||
|
|
5d3edeb383
|
🪄 feat: Smooth Activity Phase Transitions (#14832)
* feat: Animate activity phase transitions
* style: Match activity phase formatting
* 🪄 fix: Fold activity phase entrance in one direction, flush-left label
The phase header replaced <summary> with <button>, which brought the UA
`text-align: center` with it — the label span is `flex-1`, so the text
filled the row and centered inside it. Left-align it and drop the leading
glyph: the card's border and fill already carry the weight, and the child
tool groups keep their own icons.
The entrance also read as two movements. The card, header and inset all
hard-cut in at full size, displacing the transcript below by ~57px, then
folded back up past the header that had just pushed it down. The card now
mounts in the shape of what was already on screen — zero-height header,
transparent chrome, no inset — and grows the header as the panel collapses,
so the block's height only ever decreases. Chrome, padding and both heights
share one curve.
The collapse also waits for a painted start value; a single rAF can land
before paint, and a start value the compositor never saw snaps rather than
transitions.
- Restore the e2e parent-phase selectors, which still matched `summary`
- Memoize the hoisted `groupActivityPhases` pass and its phase-index set
- Finish the amber -> `text-text-warning` sweep in ToolCallGroup and Part
* 🩹 fix: Scope phase-entrance history and resolve media queries at mount
Addresses both Codex findings on #14832.
`MultiMessage` renders siblings without a key, so `ContentParts` survives a
sibling switch with its refs intact. The recorded phase-marker set outlived
the message it described, and any phase in the newly selected sibling whose
index was absent from the previous sibling's set was read as a live arrival —
already-loaded history mounted expanded and collapsed itself. Scope the set
to its messageId and treat a mismatch as a fresh mount.
`useMediaQuery` initialized to `false` and resolved only in a passive effect,
so the first render always reported "no match". Anything branching once at
mount — the frozen entrance flag here, and every other first-paint decision
across its call sites — never saw the correction, which is how a
`prefers-reduced-motion: reduce` user still got the fold. Read the query
synchronously in the state initializer and guard both paths for environments
without `matchMedia`.
* ♿ fix: Honor reduced motion on manual phase disclosure
The entrance already respected the preference, but manually opening or
closing a phase did not: `useExpandCollapse` writes its transition as an
inline style, which cannot carry a `prefers-reduced-motion` media query,
and there is no global reduced-motion reset in the stylesheet. Before this
PR the phase used `<details>`, which had no animation at all — so the swap
to an animated disclosure handed reduced-motion readers a 300ms fold they
did not have.
Resolve the preference in the hook and drop the transition outright. Every
expanding panel in the message content shares it, so tool calls, thinking
blocks, attachments and web-search sources are covered by the same change.
The chevron and the fold's own utility classes get `motion-reduce`
overrides, which the inline styles cannot express.
* 🩹 fix: Keep the collapse completion signal under reduced motion
`transition: none` emits no `transitionend`, and ToolCallGroup waits on
that event to drop `shouldRenderBody`. Removing the transition therefore
left every collapsed tool subtree mounted indefinitely — expensive and
stateful children retained for exactly the readers who asked for less
work, not more.
Shorten the duration to 0.01ms instead. It is imperceptible, still fires
the event, and keeps the hook the single place that knows about the
preference. Caught by Codex on
|
||
|
|
c06fbff475
|
📦 chore: bump @librechat/agents to v3.5.1 (#14830)
* 📦 chore: bump `@librechat/agents` to v3.5.0
* chore: bump agents sdk to v3.5.1
|
||
|
|
bc6392d05b
|
🪢 fix(langfuse): mark provider-backed agent traces (#14833)
* fix(langfuse): mark provider-backed agent traces * fix(langfuse): mark stored response traces * test(langfuse): isolate provider marker setup |
||
|
|
d170ecf481
|
🧹 ci: Remove Obsolete Test Server Deployment (#14823) | ||
|
|
0ce4c3374b
|
⏲️ test: Give ServerConfigsDB Mongo Hooks a 60s Timeout Budget (#14831)
`beforeAll` boots a real mongod via MongoMemoryServer, resets the module registry and re-imports data-schemas, ServerConfigsDB and the MCP OAuth handler before a single test runs. That exceeds the 15s global `testTimeout` once the runner is busy: the suite finishes in ~4.5s on its own but has been observed at 16.8s under a loaded `@librechat/api` shard, failing every test in the file with "Exceeded timeout of 15000 ms for a hook". Give both mongo hooks an explicit 60s budget, matching `checkpointer.integration.spec.ts`, the other MongoMemoryServer suite that already opts out of the global default. `afterAll` gets the same treatment since `mongoServer.stop()` is subject to the same contention. No behaviour change — the timeout only bounds setup, and the suite still completes well inside it. |
||
|
|
eaef87fa26
|
🚀 chore: Prepare v0.8.8-rc1 (#14394)
* 🚀 chore: Prepare v0.8.8-rc1 release * 📚 docs: Complete v0.8.8-rc1 operator references * 📚 docs: Mark stateful sessions experimental * 📚 docs: Clarify background code capability * 📚 docs: Refresh v0.8.8-rc1 operator guidance * 📚 docs: Highlight v0.8.8-rc1 features in README * 📦 chore: Bump publishable packages again * 📚 docs: Add streaming question progress * 📦 chore: Bump publishable packages again * 📚 docs: Refresh v0.8.8-rc1 release highlights * 📦 chore: Bump publishable packages again * 📚 docs: Refresh v0.8.8-rc1 release guidance * 📦 chore: Bump publishable packages again * 📚 docs: Highlight batched Agent questions * 📦 chore: Bump publishable packages again * 📦 chore: Bump publishable packages again * 📦 chore: Bump publishable packages again * 📦 chore: Refresh v0.8.8-rc1 package versions * 📦 chore: Refresh v0.8.8-rc1 package versions * 📦 chore: Refresh v0.8.8-rc1 package versions * 📄 docs: Note PowerPoint template support * 📦 chore: Refresh v0.8.8-rc1 package versions * 📄 docs: Note latest provider and file support |
||
|
|
d4c64d485f
|
ci: gate the e2e activity-phase DOM assertions on the persisted phase (#14821)
`activity-phases` asserted the parent `summary` was visible immediately after `sendMessage` resolved. A parent phase only exists once the turn completes, the phase closes, and its summary round-trips to the phase-label model — so that assertion raced the entire pipeline and only survived on Playwright's retries. It shows as `1 flaky` on the memory lane of a green dev run, and fails all three attempts on slower hardware. Gate the DOM on the durable projection instead. The test already fetched /api/messages twice; the first fetch now also waits for the persisted phase part before any DOM assertion runs, so the client is only asked about a phase the server has already written. Also drops the duplicate fetch. The two poll blocks queried the same endpoint for the same message and both asserted `finalTextIndex === activity_end_index`; the removed copy left `liveAssistant`, `livePhase` and `liveFinalTextIndex` shadowing their durable equivalents. No coverage removed — every assertion is preserved, reordered to follow the dependency chain: persisted shape, then DOM, then label-model requests, then the reload round-trip. |
||
|
|
2f0cd2eb75
|
🔌 chore: Bump the MCP SDK to 1.30.0 and Parse Content-Type Instead of Searching It (#14820)
`@modelcontextprotocol/sdk@1.30.0` is a small maintenance release on the 1.x line (upstream's active line is now the 2.0.0 scoped packages). The range was already `^1.29.0`, so only the lockfile pinned the old version; the manifests move too so the floor matches what we test against. Nothing in it is breaking. The four changed type declarations are additive — optional `maxBufferSize` on `StdioServerParameters`, an optional third constructor argument on `StdioServerTransport`, optional options on `ReadBuffer`, optional `keepAliveMs` on the server transport — and the only manifest change is `@hono/node-server` widening to `^1.19.9 || ^2.0.5`. No new dependencies. Two behavior changes are worth knowing about even though neither is an API break. `ReadBuffer` now caps a single stdio message at 10 MB (previously unbounded) and errors the transport instead of growing, which is reachable through `StdioClientTransport` if a stdio server returns a very large single result; it takes `maxBufferSize` if that ever needs raising. And Content-Type handling switched from substring search to parsed media types, client and server. Most of the release is Streamable HTTP server hardening we do not run — a 15s SSE keep-alive, `X-Accel-Buffering: no` on SSE responses, guards so a stale stream's cancel cannot tear down its successor, and `_closed` checks so a transport closing mid-request stops registering streams into swept maps. None of it changes how we behave as a client. In particular it does not address the stale-stream 409 in #14816: that keep-alive runs in whichever server we connect to, not here. The same substring-vs-parse mistake the SDK corrected exists in our streamable HTTP response guard, which classified a response as SSE with `contentType.includes('text/event-stream')`. A `Content-Type` naming the SSE type in a parameter — `text/plain; boundary=text/event-stream` — is not an event stream, but matched. The guard then took `canEmitFallbackSSEError`, so an oversized body was answered with a synthetic SSE error frame the caller reads as a well-formed response body, rather than the throw a non-SSE response gets. The check now compares the parsed media type, via a `mediaTypeEssence` helper added to the header utils where `mergeHeaders` already lives. Verified against 1.30.0 rather than assuming: the package was staged into the worktree's own `node_modules` so it shadowed the shared install, and `packages/api` `src/mcp` ran green on it — same four pre-existing red suites as on 1.29.0 (`MCPReinitRecovery` plus three Redis `cache_integration` suites that need a live Redis), no new failures. |
||
|
|
24d111fde9
|
⚡ feat: Add Gemini 3.7 Flash Support (#14818)
* ⚡ feat: Add Gemini 3.7 Flash Support Adds first-class support for Google's Gemini 3.7 Flash (`gemini-3.7-flash`) for both the Gemini API (AI Studio) and Google Cloud Gemini Enterprise Agent Platform, following the Gemini 3.6 Flash integration (#14369). - Context window (1,048,576) in googleModels; API + cache pricing in tx.ts. - Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations. - Register the model in the Flash-family handler so it inherits the existing strip of deprecated sampling params (temperature/topP/topK), rejected penalty params, and thinkingBudget, and defaults to `medium` thinking. - Generalize that handler's enumerated table from a [id, level] tuple to a rule object, so a model can also declare thinking levels it rejects. Gemini 3.7 Flash errors on `minimal` (which the Google endpoint offers in its thinkingLevel slider), so an explicit `minimal` is substituted with the nearest supported level, `low`. Explicit low/medium/high pass through unchanged. - Apply Google's introductory pricing ($0.75 in / $3.75 out / $0.075 cached, per 1M) to Gemini 3.7 Flash and correct Gemini 3.6 Flash to the same rates. Both revert to $1.50 / $7.50 / $0.15 on 2027-01-01; noted at both call sites. Resolves #14802 Ref: https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash Ref: https://ai.google.dev/gemini-api/docs/pricing * 📝 docs: Match the House Style for Promotional Rate Comments Align the Gemini 3.6/3.7 Flash introductory-pricing notes with the existing Sonnet 5 convention in the same file: one comment per group, naming the models and the exact values to restore, so the manual follow-up is unambiguous. No rate changes. * ⬆️ chore: Bump `@librechat/agents` to 3.4.7 for Gemini 3.7 Flash Prefill Unblocks this PR. `NO_PREFILL_GEMINI_MODELS` is model-enumerated in the agents SDK, so 3.4.6 does not know `gemini-3.7-flash` forbids a trailing `model`-role turn — editing an assistant reply and resubmitting would reach Google as a prefill and return HTTP 400 on a model this PR adds to the default list. 3.4.7 (danny-avila/agents#412, released via #413) adds it. Verified the published tarball: `3.4.6...3.4.7` touches only `dist/{cjs,esm}/llm/google/utils/common.*` — the prefill array and its comment. `dist/types` is byte-identical, so there is no API surface change. Raises the declared range in both workspaces alongside the lock. `^3.4.6` already permitted 3.4.7, but the fix is required rather than merely compatible, so the floor should say so. |
||
|
|
5e464bc930
|
📎 fix: Alias Shell Script MIME Variants to application/x-sh (#14817)
* 📎 fix: Alias Shell Script MIME Variants to `application/x-sh` Chrome on Linux reports `.sh` files as `application/x-shellscript` (freedesktop shared-mime-info) and libmagic reports `text/x-shellscript`. Neither string appears anywhere in the source, so uploads were rejected even though `application/x-sh` is in the default allowlist and `codeTypeMapping` maps `sh` to it — `inferMimeType` only consults the extension map when the client sends no type at all, so a non-empty browser value passed straight through to the allowlist check. Alias both variants to the canonical `application/x-sh`, matching the existing treatment of `text/x-markdown` and `application/x-zip-compressed`. Also attach `statusCode`/`body` to multer file-filter rejections. Without them the error misses the `isCustomError` branch in `ErrorController` and falls through to a bare `500 An unknown error occurred.`, so the rejection reason was logged server-side but never reached the client. The upload hook already surfaces `error.response.data.message`, so a rejected file now explains itself instead of showing a generic upload failure. * 🔁 refactor: Move Upload Error Contract Into `packages/api` Addresses codex P1 on #14817. The producer of the `statusCode`/`body` pair now sits beside its consumer: `isCustomError` and `ErrorController` are already in `packages/api/src/middleware/error.ts`, and `CustomError` is already in `packages/api/src/types/error.ts` — only the construction of that pair was stranded in legacy JS. `createCustomError` is exported from the same module as the guard that recognizes it, and `multer.js` is back to a thin caller. Also pins the `.sh` back-compat claim with tests: configs from the documented workarounds (`application/x-sh` per #4660/#5689/#6297, and the broad patterns from #14804) still accept a `.sh` upload after the alias rewrites the type. A negative control confirms the endpoint config is genuinely in play rather than falling back to the default allowlist. |
||
|
|
6c46fd1252
|
📄 feat: accept PowerPoint template MIME type (#14761) | ||
|
|
6cbfd82772
|
🔌 fix: Recover Quietly From Stale MCP SSE Stream Conflicts (#14816)
A Streamable HTTP server allows one standalone `GET` SSE stream per session and
releases its mapping from the response stream's cancel callback. That callback
never runs when the connection dies at a proxy rather than at the client, so the
server keeps holding a stream nobody is reading while the client knows its stream
is gone. Every reconnect carrying that session id then gets a 409:
SSE stream disconnected: TypeError: terminated
Transport error (may require manual intervention):
Streamable HTTP error: Failed to open SSE stream: Conflict
Transport error (may require manual intervention):
Maximum reconnection attempts (2) exceeded.
Nothing there requires manual intervention. The connection recovers on its own in
a few seconds, because the rebuild the first 409 escalates to sends the
spec-mandated `DELETE`, which drops the server's session along with the stream it
leaked. Two things made a self-healing event read as a fatal one.
`extractSSEErrorMessage` classified status by scanning the message text for
digits, but `StreamableHTTPError` and `SseError` carry the status on `code` and
their messages do not always repeat it. "Failed to open SSE stream: Conflict"
has no digits at all, so a 409 never reached the status branch and fell through
to the terminal `isTransient: false` — the same verdict as a DNS typo. A 5xx
arriving on `code` alone had the same blind spot. The status is now read from
`code` when it is in HTTP range, with the message scan kept as a fallback, and
409 joins 5xx as transient: the stale session it reports is cleared by the
rebuild, with nothing for an operator to do.
The second is volume. Each SDK retry fires `onerror` twice — once with the raw
throw out of `_startOrAuthSse`, once with the `Failed to reconnect SSE stream`
wrapper. Only the wrapper matched the existing suppression, so every doomed retry
logged at error level, and the retries are doomed by construction: nothing about
the same session id can stop conflicting. The first conflict now escalates for
rebuild and the rest are logged as the echo they are, along with the SDK's
out-of-retries announcement when a rebuild is already underway. The non-conflict
path for that announcement is untouched, so an exhausted budget still falls
through to our reconnection everywhere else.
`extractSSEErrorMessage` moves to `errors.ts` alongside `isOAuthAuthenticationError`.
It had no test: `MCPConnection.test.ts` held a hand-copied clone marked "keep in
sync with the actual implementation", so 66 assertions were exercising the copy.
The clone is deleted and the suite now imports the real function, which it turns
out had not drifted.
`MCPConnectionSseConflict.test.ts` drives a real client transport against a real
in-process `StreamableHTTPServerTransport` reproducing the sequence above: the
stream opens, its socket is destroyed underneath the client, and every later
`GET` on that session id conflicts while a rebuilt session gets a healthy stream.
|
||
|
|
0654efb7ed
|
🔌 fix: Preserve MCP serverInstructions Declaration Through Inspection (#14815)
`MCPServerInspector` overwrote the operator's `serverInstructions` declaration with the text fetched from the server. That made a YAML server's cached entry differ from its own raw config on an admin-configurable field, so `isUnmodifiedYamlServer` misclassified it as admin-modified and re-inspected it on the first user-scoped resolve. The second inspection produced a config with a newer `updatedAt`, which: - flipped `getServerConnectionStatus` to `disconnected` permanently, since the healthy app connection was then measured against the newer timestamp; and - made `isAppServerConfig` reject the effective config, gating off the app connection so `GET /api/mcp/tools` returned zero tools and cached nothing. Fetched instructions now land on a separate `resolvedInstructions` field, matching how every other inspector-derived value is stored, so the declaration survives inspection and the guard compares like with like. Bumps `REGISTRY_STORAGE_SCHEMA_VERSION` so Redis-backed deployments rewrite entries whose `serverInstructions` still holds fetched text. Fixes #14798 |
||
|
|
7694428ca9
|
💬 style: Right-Align In-Flight Steer Bubbles to the Message UI (#14814)
* 💬 style: Right-Align In-Flight Steer Bubbles to the Message UI The chat surface reads as message bubbles now — user turns on the right, assistant turns on the left — but the in-flight steer bubbles anchored above the composer were still left-aligned, so a steer sat on the opposite side from the words the user had just sent, then jumped across on `on_steer_applied` when the persisted `SteerPart` landed in-thread on the right. Align the overlay with the user turn it belongs to: - The bubble stack right-aligns and is constrained to the message column (`max-w-3xl`), so the in-flight bubble sits where its applied twin lands instead of ~52px further right (the composer runs wider than the message column at `xl`). - The bubble adopts the same theme-token geometry as `SteerPart` and every user turn (`rounded-theme-surface rounded-br-theme-control`, `px-theme-normal`), replacing the raw `rounded-3xl`/`pl-3 pr-4`. It keeps its outline: an in-flight steer is still provisional. - The controls flank the bubble — overflow menu outboard-left, send-now arrow outboard-right — so neither reads as belonging to the other. DOM order matches visual order, so focus order stays coherent. Also drops the thin `bg-border-medium` divider that bound the arrow to its message: with the arrow now outboard on the far side of the bubble it has nothing to separate, and `EscalateNowButton` no longer needs its fragment. * 💬 style: Center the Steer Controls on the Bubble's First Line The flanking controls read as neither top-aligned nor centered, because their resting position was an accident of `sticky top-2`: the topmost rail trips the sticky inset at rest and is shoved 8px below the row top, while every rail below it clears the inset and stays at the top. So the controls sat 3.8px above the bubble's centre — and stacked steers did not even agree with each other. Give each rail a `py-3` band that reproduces the bubble's own first line (its `py-2.5`, its 1px border, and half the gap between the 24px control and the taller text line box), and pad the overlay evenly so the topmost rail already clears the sticky inset instead of being displaced by it. A 24px control now centres on the first line: measured at 722.0 against the text's 721.8, versus 718.4 before. Beside a one-line steer that reads as centred; on a tall one it aligns to the opening line rather than drifting to the middle, and sticky still carries it while the stack scrolls. |
||
|
|
da390fa919
|
🩹 fix: apply agent updates that match the newest version entry (#14810)
`updateAgent` returned early when the resulting state matched the newest `versions` entry, so `findOneAndUpdate` never ran and the caller's update was discarded behind a 200 response. Suppressing a redundant version entry is correct; suppressing the write is not. The document is regularly not equal to its newest version entry: `$push`/`$pull`/`$addToSet` updates snapshot the pre-update state (as `addAgentResourceFile` does on every file attach), `skipVersioning` writes snapshot nothing, and `removeAgentResourceFiles` bypasses `updateAgent` altogether. Any update that moved the document back onto that entry's content was then dropped, leaving the drifted state in place. Keep the version entry suppressed, apply the write, and still report the unchanged `versions` count as `version` so callers keep their existing "no new version" signal. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
abc669ab58
|
🩹 fix: Restore the @librechat/api Build and Remove Legacy Code (#14808)
* 🧹 chore: Remove Dead Legacy Agent Controller `_LegacyAgentController` has been unreachable since the resumable path became the only route: it is unreferenced, unexported, and untested. It had also drifted out of compilability against the live file — line 2009 called `attachConversationCreatedAt(req, { userId, conversationId, isNewConvo })` against the 3-argument signature declared at line 97, which would await `undefined` and then throw dereferencing `resolved.createdAt`. Keeping it was not free. It carried a third independent copy of the response message-id wiring (`getReqData`, `onStart`, four `updateMetadata` calls), so every change to how a generation identifies its response row had a dead third site to keep in step, and no test to say whether it had been kept in step. Removing the block leaves `createCloseHandler` and the `sendEvent`, `clientRegistry`, `requestDataMap` and `handleAbortError` imports with no remaining callers, so those go too. `AgentController` was a three-line passthrough to `ResumableAgentController`; the real controller is now exported directly, which also matches the `[ResumableAgentController]` prefix every log line in the file already uses. `server/routes/agents/chat.js` binds the export to its own local name and passes the same five arguments, so the route is unchanged. No behavior change: 379 lines removed, 2 added. * 🩹 fix: Remove Duplicated Anchor Block Breaking the `@librechat/api` Build `dev` does not build. `packages/api/src/agents/activityPhases/runtime.ts` carries two byte-identical 98-line copies of the same block (former lines 516-613 and 614-711), so rolldown fails to parse it: [PARSE_ERROR] Identifier `AnchorFields` has already been declared The duplicated block is the anchor-construction work from #14805: `AnchorFields`, `laterDefinedIndex`, `foldedAgentIds`, `boundedAnchor` and `mergeAnchors`. #14807 was squashed from a branch that predated #14805 and re-included that commit, so both copies landed. Only the `type` produced an error — the four function declarations simply redeclare. This removes the first copy. The two blocks were verified byte-identical before the cut, and the resulting file has no duplicate top-level declarations, is missing nothing that #14805 introduced, and retains everything new to #14807 (`ResolvedPosition`, `resolvePosition`). Verified: `tsdown` builds, `tsc --noEmit` clean, `config/circular-deps.mjs` green across all five graphs (it was reporting `✗ @librechat/api` purely because the build it shells out to was failing), and the 68 tests in `activityPhases/runtime.spec.ts` pass. Carried here rather than in a separate PR because this PR's checks cannot go green until it lands: the failed `packages/api` build cascades into e2e, MCP list_changed, bombadil and the Docker image jobs. |
||
|
|
5bd745783c
|
🖌️ style: Use correct text colour for answer textarea in dark mode (#14791) | ||
|
|
d920328bfa
|
💬 style: Unify Message Row Layout and Edit Surfaces (#14770)
* style: Unify message row layout and edit surfaces Route chat, share, and search messages through a shared MessageRow so user turns render as right-aligned bubbles and assistant turns keep a visible identity column. Replace per-part text editors with one edit surface that keeps tools, errors, and artifacts visible. Preserve non-text fields when saving content parts, copy the full serialized message, and hide hover actions that do not apply during streaming or errors. * style: Align edit footer and lighten editor field in dark mode Drop the divider above the user edit footer so both edit surfaces share the same footer treatment. Move the editor fields to surface-tertiary-alt. Light mode is unchanged at #fff, while dark mode lifts from #0d0d0d to #2f2f2f so the field sits above the #212121 panel instead of sinking into near-black. * style: Drop focus border and ring from message editors The editor fields changed border color and added a ring on focus. Keep the border static and rely on the app-level focus handling instead. * fix: Keep a triggered message action visible when the row is not hovered Hover actions fade out on non-last rows, and mobile.css only restored display and visibility for an active button, never opacity. Opening the fork popover therefore left it anchored to an invisible trigger once the pointer left the row. Skip the fade entirely while a button is active. Extract the recipe the three toolbars repeated so the rule has one home. Rework the streaming guard to the contract the toolbar now implements: edit and fork are omitted from a streaming response rather than rendered disabled, and the settled turn above keeps its own actions. It asserted the removed disabled-and-transparent behaviour and its opacity check only held because the growing response shifted the row out from under the pointer. * style: Trim message edit chrome and stabilize the status row The edit surface was a titled card sitting inside the conversation: a bordered panel with an "Edit message" heading wrapping bordered fields, which read as a settings dialog rather than an inline editor. Drop the card background, border and heading, and take the footer buttons down to the small size so the editor reads as a field in the message flow. The captured row goes from 253px to 187px. Move "Unsaved changes" into the footer and merge the rerun hint into the same slot. Both previously added their own row, so typing pushed the rest of the conversation down. The slot is clamped to two lines, which stays under the 36px button row, so the footer height holds at 36px regardless of which message is showing. * test: Cover message edit layout stability Add a mock e2e spec that measures the edit footer and section boxes and asserts they hold steady as the status text appears, for both the single-part user editor and a multi-part response. The multi-part case needs an assistant message with two editable parts, so add an E2E_THINK_REPLY marker to the fake model. Its think tags are parsed downstream by the agents stream pipeline, which yields a reasoning part followed by a text part. * fix: Read the fork popover open state from its store Fork mirrored the popover state into its own useState and reset it from an onClose prop. Ariakit 0.4 has no onClose, and React's DOM types accept the name on any element, so it type-checked, landed on a div and never fired. Closing by Escape or an outside click therefore left the button reading as active until the trigger was clicked again. Read the state from the store instead so every close path clears it. * fix: Keep the whole toolbar visible while an action is open Only the triggered button escaped the hover fade, so opening the editor or the fork popover left the row as a single floating button once the pointer moved away. Mark the active button and have every action in the toolbar key off it, so the group stays opaque for as long as a surface is open. The marker is a dedicated class rather than the existing `active`, which HoverButtons pins to the edit button of every assistant message and would hold those toolbars open permanently. The existing guard pressed Escape to close the editor while focus sat on the body, so the editor never closed and its assertion only held because the sibling faded regardless. Close the editor through its own control, and drop focus before measuring the fade now that Escape returns it to the trigger. * fix: Withhold copy while a response is still streaming Text-to-speech, fork and feedback were all withheld from a message that is still generating, but copy was rendered throughout, so the button offered to put half a sentence on the clipboard. Gate it on the same condition. That empties the toolbar for the duration, and SubRow collapses an empty row, so a streaming response now carries no actions at all until it settles. Both guards encoded the old contract: the unit test asserted copy was present and counted a single button, and the browser guard used copy as its proof that the toolbar had mounted. The settled turn above takes over that role. * fix: Move retry navigation to the outer edge of a user turn A user turn is right-aligned, but its sibling navigation rendered ahead of the actions, so the retry counter sat inboard of the icons instead of under the edge of the bubble it belongs to. Order it last on user turns. * fix: Ride the stream instead of chasing it Following a generating answer went through a helper throttled at 145ms, so the thread caught up in visible jerks rather than flowing. It now writes the scroll position directly on each frame, which is what an answer arriving a few pixels at a time actually needs, and glides only for the one long trip a turn makes, when sending has to travel from wherever the reader was down to the newest word. Whether to follow at all is now answered by where the reader is and which way they were going, rather than by the abort flag. `useMessageProcess` raises that flag on any wheel at all, downward ones included, through a throttle whose trailing call lands after the gesture has ended, so nothing timed to the gesture could outlive it. Scrolling down to the newest word could therefore never resume the ride, while the scroll-to-bottom button, which touches no wheel, always could. Arrival is judged on the scroll it produces rather than the wheel tick that started it, because wheel scrolling is animated and at tick time the thread is still far short of where the tick is taking it. Arriving also counts from further out than leaving does: while an answer streams the end recedes between the last tick and the frame that measures it, so judging arrival as tightly as departure leaves a reader unable to catch it at all. * fix: Reveal retry navigation on hover while an answer generates Copy, edit, fork and read-aloud are all withheld from a response that is still generating, which left the retry counter as the only thing rendering under a half-written answer. It now reveals on hover there, like the actions it sits with, and stays put on a settled turn. * fix: Keep a refused rerun from discarding the edit While a response is streaming, the edit action stays available on every earlier row, and those editors see a per-message submitting flag that is false, so Update and rerun is enabled. The send itself is still refused: ask() returns false for the duration of the active submission. Both editors ignored that and closed anyway, so the draft went with them and no rerun ever started. Both rerun paths now check the result and leave the editor untouched when the send is refused, so the work survives until the thread is free. * fix: Let an upward gesture beat the pending send glide Sending arms a smooth glide down to the newest word, and the landing re-pins the thread to the bottom. The landing was scheduled two ways, on scrollend and on a 700ms fallback, and neither was ever cancelled. A reader who changed their mind and headed up mid-flight was pinned again regardless, then dragged back by the next streaming resize. The fallback fires for the whole window, so this held even after the glide had visibly settled. The gesture now marks the glide interrupted, wherever it lets go of the bottom, and the landing stands down when it sees that. A glide the reader leaves alone still re-affirms the ride. * fix: Fade retry navigation on every streaming response format Every other action is withheld from the row that is still generating, so the retry counter is the only thing left under a half-written answer. The plain text row already faded it to hover-only there; the structured rows did not, and left it sitting on its own. Both structured paths now apply the same condition, and the class string the three of them share moves next to the hover action styles it belongs with. * i18n: Correct the copy the edit surface rewrite left behind The multi-part hint told the reader to save first and then rerun, but a save closes the editor and reopening seeds the drafts from what was just saved, so there is nothing left to rerun and the button stays disabled. Rerunning carries a single edited section by design, so the hint now states that limit rather than pointing at a step that is not there. Drop com_ui_save_submit as well: the per-part editor that used it is gone. * test: Make the message visual baselines opt-in The suite asserts sixteen screenshots and the repository tracks none, so Playwright's default treats every one as a miss and the mock e2e job fails on Linux. Baselines only compare cleanly against the machine that produced them, and nothing here can generate ones that match the runner image. The flows keep running and asserting their structure, which is where their value was; only the pixel comparison is now gated behind E2E_VISUAL_SNAPSHOTS. * style: Restore import order in the reworked message files The repository sorter and CI disagreed with what these files were left holding after the edit surface rework. No behavior change. * test: Follow the reworded rerun hint in the edit layout spec The multi-part hint was restated in the previous commit; this assertion still expected the old wording and would have failed the mock e2e suite. * fix: Leave the send glide alone while the answer streams in Every delta of an answer reruns the scroll effect, and the plain follow writes scrollTop outright, which cancels an animation on its first frame. So the glide a send starts was killed by the first token to arrive and the reader was snapped down instead of carried. The follow now stands down while a glide is travelling, which is what the hook already documented but only enforced on the resize path. * fix: Write a saved edit onto the thread as it stands An earlier turn stays editable while the newest answer streams, and the save captured the thread before the request but wrote it back after. Every delta that landed during the round trip was overwritten. Most of the time the next delta re-merged and the damage showed as a one-frame truncation, but a save that resolved after the stream's final write left the cache wrong for the rest of the session. The thread is now read once the request has resolved, which is what the content part editor already did. The editor actions in this file also wrap again rather than hold one unbreakable row, for the reason given in the following commit. * fix: Let the editor actions wrap on a narrow row At 320px an assistant turn gives the editor about 252px once page padding, the identity column and the row gap are taken out, and Cancel, Save and Update & rerun need more than that in English alone. The group was pinned with shrink-0, so it ran past the edge of the row instead of wrapping. A longer translated label makes it worse, and the user turn had no margin left either. Both editors wrap again, which is what the footer did before the status row was folded into it. * fix: Catch up to the new bottom when the glide lands Following stands down for the length of the glide, so an answer that arrives while it travels moves the bottom past the target the glide aimed at. A short response that finished before the glide reported landing left the thread a few lines short of its own end, with nothing left to correct it. Landing now closes whatever gap opened, unless the reader took over on the way. * test: Follow the renamed rerun button in the edit flow specs The button became 'Update & rerun' when the edit surfaces were unified, but two edit-flow specs still located 'Save & Submit' and would have waited for it until they timed out. A type comment named the old button too. * fix: Judge the first thread scroll against a real position The direction check seeded its last-position ref at 0, so the first scroll event on an opened thread, which arrives carrying a large positive scrollTop, read as a jump downward. Near the end that cleared the abort flag and re-pinned a reader to the stream they were scrolling away from. Take the first event as a baseline and judge direction from the next. * fix: Hold the content part editor to what it replaced EditContentParts took over from EditTextPart and left two of its behaviors behind. An emptied box now blocks Save and rerun instead of persisting a blank part. EditTextPart refused the same edit through its form's required rule and the sibling EditMessage still does, so both editors hold one line. The keyboard shortcuts reach the save paths directly, so they are guarded there too, and the footer says why the buttons are down. The editor also follows the chat direction again, taking dir and text alignment from the same setting EditMessage reads. * fix: Hold the footer height while a response streams Every action is withheld from the row that is still generating, and a lone sibling counter renders nothing, so the footer measured zero until the answer landed and then sprang to the height of the buttons. The transcript stepped upward under the reader at the moment a response completed. The placeholder that used to reserve this space went when the footer became unconditional, so hold the height on the row itself instead. * fix: Remember where the thread was put before judging a gesture Direction is judged against the last sample, and the thread is placed at its end without the reader touching it. With no record of where it was put, their first gesture was spent taking the baseline instead of being obeyed: a single PageUp cleared no flag of its own, so the next streamed resize rode the reader straight back to the end they were leaving. Every programmatic move now records the position it left the thread at, so the sentinel stands only until something has actually placed it. * fix: Spend the start of a turn only once it can be honored A reader who scrolls away during one answer leaves the abort flag raised, and nothing lowers it until the next connection opens, which is after this effect has already seen the send. Marking the turn as started on that first pass spent it against a closed gate: by the time the flag cleared there was no start left to honor, the reader was still detached, and the answer they had just asked for streamed on offscreen. Record the turn as started only on the pass that acts on it. * fix: Show the part edits that survived a refused save The editor saves every changed part through one button, but the endpoint takes a single part per call and nothing rolls a write back. A part the server refused therefore left the earlier ones stored while the editor reported that the message could not be saved, so cancelling from there walked away from edits that were already live. Record the writes that landed and reconcile the transcript with them whichever way the save ended. The refused parts are the only ones left holding a draft, so a retry no longer rewrites what already arrived. * fix: Stop a shared transcript from calling the sharer the reader The share row reused the chat view's user label, which reads "You". It is the screen-reader heading for the user turn, so anyone opening a share link heard every prompt the sharer wrote credited to themselves. Use the neutral "User" label on this surface. It keeps the localization the row gained, unlike the untranslated string it replaced. * fix: Let go of the stream when an interaction settles over several resizes Expanding a tool result mid-answer renders the container first and fills it once its contents arrive, so one gesture produces more than one resize. Only the first was credited to the interaction. The second read the reader as still riding the stream and put them back on the bottom they had just left. The suppressed resize now settles the ride as well as the near-bottom measure, using the position the interaction actually left the reader at, so an interaction that kept them on the end still streams. * fix: Edit inside a structured text part instead of flattening it A text content part holds either a string or a { value, annotations } object. The Assistants thread sync persists the structured form with its file citations intact, and the editor reads the part through the same union, so saving an edit wrote a bare string over the whole object and took every citation with it. The same object was handed to the tokenizer, which measures length, so a part that had been edited this way also stored a NaN token count. Write the edit into value, keep the rest of the part, and count the text itself. * fix: Keep a saved part's citations in the transcript it is written back to A text or think part holds either a bare string or a { value, annotations } object, and the editor already read both through getPartText. Writing the draft back into the local message cache put the string over the whole value, so a response carrying file citations lost them the moment it was edited and did not get them back until a refetch. Reading and writing now go through the same accessor, so an edit lands in the shape it was read from and the rest of the part survives. * fix: Let the message editor follow the chosen font size Editing a message dropped the draft to a fixed 14px regardless of the Font Size setting. On dev the textarea carried the markdown class, so it read --markdown-font-size like the rendered message does; restyling it into a bordered box replaced that with text-sm, and the new per-part editor was written the same way. Anyone on Extra Small, Large or Extra Large saw the text jump the moment they entered edit mode. Share the .message-content typography with the editors through a message-editor-text class so a draft is sized like the message it replaces and keeps tracking the setting. |