Stamp agent ownership on streaming and final search snapshots. Compose the shared floating thought button. Preserve rebased group behavior, correct stale test selectors, and keep expansion callbacks outside state updaters.
A diff in the output now supersedes everything, including a create's authored
content. `create_file` overwriting an existing file returns a summary plus a
unified diff, and preferring the authored content there rendered that diff
twice: once through DiffView and again verbatim as the raw output section. This
restores the precedence the string-preview version had.
Give image tiles distinct React keys. Syndicated images reuse one thumbnail
across results that link to different pages, and grouped searches merge
attachments, so the source alone collided.
Require the composer to still hold the submitted text before restoring the
stashed ask message. The textarea stays editable while the resume is in flight,
so anything typed since is newer than both the stash and the answer.
Filter provider-supplied vertical URLs through the markdown allowlist. Search
result payloads are copied into the attachment without validation, so an image
or shopping target could be `javascript:` or another non-network scheme and the
card handed it straight to href. The same helpers are used when admitting a
result and when rendering it, so the two cannot disagree.
Derive tool-group identity from a message-wide occurrence ordinal instead of a
per-instance counter. Phase slices render through separate nested ContentParts
that share the expansion map, so a per-instance count restarted and let a later
slice inherit an earlier group's override. The ordinal is stable under
content-index shifts, and ordinal 0 keeps the bare id, so a group keeps its
identity when a completing phase moves it into a slice.
Build group attachments as a union. Repeated provider ids inside one group
resolve to the same bucket, so appending per part rendered each file and search
snapshot once per repeated call.
Disambiguate duplicate tool-group identities by how many earlier groups already
claimed the same provider tool-call id. Those ids repeat across agents in a
handoff transcript and restart per turn within one agent, and a lone tool with
reasoning now groups too, so sibling groups could share a React key and one
expansion override. Occurrence order is stable because groups are append-only,
and the first occurrence keeps the bare id, so an id that never repeats is
unchanged and still survives a content-index shift.
Restore a stashed ask message regardless of the current Save drafts setting. The
stash is only ever created while saving is off, so it never reached the
conversation draft; gating the restore on the setting dropped it whenever the
user enabled saving between expanding and collapsing.
Localize the handoff field labels the SDK defines (instruction, instructions,
context). Any other key is an admin-authored promptKey from the agent graph, so
it stays as written, like an agent's own name.
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.