mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-04 13:38:46 +00:00
2398 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f50e38217
|
📎 fix: Never Let a Stalled Attachment Disable the Composer (#15013)
* 🖼️ fix: Keep Composer Send Enabled When an Attachment Stalls The composer's send button is gated on `hasIncompleteFiles(files)`, so any attachment that can never reach `progress: 1` reads as "still uploading" and disables send for the rest of the session — draft text intact, no error, no way out but removing the chip or reloading. Two paths could park an attachment there: - `loadImage` starts the upload from `img.onload` and had no `onerror`, so an image the browser refuses to decode (unsupported codec, truncated bytes, a revoked object URL) never uploaded at all and stranded the file at `progress: 0.2`. Drop the file and surface the error instead. - Upload completion reconciled against `temp_file_id`, the server's echo of the id the request was sent with, while every client-side handle for that upload — file map key, delayed-toast timer, recovery callbacks — is keyed by the id the client owns. A mismatch applied the completion update to a key that does not exist, leaving the attachment at `progress: 0.9`. Covered by unit regressions in the file-handling suite and a composer-level spec that drives a real upload through `ChatForm`, plus a render-bound guard on typing (react-scan measures one ChatForm render per keystroke in a browser; the guard fails on a multiplier). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cq3tev2rPbc2pWyVJwnh6u * 🧹 fix: Stop the Draft Restore From Clobbering Live Composer Attachments `restoreFiles` runs on every `QueryKeys.files` write — an upload landing, an SSE attachment mid-run — not just on a conversation swap, and it was written as if the draft were always the whole truth: - An empty draft cleared the composer outright. On the swap path that is redundant (the effect already clears explicitly one line earlier); on the cache path an empty draft only means the draft write has not caught up, so clearing there discards an attachment the user just added — and with no text typed, the send button has nothing left to submit. Restoring now only adds. - A match replaced the composer's entry with the persisted record, dropping the local `File`, the blob preview the chip renders from (`FileRow` falls back to refetching `filepath`), and the tool resource the upload was staged under, and stamping `attached: true` so removing a chip the composer still owns leaves the file orphaned server-side. It now layers the record over the live entry and leaves `attached` to files actually adopted from a draft. Confirmed against a real browser run: the entry is at `progress: 0.9` when this restore fires, so it — not the upload's own completion — is what was re-enabling send. react-scan render counts are unchanged (typing 20 keystrokes: 111 renders, ChatForm=20; attaching an image: 1373, FileRow=6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cq3tev2rPbc2pWyVJwnh6u * 🔗 fix: Keep an Attachment's Stored Temporary Id Equal to Its Map Key Two follow-ups from review on the upload reconciliation. Completion stored the server's `temp_file_id` echo in the entry's value while keying the map by the id the request was sent with. `useFileDeletion` deletes map entries by the value's own `file_id` and `temp_file_id`, so where the two disagreed — the exact case the reconciliation exists to tolerate — Remove would delete the file server-side and leave the chip behind, and the draft restore could not correlate its saved key with the cached record. Store the request id. A refused image decode also left its `uploadScope.recent` reservation behind: reservations are released by the render that observes the file in the shared state, which a decode failing before that render never reaches, and once the file is deleted no later render can either. The ghost is merged into every later batch's validation, so re-picking the same file reads as a duplicate and its size keeps counting against the composer's limits. Both covered; both new guards fail without their fix. Also sorts the composer spec's imports, which the static-checks import-order gate flagged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cq3tev2rPbc2pWyVJwnh6u * 🧷 fix: Normalize an Upload's Temporary Id at the Cache Boundary The composer keys its file map — and the draft it saves — by the `file_id` the upload request was sent with; `temp_file_id` is only the server's echo of that id. The previous commit reconciled the composer's own entry against the request id but left the record the mutation inserts into `QueryKeys.files` carrying the raw echo, and `restoreFiles` can only correlate a saved draft id by matching a cached record's `file_id` or `temp_file_id`. Where the echo disagreed the draft matched neither, so the attachment was silently dropped on the next conversation switch or reload — the same class of loss, one layer further out. Normalize once where the response enters client state, and hand the normalized record to the mutation's callers, so the cache, the composer entry and the draft all agree on one id. An agreeing response is passed through untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cq3tev2rPbc2pWyVJwnh6u --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c8953b8f32
|
🪂 fix: Land Navigation Auto-Scroll on the Rendered Thread (#15014)
The "auto scroll to latest message" setting stopped taking readers to the newest message when opening a conversation, most visibly on long threads. `useMessageScrolling` fired its landing on the conversation id alone. That id reaches the hook a commit or more before the tree does, so `scrollIntoView` ran against the OUTGOING conversation's rows: it scrolled that thread to its end, and — having no dependency on the tree — never ran again once the requested thread mounted. The reader was left at whatever offset the old thread's bottom happened to be, which on a long thread is the top. Key the landing on the conversation that owns the RENDERED rows instead, using the same `messagesTree[0].conversationId` fallback `MessagesView` already uses to key the mount window, and land once per conversation so the tree identities a stream mints cannot haul back a reader who scrolled away. This is independent of the progressive row mounting: that window only ever grows upward from the newest row, so the end of the mounted content is already the end of the thread, and the landing needs no full mount to be correct. Measured against the real client (react-scan render tallies over a 10-message to 120-message navigation), render counts are unchanged at ~16k and the thread still mounts progressively; distance from the bottom on arrival goes 841px to 0. With progressive mounting disabled the same navigation landed 15421px from the bottom, confirming the anchoring was masking this rather than causing it. Also moves the `autoScroll` setting from Recoil to Jotai, keeping the same `autoScroll` localStorage key so a stored preference survives, and matching the `showThinking`/`smoothStreaming` atoms already served through `ToggleSwitch`. Claude-Session: https://claude.ai/code/session_01BDQSLdbwvtSqCmQSw7Nz91 Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b91691937e
|
🙋 fix: Free the Composer When a Question Pause Collapses (#15011)
* 🙋 fix: Free the Composer When a Question Pause Collapses Collapsing a live `ask_user_question` left the user with nothing to do. A batch of questions disables the composer, the send button, and the stop button for as long as the pause is active — and `collapse` deliberately keeps it active, while hiding the popover that carried the only dismiss. After the chevron there was no way to type, send, or stop the run short of reloading the page. Split the composer's role out of `active`: `composerAnswers` (a single question, answered IN the composer) and `composerLocked` (a batch, answered in its own card — and only while the popover is up). Collapsing a batch now hands the composer back to the thread; the stop button follows `composerAnswers`, so a paused run stays stoppable. Both collapsed cards also carry the popover's ×, so dismiss survives the handover, and `submitText` declines a batch's composer text instead of claiming it — the old `return true` reported success and dropped whatever was staged when the pause began. Contrast, per feedback that the questions were hard to read: the answer options, the answer textarea, and the digit chips all drew their edge from `border-light`, which measures 1.20:1 against the panel (WCAG 1.4.11 wants 3:1 for a UI component boundary) — a column of choices read as flat text. Adds a `choice` Button variant carrying its own fill and a `border-xheavy` edge (5.49:1 dark / 6.54:1 light), at `font-normal` so the question above stays the heading, and replaces the single-question popover's hardcoded `bg-white`/`dark:bg-gray-700` with the semantic surface role it should have been using. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPtmUb6VLBhhxXkS3PfV6r * 🧹 refactor: Render Popover Answers Through the Choice Variant The popover's option rows re-stated the shared `choice` variant's border, fill, weight, and hover on a raw `<button>` — the same answer control as the cards', so a later fix to the variant would have drifted the live popover away from them. Renders `Button variant="choice"` instead, keeping only what the popover actually owns: the full-width row layout and the keyboard highlight. Locked rows now take the primitive's `disabled:` styling rather than a local `cursor-not-allowed opacity-60`, matching the cards. ----- |
||
|
|
e4d6bb71f9
|
📁 feat: Surface Stateful Workspace Downloads (#14984)
* feat: surface stateful workspace downloads * fix: sort workspace change imports * fix: reuse workspace button primitives * fix: hide collapsed workspace actions |
||
|
|
da0491d5db
|
💻 fix(agents): require Code Interpreter for programmatic MCP tools (#14977)
* fix(agents): require code interpreter for programmatic MCP tools * test(data-provider): fix tool options fixture type * fix(agents): address programmatic tool review feedback * fix(agents): avoid no-op update on version revert |
||
|
|
006e421cd2
|
💡 feat: add DB-backed admin insights (#14898)
* feat: add Mongo-backed admin insights * feat: gate insights with environment variable * fix: tighten insights access and activity metrics * fix: preserve insights date selections * perf: parallelize insights search aggregation * test: wait for MCP conflict recovery * test: satisfy strict MCP recovery typing * fix: disable insights pagination while loading * fix: localize insights range shortcuts * fix: bound insights search input |
||
|
|
547bd8c4bf
|
🧵 feat: Persist View-Only Subagent Threads (#14957) | ||
|
|
7480e93181
|
🌐 fix: Scrollable Language Dropdown in Shared Chat Settings (#14954)
* fix: make the shared chat language dropdown scrollable and use available height The language dropdown in the shared chat settings dialog could not be scrolled with the wheel and was capped at 256px, so most of the language list was unreachable. Radix wraps the dialog overlay in RemoveScroll with its shards limited to DialogContent, so wheel events over a popover portaled to document.body were cancelled. That same portal placement also left the popover inside Radix's aria-hidden treatment, hiding the whole option list from assistive technology. Render the popover inside the dialog and let that dialog's content overflow so the popover is not clipped by it. Drop the hardcoded max-height so the popover uses the available height reported by the positioner. This also restores flipping, because the positioner can now see that the natural height overflows and place the popover above the trigger when there is more room there. Remove declarations that never took effect: max-h-[80vh] and overflow-y-auto on the popover, both shadowed by .popover-ui later in the same stylesheet, and the --anchor-max-height and --anchor-max-width custom properties, which nothing reads. Move the theme and language selectors into their own directory so the public share page no longer imports through the Nav settings tabs. * chore: drop the redundant nested winston entry from the lockfile packages/data-schemas declares winston as a peer dependency of ^3.17.0, which the root winston 3.19.0 already satisfies, so npm deduped the nested 3.17.0 copy. * refactor: give Dropdown separate wrapper, trigger and popover class props className was spread onto three elements at once: the positioning wrapper, the trigger button and the popover. A caller styling the trigger silently restyled the popover as well, and because className was merged after sizeClasses it also beat the popover's own sizing. LangfuseConnection asked for a popover the width of its anchor and got a full width one instead. className now applies to the wrapper only, triggerClassName styles the trigger and sizeClasses continues to style the popover. Call sites that relied on the old spread pass the class to the part that needs it, so the rendered result is unchanged apart from the LangfuseConnection width. Also add portalElement so a caller can render the popover into a specific container rather than document.body. * fix: align the packaged popover radius with the app stylesheet .popover-ui is declared both in the component's own stylesheet and in the app's, and the two had drifted: the packaged copy used a 1rem radius while the app used 0.7rem. The app copy wins inside LibreChat, so consumers of @librechat/client saw a different corner radius from the app itself. * fix: keep the shared chat settings dialog scrollable The dialog content was made overflow visible so the language popover would not be clipped, which meant the dialog itself could no longer scroll. If it ever grew past the viewport its content would have been unreachable. Move the scroll onto an inner region and portal the popover into the dialog content, outside that region. The popover still sits inside DialogContent, so it stays within the scroll lock shard and out of the aria-hidden subtree, while the rows above it can scroll on their own. * style: format the locales README Applies the repository Prettier style, which the file did not satisfy. Formatting only, no content changes. * chore: remove the unused DropdownNoState component The file defined a HeadlessUI based dropdown that nothing imported. It was absent from the package barrels and from the generated type declarations, so it was never part of the published API and no consumer can be relying on it. It carried the same defect the Ariakit Dropdown just had, spreading className onto the wrapper, the trigger and the popover, so deleting it is preferable to fixing code that never runs. * fix: declare the dependencies packages/client imports InputNumber imports the ValueType type from @rc-component/mini-decimal and the generated declarations re-export that import, but the package never declared it. It resolved only because npm hoists it as a transitive dependency of rc-input-number, so a consumer on a strict or nested layout would fail to resolve the type. Declare it as a peer alongside the other externals, using the same range rc-input-number asks for. The theme test requires tailwindcss directly, so add it to devDependencies rather than relying on hoisting there too. Also mark the ValueType import as a type import, matching the convention used elsewhere. * style: group the ValueType import with the package imports Type-only imports belong before local imports, as in Avatar.tsx. |
||
|
|
7d62be2ad3
|
🕸️ feat: Run Saved Agent Teams as Subagents (#14944)
* feat: Add graph subagent integration * style: Sort response usage test imports * fix: Preserve lazy graph runtime context * fix: Use isolated graph input helper * test: Align graph integration fixtures * fix: Preserve lazy graph runtime capabilities * fix: Bound lazy graph metadata preload * fix: Harden lazy graph resolution lifecycle * fix: Coalesce lazy graph member resolution * fix: Snapshot initialized graph members only * fix: Preserve lazy agent runtime context * fix: Preserve batched lazy context preparation * fix: Preserve graph member capability bounds * fix: reconcile graph subagents with execution profiles * style: align graph subagent types with formatter |
||
|
|
f9876eaaf0
|
🪜 style: Step Through Batched Questions One at a Time (#14935)
A batched `ask_user_question` interrupt rendered every question stacked in one scrolling form, which reads as a wall on mobile and desktop alike. Show one question per step instead, with clickable progress dots, Back/Next, and Submit only on the last step. The batch contract is untouched: one interrupt, one answer map, Submit still gated on every question having an answer, Skip still declines the whole batch from any step. Single-question batches render exactly as before. |
||
|
|
df294fa474
|
🧩 refactor: Resolve Tool-Card State Once (#14934)
* 🧩 refactor: Resolve Tool-Card State Once (AI-1810) Each tool card derived its state several times over — the visible label from one expression, the `aria-live` announcement from another, the icon and shimmer from a third, and since #14906 the follow-scroll from a fourth. Nothing tied them together; they agreed only because each was written to agree. Thirteen of the seventeen review findings on #14873 were instances of one derivation being updated and another left behind, and #14892 added more. `resolveToolCallPhase` is now the single source: one function encoding the precedence rules, each of which a specific review finding established, returning `running | completed | cancelled | failed`. Everything the card shows reads that value. `ProgressText` takes `phase` in place of the `error` + `errorSuffix` pair, which encoded three terminal states in two booleans — `error` meant cancelled, a present `errorSuffix` meant failed — and made every consumer reconstruct the distinction. That shape is precisely what let a duration render beside "failed" (Codex round 1 on #14892). Two things fell out once the state had one home, both dead code rather than deletions of behaviour: - `progress` left `ProgressText` entirely; the phase already carries everything it was used to decide. - The `useProgress` mask went with it. Passing 1 in still matters — it stops the 200ms interval — but masking the output no longer does, because the phase treats an explicit close as terminal outright. The "both halves are load-bearing" subtlety is now one half. Scope: the nine cards that render the shared `ProgressText`. The three with bespoke layouts (`WebSearch`, `SubagentCall`, `OpenAIImageGen`) still resolve their own state and are the natural follow-up — they can adopt the resolver without adopting the component. Refactor-only. 4891/4891 client tests pass unchanged, including the suites that encode the cancelled/failed precedence in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Infer Cancellation From Reported Progress, Not The Animation `useProgress` holds below 1 for ~200ms after a call reports completion: it emits the previous value, then `0.99`, then `1` on a timeout. The resolver read that animated value for its cancellation inference, so a successful call whose submission ended inside that window rendered — and announced — as "Cancelled". The input is now split. `reportedProgress` is what the stream said and drives the inference; `displayProgress` is the animated value and drives `running` vs `completed`, so the label and shimmer still follow the animation rather than snapping. This restores `ToolCall` and `RetrievalCall`, whose previous predicates used `initialProgress` and were immune, and additionally fixes `useToolCallState`, which inferred from `rawProgress` and therefore carried the bug already — every card the hook backs was exposed to it before this PR. Three tests cover the window: a reported-complete call mid-settle is `running`, a genuinely unfinished one is still `cancelled`, and the card settles to `completed` without a cancelled frame in between. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧹 chore: Drop Unused Phase Predicates; Correct A Stale Comment `isFailedPhase` and `isRunningPhase` had no callers — every consumer compares the phase directly, which reads better than a wrapper. An unused abstraction is the thing this PR argues against, so it should not ship one. The comment above the hook's resolver call still described "the raw progress the legacy heuristic was written against", which stopped being true when the input split into reported and display progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b00a6717e7
|
🌍 i18n: Update translation.json with latest translations (#14919)
Co-authored-by: danny-avila <110412045+danny-avila@users.noreply.github.com> |
||
|
|
6b9fe97990
|
🎬 style: Reveal the Chat on Programmatic Drawer Closes (#14930) | ||
|
|
57ea1137f6
|
🛡️ feat: Let Admins Restrict Stateful Workspace Scopes (#14910)
* feat: let admins restrict stateful workspace scopes * fix: enforce stateful scope policy across agent paths * fix: close stateful scope policy activation gaps |
||
|
|
6eb2249620
|
📱 perf: Instant Mobile View Switching + Uniform Sidebar Toggle (#14913)
* ⚡ perf: Stabilize the Assistants Map Context Value * ⚡ perf: Start the Mobile Drawer Slide Before the State-Flip Commit * 📱 style: Mirror the Header Sidebar Toggle in the Mobile Drawer * 🧷 fix: Apply Reduced-Motion Flips Synchronously, Drop Stale Deferred Flips * 🎨 refactor: Promote the Sidebar Toggle Look to a Button Variant * 🧷 fix: Drive Focus and the Release Deadline From the Commit, Not Timers * 🚪 fix: Route Every Sidebar Mutation Through the Animated Toggle * 🛝 fix: Carry Navigation and Focus Past the Slide, Toggle the Latest Intent * 🆕 fix: Slide Before the New-Chat Reset, Drop Superseded Deferred Flips |
||
|
|
485abef3fa
|
🥚 refactor: Default Agents to Preferred Stateful Workspace Scope (#14908)
* feat: add user default for stateful agent workspaces * style: sort stateful workspace imports |
||
|
|
fdc9c77f6e
|
🗄️ feat: Archive All Chats From Data Controls (#14885)
* feat: archive all chats from data controls
Adds an "Archive all chats" row under Data controls > Your data, next to
Shared links, with a confirmation dialog. It calls a new
POST /api/convos/archive/all endpoint backed by archiveAllConvos, which
archives every conversation currently visible to the user in a single
updateMany and refreshes the stats of every chat project the archived
conversations belonged to.
Temporary and retention-expired conversations are skipped: they are
already hidden from the chat list, so archiving them would only surface
them in the archived view. The update runs with timestamps disabled so
each conversation keeps its own updatedAt and the archived list stays
sorted by real activity.
Archiving a conversation now also drops the new-chat message cache alias
for it. A chat's first turn writes the same message array under both the
conversation key and the new-chat key, so without this the messages of a
just-archived chat kept rendering on the new chat screen until a reload.
Deleting already handled this; archiving did not.
* fix: keep archive-all state consistent
* fix: drop stale detail caches after bulk archive
* fix: harden archive-all request handling
* fix: reconcile archive batch failures
* Fix project stats refresh races and archive route boundary
* Fix archive-all review findings
* Fix archive scan index and partial-batch stats refresh
Reconcile project stats for already-committed archive batches when a later
batch fails, and index the archive scan as { user, _id } so non-tenant
pagination can use _id order.
* Fix Recoil reset after a partial archive-all failure
Refetch the submitted conversation on error and start a new chat only
when that conversation is still active and already archived.
* Fix archive recovery from resetting a newly opened chat
Re-read the active Recoil conversation after the archive-state lookup
resolves, so a slow getConversationById cannot start a new chat if the
user already opened another conversation.
* Fix project-stat reconciliation after archive races
Keep retrying optimistic project-stat writes instead of returning a
stale document after three lost CAS attempts, and retry destination
project discovery after a transient distinct failure.
* Fix archive reset and project-count increment races
Leave already-archived chats open after archive-all, recount new
project conversations instead of incrementing, and skip a delayed
increment when a concurrent refresh already recorded that chat.
* Recover destination projects after discovery retries exhaust
Keep committed conversation IDs when post-archive distinct fails, then
rediscover those projects in finally so a moved conversation's
destination still gets reconciled after the error is rethrown.
* Fix archive-all recovery batching and remount pending state
Recover destination projects in 500-id chunks so the final lookup
cannot exceed Mongo's command size, and share archive-all pending
state through a mutation key so Settings remounts stay disabled.
* Stamp bulk-archived chats and refresh the pinned cache
Bulk archive wrote only isArchived, so the archived table dated every
swept chat by createdAt and the default archivedAt sort dropped the whole
run into the legacy null group. Stamp one timestamp for the sweep; the
filter only matches unarchived chats, so an existing stamp cannot move,
and timestamps: false still preserves each updatedAt.
The pinned section fetches on its own key with a five-minute stale time,
so an archived pin kept rendering in the sidebar until that expired.
Invalidate it alongside the other lists on both success and failure.
Also drop the async from the failing-batch updateMany mock: its
Promise<never> is not assignable to the Query return type, while a plain
synchronous throw types as never.
* Bound archive recovery state with the sweep marker
Recovery held every committed conversation id for the life of the
request so the finally block could re-run project discovery after an
in-loop distinct gave up. Slicing that array into 500-id queries capped
the BSON command size but not the heap, so a very large history could
exhaust a worker mid-archive.
The archivedAt stamp already identifies exactly what this call
committed, so recovery is now one distinct scoped to it. That filter is
a prefix of the existing user/isArchived/archivedAt index, and the two
discovery call sites collapse into one filter-taking helper.
* Reconcile archive stats when a write outcome is unknown
A batch that commits but whose result never returns, a stepdown or a
connection drop between commit and acknowledgement, left archivedCount
at zero, so the finally block skipped both marker recovery and the stats
refresh. The chats were archived, so no retry could find them again: the
sweep filter no longer matches them and their projects kept stale
counts.
Both now key off the write attempt rather than the returned count.
Nothing else needs to change, because the marker is stamped by the same
write whose result went missing.
* Retry dropped project refreshes and guard stale pointer writes
Two ways a project could keep stale stats after archive-all.
A refresh that rejected was logged and dropped for good. Its chats are
archived, so no retry of archive-all can find them again to recompute
against, and the likeliest rejection is the recoverable one:
refreshChatProjectStatsForUser gives up when the project changed under
every compare-and-set attempt. Failures are now collected and replayed
once the rest of the run has stopped competing with them.
A save already in flight could also undo the sweep. Its conversation
document still said visible, so its tail took the pointer branch and
wrote lastConversationId back to a chat the sweep had just archived,
leaving the project advertising activity on a chat the workspace hides.
The pointer write now confirms the chat is still visible first, and
recomputes the project when it is not.
* Verify project pointers after the write, not before
Checking visibility before the pointer write only moved the race earlier:
a sweep landing between the check and the update still archived the chat
and cleared the project, and the write then restored it as
lastConversationId.
The check now runs after the write and repairs instead of preventing. A
sweep that lands earlier is caught here; one that lands later refreshes
the project itself, and refreshChatProjectStatsForUser compare-and-sets,
so it cannot commit a count it read before this write. Same single
indexed read as the check it replaces.
|
||
|
|
7ebf6b2548
|
📋 feat: Attach Long Pasted Text as a File (#14884)
* Attach long pasted text as a file Pasting more than 2500 characters into the composer now attaches the text as pasted-text.txt instead of filling the message box. The text still reaches the model in full: the attachment is routed to the context tool resource, which inlines it verbatim. Shorter pastes and pasted files keep their existing behavior. Add a "Paste long text as a file" toggle under Settings > Chat > Sending, on by default and persisted locally. Number successive pastes so uploads, which dedupe on name, size and type, do not reject a second paste that merely matches the first one's length. handleFiles now reports whether files were accepted, so the "Attached as text" toast is held until the attachment actually happens instead of pairing a success message with a rejection error. * fix: Respect long paste threshold * fix: Preserve long paste semantics * Fix long-paste upload failure recovery and copy * Fix concurrent paste upload recovery * Guard asynchronous paste recovery * Fix long paste handling in the composer * fix: skip delayed paste recovery in answer mode * fix paste recovery cleanup on attachment removal * fix paste recovery across drafts and reloads * fix paste recovery isolation across side-by-side panes * fix idle new-chat draft isolation and paste replacement recovery * fix pane-scoped draft cleanup and multi-paste restore offsets * fix paste recovery around run end, live uploads, and draft edits * fix paste recovery when both sides of the caret were edited * fix new-chat draft cleanup, pane-scoped abort recovery, and one-character snapshots * fix paste persistence failures and pane-scoped file routing * fix paste recovery before upload wait and blocked storage reads * fix new-chat draft clearing, paste name collisions, and stale composer uploads * keep the composer draft across late agent metadata refreshes * resolve paste anchors by their unique intact junction * anchor paste recovery to the junction nearest the captured caret * honor the paste setting before file config lands and migrate pending drafts one copy at a time * route pastes past the pending file config and chunk large recovery encoding * sort imports in useAutoSave |
||
|
|
e736fcfa09
|
🏷️ refactor: Keep Agent Conversations From Revealing Model Labels (#14909) | ||
|
|
107050396e
|
📜 feat: Follow Streaming Args in Tool Detail Panes (#14906)
* 📜 feat: Follow Streaming Args in Tool Detail Panes * 📜 fix: Gate Follow-Scroll to Expanded Panes, Re-Pin on Highlight Commit |
||
|
|
27ed491a2a
|
🏷️ fix: Persist the Ephemeral Agent's Display Label as Sender (#14899)
* 🏷️ feat: Add getEphemeralSender and Cover the Ephemeral-Id Format * ♻️ refactor: Consolidate the Ephemeral Sender Chains * 🏷️ fix: Decode the Ephemeral Sender for Persisted Messages * 🏷️ fix: Mirror the Persisted Sender Chain in useGetSender * ✅ test: Widen the Custom-Endpoint Fixture Type * ✅ test: Expect the Spec Label in the Composer Placeholder * 🏷️ fix: Resolve the Sender from Exact Labels, Not the Lossy Id |
||
|
|
c939a6fb17
|
📱 feat: Swipe the Mobile Drawer Open and Closed (#14902)
* 📱 feat: Swipe the Mobile Drawer Open and Closed * 📱 fix: Harden the Drawer Swipe Against Interrupts, RTL, and Cold Mounts * 📱 fix: Track the Initiating Touch and Settle Only What the State Confirms * 📱 fix: Resolve Interrupted Drags to the Current State and Scope Overscroll |
||
|
|
1b7e2a4e6a
|
⚡ perf: Optimize First Load of Large Conversations (#14901)
* ⚡ perf: Index the Conversation Fetch and Trim the Client Message Projection * ⚡ perf: Memoize the Message Tree per Cache Write * ⚡ perf: Serve Message Reads via the Trimmed Projection and an Ownership Probe * ⚡ perf: Defer Collapsed Disclosure Bodies Until First Expansion * ⚡ perf: Progressively Mount Long Threads from the Scroll Anchor * 🩹 fix: Address Codex Findings on Retention, Anchoring, and Cache Bounds * 🩹 fix: Poll the Oversized Export Precondition Through the Progressive Mount * 🩹 fix: Keep Video Results in the Client Message Projection |
||
|
|
df5abbb377
|
🖱️ fix: Reveal Message Metadata on Hover, Not on Click (#14900)
* fix: reveal message metadata on hover, not on click
The message timestamp, the provider/model label crossfade, and the hover
action toolbar all revealed on `:focus-within` over the message row. A mouse
click sets focus, so clicking a tool card, an expand toggle, or a code block
button parked focus inside the row and pinned all three open long after the
pointer had left.
Key the focus half of each reveal on `:focus-visible` instead. A pointer
click no longer counts, while keyboard focus still does, so a sighted
keyboard user still reaches the model name and the timestamp by tabbing. An
action that opens a surface keeps the toolbar up through `hover-button-active`
as before.
* fix: fade the message row reveal instead of snapping it
The footer actions carried no opacity transition at all, so they arrived in a
single frame while the timestamp eased in behind them over 200ms and the
provider/model crossfade ran on the 300ms card-resize spring it had borrowed.
One hover, three different arrivals.
Put all three on the shared `duration-theme-normal` motion role with a common
ease-out, and add the reduced-motion guard the timestamp and the footer were
missing. `MinimalHoverButtons` now composes the shared reveal helper rather
than repeating its classes inline.
The reveal transition names `color` and `background-color` alongside `opacity`
because `cn` merges the whole `transition-*` group: a bare `transition-opacity`
would replace the `transition-colors` a `Button` contributes and the hover tint
would snap.
* fix: widen the message row keyboard-focus test
Two gaps in the `:focus-visible` reveal, both raised in review.
`:has()` never matches its own subject, so keying the reveal on
`:has(:focus-visible)` missed the row element itself. `MessageNav` moves the
reader by setting `tabindex="-1"` on the row and focusing it, which left a
focused row showing its focus ring while its timestamp, its model name and its
actions all stayed hidden.
Text-entry controls match `:focus-visible` even when a mouse clicks them, so a
click into the textareas `ToolApproval` and `AskUserQuestion` render inside a
row still pinned that row's metadata open with the pointer somewhere else. They
are excluded from the descendant half of the test. Every toolbar action is a
button, so a keyboard user still never focuses a hidden one.
Both halves are now one condition,
`:is(:focus-visible, :has(:focus-visible:not(:is(input, textarea, [contenteditable]))))`,
applied to the timestamp, the header label and the footer actions alike.
* fix: split the row focus test into two variants
Folding the row-itself and descendant halves into a single
`group-[&:is(...)]` made Tailwind emit a bare `.group$ { opacity: 1 }`, which
lightningcss refuses to minify. That failed the client CSS build and every job
downstream of it while jest and tsc stayed green, because neither ever builds
the stylesheet.
The condition is unchanged in behaviour, expressed as `group-focus-visible`
plus `group-has-[:focus-visible:not(:is(input,textarea,[contenteditable]))]`.
The plain CSS in style.css keeps the `:is()` form, which is valid there.
The stale string also had to come out of the specs: tailwind scans
`src/**/*.{ts,tsx}`, so a class literal in a test file reaches the production
stylesheet.
* fix: split the timestamp focus selector too
`:has()` nested inside `:is()` made postcss log "Failed to parse selector" on
every client build. The rule survived intact, but the warning was noise coming
from this change, and splitting it matches how the Tailwind side now expresses
the same condition.
Behaviour is unchanged: hover, a focused row and a mouse-clicked textarea all
measure the same as before.
|
||
|
|
7d850c308a
|
🧠 feat: Add Live Reasoning Labels (#14893)
* feat: add live reasoning labels * fix: Stabilize reasoning label checks * fix: Address reasoning label review findings * chore: Bump Agents SDK for reasoning labels * fix: Reset reused reasoning step evidence * fix: Reconcile cleared reasoning labels * fix: Fence reasoning label resets * fix: Reset reasoning ownership before gap labels * fix: Preserve THINK type through label reset * test: Expect run-global reasoning revision |
||
|
|
3bd2358805
|
🗜️ feat: Let Users Toggle Client-Side Image Resizing (#14883)
* feat: allow users to toggle client-side image resizing Client-side image resizing could only be configured in librechat.yaml and defaulted to off, so users had no way to enable it for themselves. Add a "Resize images before upload" toggle in Settings > Chat > Sending, stored per device in localStorage. When librechat.yaml sets clientImageResize.enabled, mergeFileConfig marks the value as enforced and the toggle renders the admin value read-only. Admin resize parameters still apply without locking the toggle when enabled is omitted. Also fix shouldResizeImage, which compared file size against 10% of a 512MB fallback limit and so only triggered above roughly 51MB. It now uses a 512KB floor, which lets the setting affect everyday photos. * fix: harden client image resizing * fix(client): restrict image resizing and localize toast * fix: harden client image resizing * fix(client): recognize static WebP image chunks * fix(client): clamp resized image dimensions * fix(client): enforce safe image resize uploads * fix(client): recheck duplicates after transforming uploads * fix(client): keep selected files after input reset * fix(client): disable image resizing when file config fails * fix(client): decode resize candidates without a base64 copy * fix(client): coordinate upload batches across hook instances * test(client): drop the untyped conversation from shared upload state * fix(client): start the config wait before queueing uploads * fix(client): disable the resize switch while file config is pending The switch stayed clickable during the initial file-config load even though the checked state cannot update until that query settles. * fix(client): only track upload reservations for observable state |
||
|
|
2b1644406a
|
💄 style: Align the Thinking Dot with the Header Icon (#14895)
* 💄 style: Align the Thinking Dot with the Header Icon * 💄 style: Keep the Dot Nudge Logical and Gated to the Header Axis * 💄 style: Route the Seeded Empty-Text Placeholder Through the Nudged Cursor * ♻️ refactor: Guard MemoryArtifacts on Its Memoized List |
||
|
|
832bac39ad
|
🗄️ feat: Record When a Conversation Was Archived (#14863)
* feat: record when a conversation was archived The archived chats dialog has a "Date Archived" column that was bound to createdAt, so it showed when the chat was created rather than when it was filed away. Nothing recorded the latter. Conversations now carry archivedAt, set on archive and cleared on unarchive, and the column reads it. Chats archived before the field existed have no stamp and fall back to createdAt, which is exactly what that column already showed for them. The archive view sorts on the new field. archivedAt is absent on every previously archived chat, so the missing-value group is the common case here rather than an edge case: the cursor's null handling, written for titles, now covers both, and an absent stamp survives the cursor as null instead of collapsing to the epoch and replaying the whole archive. * fix: address review findings on the archived-at stamp - Protect `archivedAt` from saveMessageToDatabase's unset sweep. Any persisted field missing from endpointOptions is unset, so sending a message in an archived chat cleared the stamp while leaving isArchived true, silently dropping it into the legacy fallback group. - Order the legacy group by the createdAt the dialog displays rather than by last activity. The cursor's secondary key is now chosen per sort field, so the fallback the cell renders and the order the server returns cannot disagree. - Put that secondary key in the archive index too, so paging the legacy group does not fall back to a blocking sort. * fix: keep archivedAt on a redundant archive request Opening an archived chat and hitting the archive shortcut, or retrying the POST, sent isArchived: true again and replaced Date Archived with now. saveConvo now stamps only on the unarchived-to-archived transition and still clears the field on unarchive. * fix: make archive timestamp updates atomic * test: type the archive race spy against the driver signature * fix: archive without an aggregation-pipeline update DocumentDB documents no support for pipeline-form updates on any engine version, and the repository's compatibility assessment records that a prior P0 rewrote the three that existed. Stamping archivedAt through a $cond pipeline reintroduced one, which would have sent every archive and unarchive to the route's 500 handler on a supported 5.0 deployment. The conditional stamp is now a compare-and-set on isArchived, which keeps the transition atomic without a pipeline: only the write that finds the chat unarchived stamps it, so a duplicate or retried archive leaves the original date alone and an unarchive that lands first is re-stamped. Schema defaults and createdAt-on-insert go back to mongoose's own setDefaultsOnInsert and $setOnInsert, and tenantId is once again stripped by the tenant-isolation plugin rather than by hand. * fix: do not report a racing archive as a missing chat Both conditional writes of the compare-and-set miss when the archive flag flips between them: the chat was already archived when the transition write ran and unarchived again before the already-archived write. saveConvo returned null for a conversation that plainly exists, so POST /api/convos/archive answered 404. Confirm the conversation is really gone before accepting that result, and retry the pair when it is not. An unknown id still costs one existence read and falls straight through to the 404. * fix: resolve a fully contended archive to the chat's real state Alternating archive and unarchive requests can split every attempt of the compare-and-set: each transition write sees the chat archived and each already-archived write sees it unarchived. Exhausting the retries therefore proved nothing about whether the conversation exists, and the no-upsert archive route turned a lost race back into a 404. Read the conversation once more when the retries run out and answer with its actual current state instead. |
||
|
|
fb8ae881cf
|
⏱️ feat: Show Run-Step Durations On Tool Cards (#14892)
* ⏱️ feat: Show Run-Step Durations On Tool Cards Surfaces how long each tool call took, derived from the `closed_at` / `created_at` pair already carried by `on_run_step_closed` — the same event #14871 and #14873 use for the terminal status. No new event, no new SDK surface. The duration is stamped onto the content part at the same three sites as `runStepStatus`, so it survives a reload and a resumable reconnect rather than living only on the live React message: - `callbacks.js`, on the aggregated part before the event is forwarded - `RedisJobStore`, in the host-authored replay reconstruction branch - `useStepHandler`, on the live message Rendering lands in the shared `ProgressText`, which nine tool cards already use, rather than in each card: one place decides whether a duration is shown and how it reads, and the cards only forward the number. That keeps this from adding a tenth independent state derivation to a component family whose label/announcement/progress split is already the subject of AI-1810. The value is deliberately absent rather than zero whenever it would be a guess — no `created_at`, non-finite input, or a negative elapsed time from two clocks that disagree, which is now reachable because a step can be opened in one process and closed in another after a checkpoint resume. Sub-second durations are suppressed as noise, and it renders only on a settled, non-error card, where the slot is not already carrying the cancelled icon or the error suffix. For assistive technology the compact form (`3.5s`) is hidden and paired with a spoken equivalent ("took 3.5 seconds"), both inside the button, so the accessible name carries the duration without an `aria-live` region re-announcing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🎨 style: Sort Imports In Touched Files The import-sort gate runs against the files a PR changes, so pre-existing drift in `ProgressText.tsx` and `RedisJobStore.ts` surfaced on this branch. Both were already unsorted on `dev`; this is the sorter's output, with no semantic change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Accept Partial Timestamps In Run-Step Duration Helper `getReportableRunStepDurationMs` declared its parameter as `Pick<RunStepClosedEvent, 'created_at' | 'closed_at'>`, where `closed_at` is required. That contradicted the function's own purpose: every guard inside it exists precisely to handle stamps that may be missing. The Redis replay branch reconstructs closures from persisted JSON and holds nothing stronger than "might be a number", so it failed to typecheck against the narrower signature. Widened to an exported `RunStepTimestamps` shape with both stamps optional, rather than asserting at the call site — an assertion would move the decision about what is trustworthy somewhere it cannot be enforced, which is the thing the helper exists to centralize. Callers holding a fully-typed event still pass, since a required field satisfies an optional one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Suppress Duration When Failure Arrives As errorSuffix Alone At every call site `error` carries cancellation while failure travels through `errorSuffix` with `error` false, so gating the duration on `!error` alone rendered "· 3.5s" beside "· failed" — and announced it. The gate now checks both terminal-failure channels. The original test pinned only the `error: true` path, which is why this survived; the failed-via-suffix path is now pinned separately, both the visible and the announced half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧩 refactor: Persist Raw Run-Step Durations, Threshold At Render Only The three stamp sites filtered through the 1-second reportability threshold before persisting, baking a presentation rule into stored data: a 900ms step stored nothing, making "fast" indistinguishable from "not derivable" and unrecoverable if the display rule ever changes. Stamp sites now persist the raw `getRunStepDurationMs` value — absent only when genuinely not derivable — and the renderer alone decides what is worth showing, which `ProgressText` already did. Rendering is unchanged. `getReportableRunStepDurationMs` is removed; it existed only to serve the write-time filter, and a test now pins that sub-threshold durations survive to storage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Suppress Duration On Backgrounded Bash And Code Cards A backgrounded call's run step closes when dispatch returns the handle, so the stamped duration is the dispatch time. Rendering it beside "Running/Finished in background" misstated a detached task's runtime as seconds — and violated the "settled card only" rule, since the card is still tracking the detached run. Scope is exactly the two cards that parse background handles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🌍 fix: Format The Sub-10s Decimal For The Active Locale The fractional seconds value was interpolated as a raw JS number, which hardcodes the en-US decimal point into every language — "1.4s" where the locale writes "1,4 s" — and translators cannot fix a number formatted in code. The value is now formatted via Intl.NumberFormat with i18n.language, following MessageTimestamp's pattern of threading the language into the util; plural-key selection stays on the numeric value. A malformed language tag falls back to the plain number. Also documents the two accepted limits of the derivation, so they read as decisions rather than oversights: positive clock skew is undetectable from a single stamp pair, and the value is wall-clock elapsed, so a step held open across a suspension (checkpoint resume, HITL approval wait) includes that time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Persist A Durable `backgrounded` Marker Through Harvest; Localize Minute Digits Codex round 3, both findings confirmed. **Background origin survived only as transient state.** The dispatch handle in `tool_call.output` and the live status-marker attachment are both gone once the harvester patches the settled task's stdout over the handle — so the round-2 suppression (`backgroundHandle == null`) came back on after harvest or reload, showing dispatch time as the task's runtime. Following the same rule as e4bd15d (persist facts, decide at render): the harvest patch now stamps `backgrounded: true` onto the tool call in the same atomic write that erases the handle — on the heal path too, which re-applies over full-row saves that reverted the part. The cards gate on handle-or-marker; the dispatch duration itself stays stored. **Minute-branch digits bypassed locale formatting.** The seconds branch went through Intl.NumberFormat while minutes interpolated raw numbers, so Arabic/Persian locales flipped to ASCII digits above one minute. All interpolated values now flow through the (renamed) formatDurationValue; an ar-EG test pins the localized digits. data-schemas cannot be installed in this environment (same npm ci 403 as packages/api), so message.ts/harvest.ts are syntax-checked with resolution off and otherwise verified by review; CI runs their real typecheck and suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧪 test: Assert The `markBackgrounded` Stamp In Harvest Expectations The successful-harvest test's exact `toHaveBeenCalledWith` object did not include the newly forwarded `markBackgrounded`, so the API suite would fail on it. All three harvest-call expectations now assert `markBackgrounded: true` — the exact-object one of necessity, the two `objectContaining` ones deliberately, since the durable stamp (on the best-effort file-failure path and the reapply heal alike) is now part of the behavior under test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🎨 style: Wrap Harvest Spec Expectation Per Prettier Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d79d1ff76a
|
🎛️ feat: Consistent Dialogs, Clearer Settings, and a Keyboard Shortcuts Switch (#14882)
* refactor(AdminSettings): consolidate every admin dialog on one implementation The People Picker admin dialog was a standalone reimplementation of the shared AdminSettingsDialog with a better layout, leaving two components to keep in sync by hand. Port that layout into the shared component and rewrite People Picker to configure it, so all eight admin dialogs render from one place. The shared dialog gains the icon-tile header, the role selector and permission switches as bordered cards, and a real footer. Its header row was also top-aligning the 40px icon tile against a single-line title, leaving the icon hanging 6px low; it now centers. Both behaviors the standalone version lacked are preserved: the admin access warning and the confirm-before-disable flow used by Prompts. Adds an optional descriptionKey for a screen-reader description, and closes the dialog when the mutation reports success, which is how People Picker kept its auto-close. Permission switch ids are prefixed with useId so two mounted dialogs cannot collide. Marketplace dropped its dialogContentClassName override because the max-w-md and background it set conflicted with the new padding. * feat(ui): add FieldMessage for helper text that never shifts layout Form fields across the app render their validation error conditionally, so the error appearing pushes every field below it down. FieldMessage always occupies one line and only swaps its content and color between a resting hint, an error, and nothing, so the surrounding layout is fixed by construction rather than by whichever message happens to be showing. * fix(Memories): validate the key and value on the client Creating or updating a memory only learned that its key was malformed or already taken after the request came back, and the failure arrived as a toast. getMemoryKeyError mirrors the schema validator in data-schemas and checks for a duplicate within the same agent partition, so both dialogs resolve the outcome before sending anything. Errors render inline under the field instead of as a toast, live from the first character typed, and Create and Save stay disabled while any error stands. A pristine empty field shows only its hint, so the form does not report a problem before there is one. The edit dialog also closed itself in onMutate, discarding the user's edits whenever the server rejected the write and leaving the error toast to land on a dialog that was already gone. It now closes on success. This drops six toasts to two: the field-required and duplicate-key toasts are covered inline, leaving one generic toast per dialog for unexpected server failures alongside the existing success toast. * fix(Bookmarks): consolidate title validation and show it inline The duplicate-title check ran from three sources against two different strings: an inline validator reading the bookmark context, plus two warning toasts in onSubmit reading the tags prop and the conversationTags cache. All three now feed one helper behind the single inline validator, so both toasts are gone and the message is always com_ui_bookmarks_tag_exists. The title error was rendered conditionally, so it pushed the description field down as it appeared and disappeared; it now uses FieldMessage. The description registered a maxLength rule but rendered its error nowhere, so exceeding 1048 characters silently refused to submit with nothing on screen. It now reports like the title does. Submitting also closed the dialog immediately, throwing away what the user typed if the request failed, even though the mutation's onSuccess already closed it. Dropping that leaves the form with no reason to take setOpen. Renaming is no longer blocked when the title is unchanged: the tags-prop check had no exemption for the bookmark's own title, so editing just the description of a bookmark attached to the current conversation reported a duplicate and refused to save. The two tests that asserted the removed toasts now assert the inline error and that no toast fires. * feat(Skills): label the availability toggle and drop the detail icon The toggle in the skill detail header was a bare switch whose only name was an aria-label reading "Toggle skill active state", so nothing on screen said what it did, and "active" did not say active for what. It now carries a visible "Available to agent" label bound to the switch, plus a tooltip stating the effect: when on, the agent can use this skill in new messages. The label text stays fixed while the switch carries the state, so flipping it cannot resize the action row and nudge the buttons beside it. Also removes the decorative ScrollText circle from the detail header. It conveyed nothing the heading did not already say, and dropping it lets the title block sit at the top level instead of nested inside a flex row that now has a single child. * feat(Settings): move file management into Data Controls and clarify its labels Files were reachable only from the account dropdown, away from the other data-management entries. A Manage files row now sits in Data Controls beside Import conversations and Shared links, opening the same modal, and the account menu item is gone so there is one place to look. Two labels renamed for accuracy and consistency: - "Revoke all user provided credentials" becomes "Revoke all provider API keys". It sits in the API keys section beside Provider API keys and Agent API keys, so it should name what it revokes; "credentials" was vague and "user provided" described the system's perspective rather than the user's. - "Clear all chats" becomes "Delete all chats", matching its own Delete button and the "Delete TTS cache storage" row beside it. The action is irreversible, which delete states more plainly than clear. The TTS cache row gains an InfoHoverCard, the same explanation affordance used by the API keys dialog. Nothing previously said what the cache held or why its button is so often greyed out, which happens whenever the cache is empty, including for anyone who has never used text-to-speech. * feat(Shortcuts): add a switch that disables every keyboard shortcut There was no way to turn shortcuts off short of rebinding each one to nothing, which loses the bindings. A switch at the top of the shortcuts dialog now suppresses all of them at once while keeping every custom binding intact, so turning it back on restores the previous setup. The preference persists per browser in localStorage next to the custom bindings. Enforcement is a single guard in the window keydown handler, which already owns every shortcut, so nothing dispatches while it is on. Nothing is exempt, including the chord that opens this dialog. The dialog is still reachable from the account menu, so the switch cannot lock anyone out, and an exception would contradict what it says. useShortcutDisplay and useShortcutAriaKey return empty while it is on, so tooltips and aria-keyshortcuts across the app stop naming chords that would not fire. The binding rows stay editable, so shortcuts can be configured before turning them back on. * style(Skills,Prompts): align side panel spacing with the other panels Memories and Bookmarks share one spacing contract: 8px above the header, 12px down each side, and 12px under the last row. Skills and Prompts each drifted from it, so switching panels nudged the content. Skills sat at 16px per side and 12px on top, with the list running flush into the bottom edge. Its top padding now comes from the panel root like the other panels, its header and list use the shared 12px sides, and the list gets the same bottom inset. Prompts was applying the top padding twice, once on the panel root from the accordion and again on its own header, for 16px, and its list also ran flush into the bottom. The header no longer adds its own, and the list gets the bottom inset. Its asymmetric pl-3 pr-1 is left alone: the list reserves an 8px scrollbar gutter, so those values already render as an even 12px on both sides. Squaring the padding numbers would have made the panel visibly lopsided. Measured after the change, all four panels report 8px top, 12px bottom, and 12px on each side. * refactor(Shortcuts): invert the switch to an enabled-by-default control The control read "Disable keyboard shortcuts", so it was on when the feature was off. Inverting it makes the switch agree with the thing it names: it now reads "Keyboard Shortcuts", ships on, and turning it off is what stops the shortcuts. The stored value follows, from keyboardShortcutsDisabled to keyboardShortcutsEnabled defaulting to true. Nothing migrates the old key because the previous shape never shipped, and an absent value now means enabled, which is the default anyway. The row loses its filled card and sits as a plain bottom-bordered row under the title, reading as a section header for the list rather than a block competing with it. Every binding row now renders as disabled while the switch is off, dimmed with its edit and reset buttons actually disabled rather than merely looking inert. Any row left mid-edit is closed when the switch goes off, so the recorder cannot keep capturing keys for a shortcut that would not fire. * style(Shortcuts): fit the dialog on desktop without a scrollbar Open panels was a full-width block stacked under the two shortcut columns, so opening the dialog on a desktop viewport always started with a scrollbar, at about 100px of overflow. It becomes the third column instead. That removes the stacked block entirely, the three columns land at comparable heights, and the content now fits with nothing to scroll. The dialog widens on large screens to hold the extra column. Narrower viewports are unchanged in spirit: the panels list spans both columns below the shortcuts at tablet width and everything stacks into one column on a phone, scrolling as it did before. Reflowing the groups with CSS multi-column was the other option and looked worse: the short groups left a tall void beside Chat, and squeezing the panel rows into four columns truncated their labels. * style(Skills): make Edit an icon button and drop the detail text below the actions Edit was the only text button in a row of icon buttons, so it read as a different kind of control than Share and Delete beside it. It becomes a pencil icon at the same 36px size, carrying its label through a tooltip and an aria-label so the accessible name survives. The header row also centred its two halves against each other, which pinned the title level with the action buttons. The actions now pin to the top of the row and the text column starts below them, giving the title, author, date, and description a little room without moving the controls. * test: mock shortcut setting in expanded panel * remove unused com_ui_skill_toggle_active i18n key Superseded by com_ui_skill_available and com_ui_skill_available_hint in SkillToggle.tsx, but the old key was left behind in the locale files. * fix: honor shortcut switch in composer * fix: defer file loading until dialog opens * fix: preserve memory API errors * chore: restore automated locale entries * fix: honor shortcut switch during generation * refactor: share field message primitive * fix: reserve helper height for wrapping field messages * fix: wrap skill detail actions at narrow widths * fix: reset the memory create dialog when it closes |
||
|
|
8a946290f6
|
📌 fix: Fetch Pinned Chats Independently of the Chats List (#14860)
* feat: give the pinned chats section its own fetch The sidebar's pinned section filtered pinned chats out of the paginated chats list, which only holds the 25 most recently updated conversations. Once 25 newer chats existed, a reload hid the pin until the list was scrolled far enough to fetch the page it lived on. Pins are now fetched directly via GET /api/convos?pinned=true behind a dedicated query, so every pin paints with the sidebar regardless of where it falls in the chats list. Pin and unpin invalidate that query, and the shared conversation cache helpers keep it in step so a rename, delete or archive is reflected without waiting for a refetch. Pins stay out of the date groups, which groupConversationsByDate already handled. * fix: address review findings on the pinned chats section - Drain the cursor rather than capping the pinned request at 100. Since pins are kept out of the chats date groups, anything this query dropped was invisible in the sidebar entirely, not merely further down a list. - Apply the active bookmark filter to the pinned request and key its cache by it, matching the chats list beside it. - Move a pin to the top of the section when the caller asks for it, so a pin that just received a message leads the way it does in the chats list instead of waiting for a refetch. - Invalidate the pinned list when a conversation is unarchived, since archiving removes it from that cache and nothing put it back. - Index the pinned lookup: it filters on user + pinned and sorts by updatedAt, which no existing compound index covered. - Protect `pinned` from saveMessageToDatabase's unset sweep. Any persisted field missing from endpointOptions is unset, so sending a message in a pinned chat silently unpinned it. * fix: keep the pinned cache reconciled across the other convo mutations Second review pass on the independent pinned query. - Fall back to the pins already loaded in the chats pages when the dedicated request fails. Pins are stripped from the date groups, so an error otherwise emptied the section and hid them everywhere. - Restore default focus and reconnect refetching, matching the conversations query. A pin changed in another tab is only reconciled by a refetch, since that tab's mutation never touched this cache. - Invalidate the pinned list from the mutations that can produce or alter a pinned chat without going through pin itself: duplicate, fork, import, project assignment, and shared-link deletion. * fix: invalidate pins on tag and project-deletion changes Third review pass, same class as the last: the pinned query is keyed by the active bookmark filter, so changing a chat's tags can move it in or out of that filtered set, and deleting a project unsets chatProjectId on its chats, pinned ones included. * fix: cancel in-flight pinned fetches when deleting a conversation Deletion cancelled the regular and archived queries but not the pinned one, so a pinned GET issued before the delete could resolve after the row was stripped and write the deleted conversation back, leaving a row that navigates to a missing chat. Restoring default focus and reconnect refetching in the previous commit made those in-flight fetches more likely, so this widened rather than appeared. Cancelled on mutate, and invalidated on success since cancelling a race is best effort. * test: make the SSE query-cache mock key-aware The conversation cache helpers now run a second, pinned-keyed findAll pass. This mock ignored its key argument and always returned an allConversations entry, so those pinned writes were attributed to allConversations and the write-count assertions saw three instead of two. * fix: keep pins in sync through upsert and pin-only pages Root-level SSE updates and resumable settlement call upsert rather than update, so the independently cached pinned row never moved or refreshed. An all-pin first page also left the chats virtual list empty, so onRowsRendered never asked for the next cursor. * fix: keep pins current through SSE recovery and project delete Resumable SSE reconciliation invalidated conversation and allConversations only, so an independently cached pin kept stale title and order. Deleting a project-backed pin that lived only in that cache also skipped the project query, because the mutation never read chatProjectId there. * fix: keep pins current after bookmark edits and failed pages Renaming or deleting a bookmark rewrote tags on conversations but left the tag-keyed pinned cache pointing at the old filter. An all-pin page whose next fetch failed also retried forever because the empty-list effect had no memory of the attempt. Unpinning a pin that only lived in the dedicated cache removed it from Pinned without inserting it into Chats, and later cursor pages cannot recover a row whose updatedAt just jumped ahead of the current cursor. * fix: keep pins visible after a failed refetch A failed pinned refetch left React Query holding the previous list, so the nullish fallback never ran and a newly pinned chat vanished from both sections. Unpinning an older pin also inserted it into every cached chats variant, including bookmark and search results it would not match. Drop the checked-in agent task prompt. * test: type the pinned conversation fixtures correctly The delete mutation takes a plain string conversationId, but reading it back off a TConversation fixture widens it to string | null. Hoist the id into its own constant so the call site passes the real string. Type the tag fixture as TConversationTag so it carries the required _id and user fields the mocked resolved value expects. * style: sort the sidebar imports to the repo order The new pinned-section imports went in out of the longest-to-shortest order the import sorter enforces. * fix: keep drained pins and empty chat caches from breaking the sidebar A pinned page failing partway through the drain rejected the whole query, so every pin already fetched was discarded and the section fell back to whatever the chats cache happened to hold. Publish the accumulated pins before rethrowing so the retry renders against the partial set. Unpinning a chat that only lives in the pinned cache reinserted it into the chats list by spreading the first page, which is absent once removal has filtered out the last loaded row. Rebuild that page instead, matching the upsert path. * fix: order fallback pins by their timestamp The merge kept dedicated rows in Map insertion order and appended the pins recovered from the chats cache after them. A chat pinned while the dedicated refetch is failing is the newest pin, so the server would return it first, yet it landed last and could sit below the section's visible 30vh. Sort the merged set newest-first so a fallback row takes the place the server would give it. * fix: keep the shared badge and the move-to-top order on pins The pin response has no isShared: the flag is derived per list request by attachSharedFlags, which only runs for the list queries. Reinserting an unpinned chat into Chats therefore dropped its shared-link badge, because unlike an in-place update there is no existing row to carry the flag over from. Read it off the cached pin before the update removes that row. The chats cache refreshes updatedAt when it moves a conversation to the top, but the pinned cache only reordered, leaving the previous turn's timestamp on the row. Sorting the section newest-first then put it straight back. Refresh the timestamp there too, so the move survives the sort and both caches agree. |
||
|
|
0b995065bc
|
🗂️ feat: Rework the Projects Dashboard, Sidebar and Scoped Composer (#14866)
* style: Redesign the Projects Dashboard and Sidebar Give /projects a sticky navbar, quieter search/sort toolbar, and folder-style cards. Drop the create-dialog close control, restyle the sidebar Projects row, and align the workspace with the same layout language. * feat: Edit a Project Name and Description Add a shared edit dialog so a project can be renamed and given a description from the workspace or the sidebar menu. The create flow already stored descriptions; this is the matching update path. * feat: Delete a Project from the Workspace Extract the project delete confirmation into a shared dialog and expose it on the workspace header so deleting no longer requires the sidebar menu. * feat: Add Edit and Delete Actions to Project Cards Give dashboard cards a more-options menu that opens the same edit and delete dialogs as the workspace, so those actions are not workspace-only. * feat: Match Project Descriptions When Searching Projects Project search only matched the name, so a project found by its description was invisible in the sidebar and the projects dashboard. Match the escaped search term against name or description. * feat: Let Consumers Place and Size the ControlCombobox Popover The popover always matched the trigger width, sat 4px from it and used the same enter animation, which is wrong for a pill-shaped trigger in a composer and for a full-width field in a dialog. Add popoverClassName, matchTriggerWidth, gutter and portal so a consumer can opt out of each. All four keep the current behaviour by default, so existing comboboxes are unchanged; portal in particular stays true, as turning it off inside a scrollable dialog would clip the list. * fix: Re-render Conversation Rows When Pinned State Changes areConversationListItemFieldsEqual left pinned out of its comparison, so a row memoised on it kept rendering the stale pin state until some other tracked field changed. * fix: Keep the Project Scope When Starting a Chat From a Project Starting a chat from the project workspace set chatProjectId on the draft but left the URL on the unscoped route, so a reload or a refresh of the route dropped the project. Navigate to the project-scoped new chat URL alongside the draft. * style: Move the Project Chip Into the Composer The chip floated above the composer as a separate row, which read as an unrelated control and pushed the conversation starters down. Render it inside the composer border as the first row instead, and pass the project through ChatForm so the memoised form still controls it. The remove button no longer fades in on hover, since a control that only appears on hover is unreachable by touch. Its popover opens upward with a matching bottom-origin animation that honours reduced motion. * feat: Rebuild the Change Project Dialog on the Searchable Combobox The dialog used a bare select, so picking a project meant scrolling an unsearchable native list capped at the default page of 25. Use the searchable ControlCombobox, request the full first page, and disable Save until the selection actually differs from the current project. The combobox opts out of portalling so its search field sits inside the dialog's focus trap and can be typed in, and the dialog is overflow-visible so the list is not clipped. Unassigning now lives on the menu's own Remove From Project action, so the dialog no longer needs an empty option. Returning focus to the menu button rather than the menu item fixes focus being lost on close, since the item unmounts with the menu. * feat: Add an Overflow Menu to Project Workspace Chats Chats in a project workspace could only be opened. Managing one meant finding it again in the sidebar, so add the same actions to the row: change project, remove from project, and delete. The row becomes a card matching the project cards, and the endpoint icon is rendered at landing size rather than in a tinted tile. Its memo comparison now uses areConversationListItemFieldsEqual, since the render props comparison ignored fields the row displays. * feat: Rework the Projects Sidebar Section The section header duplicated the projects count and the New Project action already on the dashboard, and spent a row on a chevron button separate from its label. Collapse it to a single label toggle with an All Projects action, and drop the per-project count that was hidden on hover anyway. The new chat action becomes a real link to the project-scoped URL, so it can be middle-clicked and opened in a new tab, and modified clicks fall through to the browser. On the new chat route it commits ?projectId synchronously, because a deferred search param update lets ChatRoute see a project-scoped draft on an unscoped URL and wipe it. Row actions stay visible on devices without hover, where an action that appears on hover cannot be reached. * style: Widen the Projects Dashboard and Workspace Layout The dashboard and the workspace sat on bg-surface-primary at different max widths, so moving between them shifted the content and the shade did not match the rest of the app. Put both on bg-presentation at max-w-6xl. Project and chat cards gain a border, since colour alone separated them from the background and that separation is thin in light mode. The translucent blurred headers become opaque, and the scale-on-press transforms are dropped. In the workspace the edit and delete actions move out of the heading row into their own group, so a long project name no longer pushes them around. * fix: Reopen the Change Project Dialog From the Conversation Menu Closing the Ariakit menu in the same handler that opens the dialog made the menu's own dismissal land on the freshly mounted Radix dialog, which closed it again before paint, so Change project did nothing. Leave the menu close to the dialog, which already receives setMenuOpen and closes it once the assignment succeeds, matching the share and delete handlers beside it. * fix: Keep the Project Chat Menu Mounted While its Dialogs Open Hiding the Ariakit menu in the same handler that opens Change project or Delete restores focus to the menu trigger, which the dialog mounting alongside it reads as an outside interaction and closes on, so the action could do nothing. Both dialogs already receive setIsMenuOpen and close the menu once they finish, so leave the close to them, as the conversation menu does. * perf: Fetch a Project's Chats Only Once its Row is Expanded Collapse hides its children with CSS and inert rather than unmounting them, so every project row's chat query ran on sidebar load, up to one request per project, even with the whole section collapsed. Gate the query on the row's expanded state. React Query keeps what it already fetched, so reopening a row is still instant. * refactor: Move the Bottom Popover Animation Into the Shared Primitive ControlCombobox owns its popover animations in AnimatePopover.css, so the upward variant it needs belongs there rather than in the application stylesheet, where the control's appearance would diverge from the package that ships it. The app already loads the package stylesheet, so the animation resolves the same way the existing variants do. * fix: Stop Project Names and Descriptions Being Silently Truncated The dialogs accepted any length and reported success, while the persistence layer trimmed names to 100 and descriptions to 1000 characters, so reopening a project revealed text had been dropped with no warning. Share both limits from data-provider and cap the inputs at them, so the fields stop where the server would have cut them and the rule has one definition instead of the three it had. * fix: Do Not Report the Loaded Page Size as the Project Total The dashboard counted the projects fetched so far, so an account with more than one page read as exactly one page's worth and the supposed total grew with each Load more. Show the loaded count as a lower bound while another page exists. * chore: Satisfy the Static Checks for the Projects Rework Sort the delete dialog's imports to the repository order, and drop the three English keys this branch orphaned: the sidebar menu now says Edit project, the dashboard labels its own sort control, and the change project dialog no longer offers an Unassigned option now that removal lives on the menu. * fix: Highlight Only the Route Project in the Sidebar A leftover conversation project was still lighting a second row after opening another project's workspace. Prefer the workspace route, and only fall back to the conversation project outside that view. |
||
|
|
1153b70898
|
🔙 fix: Stop Stacking History Entries on New-Chat Param Changes (#14891) | ||
|
|
bce93f9c55
|
🎯 refactor: Infer Agents Endpoint for Model Specs Naming an Agent (#14889)
* 🎯 fix: Infer Agents Endpoint for Model Specs Naming an Agent A model spec whose preset names an `agent_id` but omits `endpoint` was unusable. `isModelSpecEndpointMatch` compares the request's endpoint to `preset.endpoint` by strict equality, so an undefined endpoint matched nothing and every request selecting the spec was rejected with a bare `Model spec mismatch` — an error naming neither the spec nor the missing field. The selector had the matching half of the same gap: `handleSelectSpec` read `preset.endpoint` directly, so it sent no endpoint and skipped assigning `agent_id` to `model`. Fixing only the server would leave the request malformed, so the resolution is shared between both. - Add `resolveModelSpecEndpoint` to `librechat-data-provider`, inferring the agents endpoint when a preset names an agent and none is set. An explicit `endpoint` always wins, so configured specs are unaffected. - Use it for endpoint matching and in the selector, so the menu and the request pipeline resolve a spec identically. * 🔁 refactor: Materialize Inferred Spec Endpoints at Config Load The review showed the lazy-resolver approach was unsound end to end: config validation rejected an endpoint-less spec before the resolver could ever run (`tPresetSchema` requires the `endpoint` key), and the resolver was applied at 2 of ~8 read sites, leaving selection handlers, startup presets, access filters, and provider-key reachability reading the raw preset. Materialize once at the boundary instead: - `tModelSpecPresetSchema` now makes `endpoint` optional (`nullish`). This is barely a widening — `endpoint: null` already validated — and only for model-spec presets; `tPresetSchema` is untouched. - `materializeModelSpecEndpoints` writes each spec's resolved endpoint back onto its preset. `createAppConfigService` applies it at both effective-config assembly points — YAML base load and DB-override merge — so admin-panel specs stored in override documents are covered. Identity-preserving, so cached configs see no new references when nothing needs filling in. - Every consumer now reads complete specs; the client's lazy resolve in `handleSelectSpec` is reverted to a raw read. `getModelSpecPreset` and the two hand-rolled preset constructions resolve the endpoint explicitly, which the narrowed preset type now enforces at compile time for any `TPreset`-shaped destination. - `isModelSpecEndpointMatch` keeps the resolver as request-time defense. * 🩹 fix: Materialize Spec Endpoints Before the YAML Missing-Endpoint Guard `processModelSpecs` warns and skips any spec whose preset lacks an endpoint, and it runs inside `loadBaseConfig` — so the previous commit's materialization received a YAML list from which the inferable spec had already been dropped. Only DB-override specs (merged after the guard) actually benefited. - Materialize at the entry of `processModelSpecs`, so inference happens before the guard and YAML agent specs survive it. The guard keeps skipping genuinely endpoint-less specs. The `createAppConfigService` calls stay: the base-path one guards alternate `loadBaseConfig` implementations, the merged-path one covers override documents, and both are identity-preserving no-ops when specs are already complete. - Constrain the widened schema: omitting `endpoint` is only legal when the preset names an `agent_id`. A preset with neither validated as a hard error before the key became optional, and silently accepting it would trade that startup-time error for a dead spec. An explicit `endpoint: null` (valid before this PR) keeps validating. * 🩹 fix: Infer Only From Non-Empty Agent IDs, Never Over Explicit Null Two edge cases in the inference contract: - `agent_id: ''` (what a form-backed writer persists for an untouched field) passed the nullish checks, validating and materializing a spec that names no agent. Both the refinement and the resolver now require a non-empty id, so such config fails validation loudly instead of producing a selectable spec that cannot work. - `endpoint: null` alongside an `agent_id` was treated as inferable, silently activating a spec that validated — and was skipped — before this PR. An explicit null is a statement, not an omission: the resolver now infers only when the key is absent, preserving prior behavior for previously valid configs. |
||
|
|
ed081964d6
|
🏷️ fix: Model Spec Menu Label and Agent Avatar Fallbacks (#14887)
* 🏷️ fix: Model Spec Menu Label and Agent Avatar Fallbacks The selector's header already falls back from a spec's `label` to its `name` (`getSelectedValueText`), but the menu item and search result render `spec.label` bare. A spec persisted without a label therefore shows its name in the header while its row in the list is blank — selectable, but unlabeled. Specs targeting an agent had a related gap: the chat landing resolves the agent and shows its avatar, while the selector resolved icons from the spec/preset/endpoint only. An agent with an avatar still rendered a generic endpoint icon in the menu unless `iconURL` was set by hand. - Fall back to `spec.name` when `label` is absent or empty, matching the header. - Resolve the target agent's avatar as the icon when the spec defines none of its own; an explicit `iconURL` still wins. `getSpecAgentAvatarURL` returns the avatar as a primitive so the memoized `SpecIcon` compares a string rather than the identity of the agents map, and both call sites already consume `useModelSelectorContext`, so reading `agentsMap` adds no new subscription. * 🔁 refactor: Normalize Spec Labels on Ingest, Harden Agent Avatar Lookup Addresses the review findings, which fell into two patterns rather than four independent bugs. The label fallback was applied per render site, but the spec list feeds five consumers: the menu row, the search row, `filterItems`, the spec/endpoint discriminator in `SearchResults`, and `getSelectedIcon`. Guarding two of them left `filterItems` throwing on `label.toLowerCase()` and the discriminator misclassifying a label-less spec as an endpoint — reproducing, inside the fix, the exact inconsistency the fix targeted. Normalize once on ingest instead: `normalizeModelSpecs` fills a missing label from the name where the selector derives its spec list, so every consumer works from a complete spec and the per-site guards are removed. It returns the original array and the original spec objects when nothing needs filling in, so memoized consumers see no new identities. The avatar helper had the same shape of problem — a second resolution path that skipped what the existing one already handled: - Resolve via `getAgentAvatarUrl`, which supports agents persisting `avatar` as a URL string rather than an object. - Gate on `isAgentsEndpoint`. `tModelSpecPresetSchema` permits `agent_id` alongside any endpoint, so a leftover id on a non-agent spec would otherwise surface an unrelated agent's identity. - Thread the avatar through `getSelectedIcon`, so the selector trigger keeps the agent avatar after selection instead of reverting to the generic endpoint icon. * 🩹 fix: Treat Empty Spec Icon Fields as Unset `??` only skips null and undefined, so an `iconURL: ''` — what a form-backed config writer persists for an untouched field — stopped the chain and suppressed both the agent avatar and the endpoint icon, leaving the generic fallback. Resolve the first non-empty candidate instead. This matches `applyModelSpecPreset`, which already ignores an empty `iconURL`, and keeps `showIconInMenu` as the explicit way to render no icon. * 🔁 refactor: Normalize Spec Labels at the Query Boundary, Cover Favorites The context-level normalization missed a parallel consumer: the favorites sidebar reads `startupConfig.modelSpecs.list` directly, so a pinned label-less spec rendered a blank row and its icon ignored the agent avatar — the same two gaps this PR fixes in the selector. Move normalization to the true shared boundary instead of adding another per-consumer call: `useGetStartupConfig` normalizes in its query function, once per fetch, cached — so the selector, search, mentions, favorites, and provider-key reachability all read complete specs. The `ModelSelectorContext` call is removed as redundant. - `normalizeModelSpecs` is now a single pass with a lazy copy (allocated only on the first incomplete spec), replacing the `some` + `map` double scan. - `FavoriteItem` threads `agentAvatarURL` into `SpecIcon`; the list resolves it per spec from the agents map it already holds, passed as a primitive so memoization is unaffected. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |