Only the free-text path sets consumedComposerText, so gating the restore on it
discarded the stashed ordinary message whenever the user answered by clicking an
option or skipping, while the deletion below ran regardless. The stash is now
restored whenever one exists, independently of the reset condition, and it
outranks whatever the composer holds since that is the answer draft the
submission just consumed.
A pure deletion (`new_text: ""`) rendered a blank green row and reported one
addition, because `''.split('\n')` yields one element. A trailing newline did
the same at the end of either side. Both sides now go through one helper that
treats an empty side as no rows and a trailing newline as a terminator. The
defect predates the structural builder; it was carried over from the string
preview it replaced.
The args preview was formatted into unified-diff text purely so parseUnifiedDiff
could read it back, and the separators that round trip needed between batched
edits are byte-identical to a real changed line: deleting a source line
`-- old_text` prefixes to exactly `--- old_text`. Neither the marker names nor
their position could tell the two apart, so the parser either inflated the
statistics or dropped a genuine edit.
buildEditPreviewDiff now constructs the preview's lines from the structured
edits, so edit boundaries are structural and no separator is ever emitted.
parseUnifiedDiff is back to parsing only real tool output, where `--- ` and
`+++ ` are genuine file headers, and it no longer special-cases synthetic
markers at all. Copy text comes from a one-way serializer.
Also include agent identity in the web-search snapshot key: handoff agents
repeat provider tool-call ids, so without it one agent's search results
replaced another's.
Upsert re-emitted web-search attachments instead of appending them. The search
helper rewrites and re-emits the entire payload after every highlight update,
keeping the same type, toolCallId and turn, and those attachments carry no
file_id so the existing upsert never saw them. Each snapshot was appended, so
collectSearchVerticals concatenated one copy of every image, product and place
per update. Keyed by type, tool call and turn, replacing wholesale since the
incoming payload is complete.
Preserve MCP identity in the tool-call label. function_name has already had the
MCP delimiter and server stripped, so re-parsing it classified an MCP function
sharing a built-in's name as that native tool and both showed and announced an
unrelated label.
Qualify grouped tool identity by agent. Provider tool-call ids repeat across
agents in a handoff transcript, and a lone tool with reasoning now groups too,
so two sibling groups shared one React key and one expansion override.
Compose the shared Button for the handoff disclosure, keeping only its geometry
local.
Submitting an answer from the expanded card never goes through collapse, so the
ordinary unsent message stashed when the question was moved back was never
handed over and its entry leaked. The resume success path now restores it in
place of the reset when one is stashed, and drops the entry either way. Read
through a ref because the callback settles long after the render that armed it.
Track cancellation separately from failure in grouped tool metadata and name it
in the group header. A stopped step is settled and not an error, so the group
auto-collapsed under a success-sounding header with the only cancellation
notice hidden inside the panel it had just closed.
Preserve an ordinary unsent message across an ask handoff. With Save drafts
off, collapsing a question releases the composer; typing there and then moving
the question back overwrote that text with the answer and lost it. The released
text is now stashed per pending action and handed back on the next collapse.
Skip the named view transition for batched questions. Only the single-question
surfaces declare view-transition-name: ask-question, so wrapping a batch
handoff gave the browser nothing to pair and it cross-faded the whole root
instead of morphing the question.
Compose the shared Button for the memory-artifacts and web-search disclosure
rows, keeping only their geometry local. The web-search row keeps its instant
hover by suppressing the recipe's color transition, since the chevron beside it
reveals without a fade and the existing test pins that.
Seed the ask-specific draft when Save drafts is re-enabled. An answer captured
while saving was off has no persisted ask draft for autosave to restore, so
expanding opened an empty composer and the next collapse read that empty value
back over the in-memory answer.
Register surface-code in the versioned theme registry and use it for the diff
preview. The token, its light and dark CSS values and its Tailwind mapping were
already in place and already consumed by CodeBar, CodeBlock and ResultSwitcher;
only the three theme interfaces, the two bundled themes and DiffView were
missing. DiffView no longer coordinates two unrelated surface roles through a
dark: override, and a custom theme can restyle the code surface centrally.
Include background-task failures in grouped outcomes. A backgrounded bash or
code task reports its verdict through a background_task_status attachment
rather than its output, and its dispatch step keeps a benign handle and usually
closes as completed, so the group reported no failed action beside a card
showing failure. Group metadata now derives that signal with the same helpers
the child card uses, correlated per part so a handoff response repeating a
provider tool-call id across agents still routes correctly.
Require a title on shopping results. A link-only card had no accessible name
and no visible identity: the anchor carried no label, its image is decorative
and the title span rendered empty.
Stamp the failed marker on the aggregated summary part server-side, mirroring
how ON_RUN_STEP_CLOSED already stamps runStepStatus. The SDK aggregator ignores
a complete event that carries no summary, so an errored round's partial deltas
were saved unmarked and a reload re-rendered the truncated text under
"Conversation summarized".
Skip memory attachments that carry a toolCallId in the message-level
MemoryArtifacts decoration. Those calls already render inline as a MemoryCall
card with the same key, value and outcome, so one mutation appeared twice.
Attachments with no originating call keep the decoration as their only surface.
Keep a reasoning-bearing group expanded when Open Thinking Dropdowns by Default
is on. The folded-in THINK part only renders inside the group body, which is
not even mounted while collapsed, so the preference had no visible effect until
the action group was opened by hand.
Give same-named place results unique keys. Several branches of one chain share
a name and often carry no identifier, so rows whose address, rating and map
target all differ collided on one React key.
Compose the shared Button for the compact reasoning disclosure, keeping only
the row geometry local so the focus ring and disabled handling come from the
shared recipe.
Lazy-mount the compact reasoning body with useLazyCollapseBody, as the
standalone panel already does. Collapsed is the default whenever thoughts are
hidden, and a streaming THINK part re-renders on every delta, so the full text
was being mounted and laid out behind an invisible panel.
Classify grouped memory failures with isMemoryFailureOutput. Those tools report
failure in prose that generic isError parsing does not recognize, so a
persisted call with no terminal status showed a failed card inside a group
reporting success with no failure count.
Announce the diff statistics to assistive technology. The +/- glyphs carry
their meaning through color and sign, so aria-hidden left screen-reader users
without the change summary; the counts are now also exposed as localized
screen-reader text.
Matching the marker names alone was wrong: deleting a real `-- old_text`
comment line, as SQL and Lua use, is prefixed to exactly `--- old_text`, so the
previous guard discarded a genuine changed line from both the preview and the
statistics. The pair is now skipped only in separator position, adjacent and
immediately followed by a hunk header, which no run of genuine content
satisfies. Covered by a test asserting such a pair inside a hunk survives as
content.
Also drops the em dashes from the comments added over this review pass.
Recognize the batched-edit separator pair for every edit in a diff preview.
formatEditPreview writes a `--- old_text N` / `+++ new_text N` pair before each
edit's `@@`, but the parser gated on `inHunk`, which is never reset, so only
the first pair was skipped and every later one was counted as a deletion plus
an addition and rendered as a marker row. Matched by name instead, which leaves
content lines that merely begin with `-` or `+` alone.
Compose the shared Button for the popover's type-below hint, suppressing only
the fill the way Summary's quiet text buttons already do, so the focus ring and
disabled handling come from the shared recipe.
Render an answer box that carries only snippetHighlighted. It is independently
optional from snippet, and the details card opens for any answer box, so such a
result previously showed a heading with an empty body.
Compose the shared Button through HoverCardTrigger's asChild for the web search
details trigger, which recreated rounding, hover colors, focus ring and motion
locally. This also replaces Radix's default anchor with a real button. Only the
hover-card reveal and the icon geometry stay local.
Copy compact reasoning through copy-to-clipboard for the same reason as the
handoff control: navigator.clipboard is undefined on a non-secure origin, where
dereferencing it threw before anything was copied.
Document the streaming-peek fade as a narrowly scoped custom-CSS exception. It
is a mask-image stencil rather than paint, so only its alpha channel is read
and no theme role could meaningfully restyle it.
Move the disclosure chevron appearance into @librechat/client as
disclosureChevronVariants, owning the hover/focus reveal, the motion and the
open rotation. The four tool, search, memory and reasoning surfaces now pass
their expanded state and keep only icon geometry local, so a change to the
shared behavior reaches all of them. Panel spacing stays feature-local, being
genuine layout.
Require a navigation target on image results, not just an image source: a
thumbnail-only entry passed the previous guard and then rendered a styled
anchor with an undefined href, looking actionable but opening nothing.
Exclude closed run steps from the pending-auth predicate. ToolCall already
hides the sign-in button on a terminal status, so a persisted OAuth call whose
progress never reached 1 left the group showing a trust warning with no
authentication action behind it.
Copy handoff instructions through copy-to-clipboard, as the sibling
message-content controls do. navigator.clipboard is undefined on a non-secure
origin, where dereferencing it threw before the rejection handler could run and
the button silently did nothing.
Reuse the shared CopyButton for the handoff instruction copy control, which
recreated hover, focus-ring, opacity, rounding and copied-state appearance
locally. The icon crossfade and tooltip now come from the shared primitive and
only the reveal-on-hover behavior stays local.
Keep unlabeled commentary out of collapsing tool groups. An unlabeled group
auto-collapses once complete, so folding commentary in hid the assistant's
prose behind a generic action header. The flush now splits the block at
commentary and groups each surrounding run on its own, restoring the shape the
previous fallback had while keeping reasoning folded in.
Drop shopping results without a link and places with nothing to name, for the
same reason the image results are filtered: every provider field is optional,
and an unusable entry still made the array nonempty, so the card rendered an
anchor with no destination that looked actionable but could not be opened.
Reuse the shared Button primitive for the three question move controls (the
chat card's chevron and both popover chevrons), which each repeated the same
hover, focus-ring, color and rounding recipe locally. Only the icon geometry
stays local.
Drop provider image results that carry neither thumbnailUrl nor imageUrl.
Both are optional in the ImageResult contract, and an unusable entry still
made images.length nonzero, so the card rendered an anchor and img with
undefined destinations instead of omitting the result.
Gate the memory deletion confirmation on a completed phase. The panel opens as
soon as the key streams in and the phase can still settle as cancelled, so
"Memory deleted" was claiming a removal that never happened while the header
correctly read Cancelled.
Key pending answer drafts by action id instead of a single shared slot. With
Save drafts disabled, editing a collapsed question in one conversation and
then editing another paused conversation's question overwrote the only slot,
and the action-scoped reader then showed the first card an empty box with its
unsent answer unrecoverable. Entries are dropped once answered.
Replace the raw green/red diff palette in DiffView and CodeWindowHeader with
the existing status-success/status-error semantic roles, so a customized or
versioned theme moves the diff colors with it.
Compose the shared Button primitive in the ask option rows instead of
recreating hover, focus-visible and disabled appearance locally, and swap the
raw text-white on the checked badge for text-on-status.
Prefer a THINK part's generated reasoning_label in the grouped rendering path,
matching the standalone Reasoning path, with the generic Thinking/Thoughts text
only as a fallback.
Derive grouped tool completion and failure from the run step's terminal status
through the shared resolveToolCallPhase helper, so a step the run closed as
failed with empty or benign output no longer reads as running and then reports
success while the card beside it shows a failure.
Carry an explicit failed flag on a summarize round that ended in error, so the
partial deltas already streamed into the slot are no longer presented under the
"Conversation summarized" label.
Also drop seven orphaned English locale keys this branch left unused, which
were failing the unused-i18n static check.
empty_messages and final_context_overflow are structured errors thrown by
the agents SDK, but they were missing from the typed-error dispatch table
and surfaced as raw JSON inside the generic failure text. Map both to
localized copy: empty_messages keeps its token budget breakdown in a
details block, final_context_overflow interpolates the projected and
available token counts.
A tiny user-set maxContextTokens re-triggers summarization on every graph
step (the summary allocation rounds to nothing and history still
overflows), burning dozens of LLM calls until the recursion limit aborts
the run with an opaque LangGraph error. Fall back to plain pruning under a
1024-token floor so the run either fits or fails fast with the actionable
empty_messages breakdown.
Failed summarize completions spliced the in-flight summary part out of the
content array, remounting every later index-keyed part (collapsing whatever
the user had expanded mid-stream) and breaking the content-position ==
step-index invariant that streaming writes rely on. Finalize the part in
place instead and scope completion to the owning step's slot so
back-to-back rounds cannot clobber a newer in-flight part.
The popover carried both a chevron and a close button. Both left the
question pending and both moved it into the chat, so their only real
difference was invisible: the chevron kept the composer wired up as the
answer box while the close button released it. Two icons for one visible
outcome, with the meaning hidden in a placeholder change somewhere else.
Collapsing now does both jobs at once. Moving a question to the chat hands
the composer back for normal messages, and the card's chevron re-arms it,
so the popover's visibility and the composer's role can never disagree.
Escape maps to the same path and the dismissed-ids state is gone. Both
chevrons get a tooltip and a matching label, since no glyph conveys this.
Also fixes two behaviours found along the way: the card's Skip was a
silent no-op once the question left the popover, because it routed through
a helper gated on answer mode instead of the live pause, and clicking a
multi-select row moved the keyboard highlight, leaving a row painted as
selected after it was unchecked.
The popover and the chat card rendered the same question two different
ways: numbered rows in one, wrapped chip buttons in the other, and the
panel was painted with hardcoded white/gray instead of theme tokens.
Both surfaces now render one AskOptions list, so numbering, check badges,
rounding and hover states match wherever a question appears. The card
groups its prompt with the description, styles the answer box like the
composer, and gains a Skip button beside Submit so declining does not
require the popover. While the popover holds the question the card keeps
a hidden copy of itself in the thread, reserving the exact space the card
will need so nothing reflows when the question moves back.
Wrap a state update in startViewTransition (with a synchronous flush) so
elements sharing a view-transition-name animate their position, size and
shape between two states instead of swapping.
The composer band gets its own named, opaque snapshot group stacked above
the morphing element: without it a surface travelling between the thread
and the composer paints over the input and bleeds through the strip below
it. Falls back to a plain update where the API is unavailable and for
reduced-motion users.
The button lived inside the scroll container with sticky/float
positioning, so horizontal scrolling dragged it through the command
text. It now sits absolutely in a non-scrolling wrapper around the
scroller and stays in the corner on both axes.
Place rows were the only vertical without a destination. Each row now
links out to the user's native maps app: Apple devices (detected via
the existing isMacPlatform check) get maps.apple.com with the query
and coordinates, everything else gets a Google Maps search link. Both
are keyless https links that degrade to a web map, so a misdetected
platform still lands somewhere useful. Rows pick up the same hover
treatment as the other linked results.
Image, shopping, and place results were only visible after expanding
the web search row, and disappeared entirely inside a collapsed tool
group. The strips moved into a shared verticals module that renders
in two spots following the attachment pattern: always below the row
for an ungrouped search, and hoisted below the group (visible while
collapsed) for grouped calls, where the row suppresses its own copy
via hideAttachments. The disclosure panel is back to sources only.
Serper vertical results were parsed into the artifact and dropped.
The expanded panel now renders them when present: an image strip of
rounded thumbnails at true aspect ratios (sized from thumbnail
dimensions, no layout shift), a shopping card carousel with product
image, title, price and store, delivery note, and star rating, and a
place list with name, category, address, and rating. All link out,
use semantic lists with localized labels, and cap item counts.
Expansion now keys off any renderable content, so a verticals-only
search still opens.
The inline answer card read as assistant prose sitting inside the
source list. The panel is back to sources only; an Info trigger at the
right edge of the row (hover-revealed like the chevron, focus-openable
for keyboard) opens a hover card with the search query, the source
count, and the provider answer box when present. WebSearch now
receives the tool call args to surface the query.
The panel only showed favicon, title, and domain while the search
artifact carries much more. Source rows now include the result snippet
(clamped to two lines) and date under the domain, and a Serper answer
box renders as a lead card above the list with its title and snippet.
Streaming behavior is unchanged; the answer box is attachment-only
since it never carries citation anchors.
The expanded source panel sat flush against the 22px favicon stack,
which overflows the 20px row. The panel now opens with a 6px top
margin while the row keeps the shared my-1 rhythm.
set_memory and delete_memory rendered through the generic tool card,
reading "Ran Save Memory" with raw parameter chips like an unknown MCP
tool. A new MemoryCall part gives them first-class rows: "Saved
memory" / "Deleted memory" with the key as a quiet subtitle, the
Brain icon, and an expanded panel that mirrors the memory attachment
display (bold uppercase key over the stored value, or a deletion note),
with error output shown inline when a write fails.
The dual old/new gutters left one column blank on every add or delete
row, reading as a stray number floating far from its marker. Each row
now shows the one relevant line number (old for removals, new
otherwise) right beside the +/- marker, and the scroll container is
vertical-only since lines wrap.
The row offered an expand affordance whenever raw output existed even
when no sources parsed, opening an empty panel. Expansion now keys off
parsed results, which also covers attachment-only sources that arrive
without text output, and collapses if results disappear.
Rebuild MemoryArtifacts on the shared disclosure row (useExpandCollapse
grid animation, hover-reveal chevron, h-5 header) in place of its
bespoke ResizeObserver height animation, and simplify the expanded
panel: section headings dropped in favor of bold memory keys, a single
p-3 inset instead of double padding, and the now-unused heading
translation keys removed.
Replace the highlighted-text diff in file authoring output with a
line-based renderer: red and green tinted rows with +/- gutters,
old/new line numbers when hunk headers carry them, and diff noise
stripped (file headers, streamed old_text/new_text markers, bare @@
separators become dividers). The code window header shows the file
name with +N/-N change stats next to Copy Code, and file authoring
rows adopt the path-first arg parsing and shared panel spacing.
Group headers now describe the activity performed: homogeneous search
groups read "Searched the web" or "Searched web and files", mixed
groups read "Ran N actions" with per-label repeat counts (Create File
x2) and a failure tally in the detail line. Generic tool rows resolve
their label through getToolDisplayLabel, the OAuth trust warning moved
into a shared ToolAuthWarning that a group renders once instead of per
call, and the group body is a flex column so row margins stop
collapsing unevenly between rows.
Map skill, read_file, create_file, edit_file, set_memory, and
delete_memory to localized display names so summaries stop showing raw
tool ids, and give the memory tools the Brain icon the Memories panel
already uses. read_file and skill rows parse the arg the backend
schemas actually emit (path, with file_path as a legacy fallback;
skillName) and fall back to a generic noun instead of rendering an
empty name in their labels.
Extract the hover-reveal chevron and expanded panel spacing into
disclosure.ts and align every tool activity row on the same layout:
h-5 header, my-1 rhythm, full-width click target, and a 200ms chevron
rotation. Rows rendered through ProgressText now stretch across the
message column (the absolute content layer was shrink-wrapping to the
text), and the copy buttons in thinking and output panels anchor to
the row instead of the panel corner.