Commit graph

5461 commits

Author SHA1 Message Date
Marco Beretta
0b64efa3fd
fix(client): render handoff instructions as readable context 2026-09-06 18:46:10 +02:00
Marco Beretta
461cf1fc6d
style(client): align tool call import order 2026-09-06 18:46:10 +02:00
Marco Beretta
8b747aef5c
style(client): fix changed-file lint formatting 2026-09-06 18:46:10 +02:00
Marco Beretta
093c7092af
refactor(client): make one control move a question between chat and composer
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.
2026-09-06 18:46:10 +02:00
Marco Beretta
a7b2d56904
style(client): rebuild the ask question surfaces on shared option rows
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.
2026-09-06 18:46:10 +02:00
Marco Beretta
fae5c00d06
feat(client): add view-transition morph helper for composer surfaces
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.
2026-09-06 18:46:10 +02:00
Marco Beretta
b334bdc757
fix(client): pin the bash copy button while the command scrolls
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.
2026-09-06 18:46:10 +02:00
Marco Beretta
81db565b9c
feat(client): platform-native map links on place results
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.
2026-09-06 18:46:10 +02:00
Marco Beretta
ea9bd91a81
feat(client): surface search verticals outside tool disclosures
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.
2026-09-06 18:46:10 +02:00
Marco Beretta
309d2ab3e8
style(client): strip link underlines in web search verticals
The image and shopping anchors sit inside the markdown prose scope, so
every label rendered with the blue link underline.
2026-09-06 18:45:52 +02:00
Marco Beretta
b00516b11f
style(client): help cursor on the retrieval relevance badge
The badge sits inside a clickable row, so it inherited the pointer
cursor while only offering an explanatory tooltip.
2026-09-06 18:45:52 +02:00
Marco Beretta
d1f84fa4a4
feat(client): image, shopping, and place verticals in web search results
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.
2026-09-06 18:45:52 +02:00
Marco Beretta
564a3a3707
refactor(client): move web search answer box into a details hover card
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.
2026-09-06 18:45:52 +02:00
Marco Beretta
aff8f62882
feat(client): richer web search source list
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
ae07f45aad
style(client): gap between the web search row and its source list
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
09f63613e5
feat(client): dedicated card for memory tool calls
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
b9f5a886df
fix(client): compact single-number gutter in diff view
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
5bd295262c
style(client): soften the message nav hover tooltip
Rounder corners and the light border weight to match the rest of the
floating surfaces.
2026-09-06 18:45:03 +02:00
Marco Beretta
59702efcc2
fix(client): gate retrieval expansion on renderable results
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
b01680826b
refactor(client): align memory updates with the standard tool row
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
af8949ea85
feat(client): render file edits as a structured diff view
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
2fb33ad0b4
feat(client): outcome-based tool group summaries
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.
2026-09-06 18:45:03 +02:00
Marco Beretta
fa02825ead
fix(client): friendly labels and correct arg parsing for native tools
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.
2026-09-06 18:44:37 +02:00
Marco Beretta
a498f0cc5f
refactor(client): unify tool row disclosure styling in a shared module
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.
2026-09-06 18:44:37 +02:00
Marco Beretta
0cb2270ba0
fix(client): handle singular grouped tool labels 2026-09-06 18:44:09 +02:00
Marco Beretta
6661499ac7
feat: Move subagent runs to an artifacts-style side panel and polish reasoning UI
- Replace the subagent dialog with a right-side panel that shares the
  artifacts slot (currentSubagentRunId + subagentPanelVisibility atoms,
  a subagentRunsState registry, and a useOpenRightPanel cross-clear so the
  two panels are mutually exclusive)
- Rebuild the inline subagent card to be tool-consistent, preview a single
  live/result line, and open the panel (auto-focusing on stream arrival)
- Relocate the originating prompt to a collapsed, smoothly-animated
  disclosure under the panel header
- Gate the Thoughts copy button to expanded state and move the floating
  bar to a scroll-out reveal, matching the code-block pattern
- Fix the reasoning expand/collapse jump by animating the trailing gap
  inside the grid track instead of an expand-only margin
2026-09-06 18:44:09 +02:00
Marco Beretta
bf5e844a29
feat: Group Reasoning with Tool Calls and Polish Activity UI
- Treat reasoning (Thoughts) as transparent to tool grouping so interleaved
  thoughts fold into the tool group instead of splitting it
- Group a lone tool call that has reasoning (e.g. a skill) so it gets the
  same collapsible chrome as multi-tool groups
- Count only real tool calls for the group header; add a reasoning indicator
  and a singular 'Used 1 tool' label
- Rework the in-group Thoughts panel: tool-row sizing, rounded content,
  header copy button, and a floating collapse/copy bar
- Round the floating thinking-bar buttons
- Refine the agent handoff row and instructions panel with consistent
  spacing and a copy affordance
2026-09-06 18:44:09 +02:00
Danny Avila
e1f2298cb8
🗓️ fix: Surface a Run Now Scheduled Chat in the Sidebar (#15630)
* 🗓️ fix: Surface a Run Now Scheduled Chat in the Sidebar

Run Now left the conversation list untouched, so the chat it started was
invisible until a reload. Refetching the list on success would not have
fixed it either: run-now answers once the trigger delivery is durable, and
the generation that writes the conversation starts after that, so the
refetch comes back without the chat.

Seed the row instead, from the id the response already names and the cached
schedule's name and agent, resolving the destination project the same way
the fire path does so it lands in that project's list and no other. Report
the started generation to the active-job list and re-arm it, since it stops
polling while nothing is listed and is the only thing that can show the row
as running.

* 🩹 fix: Wait for the Run to be Admitted Instead of Seeding a Row

Codex found four ways the optimistic row goes wrong, three of them inherent
to seeding a chat the server has not created: it is assembled from a client
snapshot of the schedule that another tab may have moved on from, it
outlives a delivery that never admits, and it can be opened before the
conversation exists, which bounces that tab to a new chat it never recovers
from.

The conversation route answers 404 until the generation's first write
commits and applies the same visibility rule the list query does, so it is
an exact admission probe: a 200 means the chat is listable now and carries
the server's own row. Wait on that instead, over a bounded set of probes,
and add what came back. Nothing is written for a run that never admits.

Re-arming the active-job poll moves to admission too, where a generation is
known to exist, rather than at the click, where the thirty-second grace can
lapse before a retried dispatch ever starts one.

* 🩹 fix: Cover the Delivery Retry Window and Keep Transient Probes Alive

Two follow-ups from codex on the admission watch.

The probe budget stopped at forty-three seconds, inside the window the
server is still willing to admit in: the trigger engine allows eight
attempts backing off from a second and doubling, about two minutes of
retries at worst. Widen to roughly five minutes across eleven probes, which
covers that twice over. A dispatch refused with a long Retry-After can still
be re-driven hours later; a watch held open for that is worse than the list
refetch that already covers it.

Ending the watch on any non-404 also gave up on failures that say nothing
about the run: a 5xx, or a request that never got a response, is the probe
failing while the durable delivery is still free to admit. Only a 4xx that
is not 404 settles the matter.

* 🩹 fix: Fence the Admission Watch to its Session and Queue its Title

Two more from codex, both real.

The watch can run for minutes, and signing out or signing in as someone else
empties the query cache in between. A response arriving after that wrote one
user's chat into another's sidebar. Fence it: capture who the cache belonged
to when the watch started, and check again before probing and before writing.

A conversation admitted mid-run has not been titled yet, so the row lands as
"New Chat". The foreground paths hand that to the title queue, which already
owns the timing the server was configured for; a scheduled run has no
foreground path, so hand it over here instead of leaving the placeholder
until an unrelated refetch.

Not taken: widening the probe budget again to cover the trigger host's
per-attempt timeout. That is the third arithmetic for the same knob, and no
fixed budget closes the class — a Retry-After refusal can defer admission by
up to a day. The ceiling is deliberate and the miss degrades to the next list
refetch.

* 🩹 fix: Honour Retryable Statuses and Keep Archived Chats Out of the Sidebar

Two narrow ones from codex, plus a test-coupling bug they exposed.

408 and 429 ask to be retried by definition, and a proxy in front of the app
can raise either without the run being involved, so a blanket "any 4xx but
404 is terminal" abandoned a watch over something that was going to resolve.

The conversation route serves an archived chat while the list query filters
one out, so a run archived from another tab between its first write and the
next probe would land a row in the sidebar that no refetch agrees belongs
there. The archive was deliberate; leave it alone.

The archived-row assertion passed at first against the missing guard, because
`clearAllMocks` leaves an undrained `mockRejectedValueOnce` queued for the
next test, where it answered the first probe. Reset implementations between
tests so one test's leftovers cannot vouch for another's.

* 🩹 fix: Bind the Session Fence Late When the User Query is Still Loading

The fence I added last round read an unloaded user cache as an identity. A
run started before that query resolved captured `undefined`, and the
account's own arrival then looked like the cache changing hands, so the
watch abandoned a run it should have tracked — the fence failing closed on
the feature it was protecting.

Run-now answering 200 is itself proof a session existed at the click, so an
empty entry then is a cache still loading rather than nobody signed in. Bind
the owner on first sight instead, and fence only once one is known.

Also restores import order in the spec, which is what Static checks caught:
a later edit added an import without re-running the sorter.

* 🩹 fix: Refuse the Write While the Cache Owner is Unknown

Last round's fence compared only once an owner was known, which left the
hole it was built to close: a run started before the user query resolved
kept an undefined baseline, so a sign-in arriving while the probe was in
flight was never noticed and one account's chat could seed another's cache.

An unknown owner cannot be compared, so it cannot be seen to change either —
"still loading" and "changed hands" are the same reading. Refuse the write
instead. A late-resolving query still binds and still tracks, because that
happens before the probe; only the genuinely ambiguous case is dropped, and
every caller of this has a signed-in session.

The tests now sign in, which is the only state this runs in, and the one
that starts signed out keeps its premise so the boundary stays pinned from
both sides.

* 🩹 fix: Write the Session Fence Once and Mirror Foreground Project Bookkeeping

Four rounds of fence patches each fixed one cell of a three-state problem
and broke another. Write it as one state machine instead, on the property
that makes it tractable: the server enforces ownership on the probe itself,
so a request sent under one account can never return another's chat, and
the only dangerous response is one sent under the account that clicked and
landing after someone else signed in. So the owner is bound only from a
reading taken before a probe goes out, a successful probe with no baseline
yet is skipped rather than abandoned, and once bound any different reading
ends the watch.

Mirror the foreground path's project bookkeeping: a chat filed under a
project moves that project's count and recent-activity ordering, which live
on the project rows the conversation caches never touch.

The watch moves to its own module so the schedules panel can hand it an
automatic occurrence, and a run announced twice — by the click and by the
poll that then lists it — is watched once.
2026-09-06 12:34:50 -04:00
Danny Avila
e2cee9659c
✅ test: Add the Portable Tenant-Isolation Contract (#15662)
`policy.spec.ts` proves the tenant rules with no database; `probe.spec.ts`
proves the Mongoose binding reaches the wire scoped. Neither answers what a
second storage engine is actually required to do.

`describeTenantIsolationConformance(harness)` is that answer — 13 behaviours an
engine must exhibit, expressed against a small harness interface rather than
any engine's query surface. Mongoose supplies the first harness; a PostgreSQL
engine satisfies the same suite, which is what turns the seam from
hypothetical into real. Engine-specific cases stay in
`models/plugins/tenantIsolation.spec.ts`.

Writing the contract surfaced one asymmetry worth pinning rather than
rediscovering: an insert naming another tenant is refused in strict mode but
tolerated otherwise, because pre-tenancy backfill has to write rows for a
tenant other than the ambient one. Both halves are now asserted.

Test-only; nothing here is bundled. Originally #15585, reopened directly on
`dev` because GitHub locks the base of a stacked PR and this change is
independent of the runtime fix in #15582.
2026-09-06 12:26:02 -04:00
Ravi Kumar L
de888cc362
🎈 ci: Gate Page Loads Under Database Latency (#15646)
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-06 12:01:44 -04:00
Danny Avila
85c90bf4c7
✅ test: Prove Tenant Scoping at the Driver, Below the Engine Binding (#15578)
Tenant isolation is enforced by the storage engine's own binding — Mongoose
middleware today. No amount of reading the binding answers whether it covers
every path, including the ones nobody enumerated, so this checks from below:
`attachTenantProbe` observes the commands that actually reach the wire via the
driver's `commandStarted` monitoring. The check does not depend on the binding
being correct, which is the point of putting it there.

It asks one narrow, decidable question — did the binding's own contribution
arrive, for the tenant active when the command was issued? — rather than trying
to decide tenant-safety for arbitrary filter algebra. Anything it cannot judge
is reported unscoped, so an unfamiliar command shape surfaces as a failure
instead of a silent pass.

`probe.spec.ts` runs the whole enforcement surface through it: 13 query
operations, 3 write paths, and the document-level paths (`doc.save()`,
`doc.updateOne()`, `doc.deleteOne()`, `populate()`) that make schema
middleware irreplaceable by a wrapper around the model's statics.

Test-only; nothing here is bundled.

One gap found and pinned for the next change: `doc.save()` on a persisted
document is filtered on `_id` alone. `estimatedDocumentCount()` is pinned as a
known unscoped side channel (one call site, boot path, metadata only).
2026-09-06 11:44:39 -04:00
Marco Beretta
fdd5f9ce65
🖼️ fix: Precache PWA Icons and Let Them Refresh (#15654)
* 🖼️ fix: Precache PWA Icons and Let Them Refresh

Production builds set `publicDir: false`, so `client/scripts/post-build.cjs`
copied `public/assets` into `dist/assets` only after `vite build` had finished.
vite-plugin-pwa globs `dist/` from its `closeBundle` hook, inside that build, so
the icon patterns in `workbox.globPatterns` matched zero files and the generated
`sw.js` shipped without a single icon, silently: workbox treats an empty glob as
a no-op. `build:ci`, used by the Playwright lanes, never ran the post-build step
at all and produced a `dist` with no icons or `robots.txt`.

Move the copy into a `copyPublicAssets()` build plugin that runs in
`writeBundle`, which completes for every plugin before `closeBundle`, and drop
the post-build script with its lone `fs-extra` dependency.

Ordering alone was not enough. The plugin defaults `dontCacheBustURLsMatching`
to `/^assets\//`, true only for hashed bundle output. The icons live in
`assets/` under stable filenames, so they entered the manifest with
`revision: null`, permanently immutable, meaning an installed PWA would never
pick up a rebranded icon: the other half of the report. Match Vite's
`[name].[hash].[ext]` shape instead so hashed chunks stay revision-free while
the icons carry content revisions.

Also drop `manifest.webmanifest` from `globPatterns`, since the plugin always
appends it as an additional manifest entry and globbing it duplicated the
precache entry.

A clean `npm run build` now yields 55 precache entries with all five icons
revisioned, against 0 icon entries before.

* 📝 docs: Describe the Full Copy Set in copyPublicAssets

* 🔒 chore: Drop fs-extra From bun.lock
2026-09-06 11:32:09 -04:00
Danny Avila
d812e2f98f
📏 style: Center Every Row Glyph on the Avatar's Axis (#15643)
* 📏 style: Center Every Row Glyph on the Avatar's Axis

Two rails were in use under a message header. `AgentHandoff` and `AgentUpdate`
gave their glyph a 24px slot — the width of the header avatar — so the glyph
centered on the avatar's axis and the text started where the name does. Every
other row used a 16px (thinking: 18px) slot: tool cards through `ProgressText`,
`ToolCallGroup` headers, the phase card's check, and the streaming cursor a
folded card renders beneath itself. Measured in Chromium against the avatar's
left edge: avatar center 12, those glyphs 8–9, and their text at 24–26 against
the name's 32. The cursor was also 5px low, because `.result-thinking` hangs
its dot 11px above a text line's box, which is right for the streaming
markdown cursor and wrong for a row.

`ROW_GLYPH_SLOT` in `rows.ts` is the one definition: a 24px, centered slot
(`min-w` rather than `w`, so a stacked icon strip can run wider). It is taken
by `ProgressText`'s icon, both `ToolCallGroup` glyph branches, the phase
check, the thinking button, and the cursor; `AgentHandoff`'s gap drops from
2.5 to 2 so its text meets the name too. The cursor stops borrowing
`EmptyText` and draws the dot through `.result-thinking-inline`, a variant
that puts the same pseudo-element in flow so the slot can center it.

After: every glyph at 12, every text edge at 32, the dot centered in its row.

* 📏 fix: Keep the Glyph Slot Inside the Row and the Dot Override Local

Review follow-ups on the row rail.

- The slot was 24px tall inside a 20px row. `ProgressText` positions its
  content absolutely at the row's top, so a taller slot could not be centered
  by the row and carried every call card's glyph and label 2px low. The slot
  keeps its 24px width and takes the row's 20px height.
- The pending skill row rendered its icon straight into the gap; the real
  `SkillCall` that replaces it in place used the slot, so the swap moved the
  glyph 4px and the text 8px. Both rows share the slot now.
- The cursor's in-flow dot no longer needs a global selector: `after:!static`
  on the element puts the one pseudo-element back in flow, and the `!` is what
  outranks the dot rule's three-class selector. The thinking button takes the
  shared slot instead of its own 24px square.
- A `span` wrapped `StackedToolIcons`, which renders a `div` for a strip; the
  wrapper is a `div` now.

* 📏 fix: Give ThinkingLabel the Shared Glyph Slot

`ThinkingLabel` exists to keep the reasoning-unavailable row identical to the
`ThinkingButton` row it stands in for, and the previous commit moved only the
button onto the shared slot — a detached subagent projection would have kept
its glyph at 9px and its text at 24px next to rows at 12px and 32px.

* 📏 fix: Put the Image-Gen and Web-Search Glyphs in the Shared Slot

`Part.tsx` routes image generation and web search to renderers that draw
their own glyph instead of going through `ProgressText`, so the slot the
previous commits gave every other row skipped them: their 16px icons and the
favicon stack still sat on the old axis, and a labeled phase containing either
call would have shifted sideways as it folded into the updated header. Both
rows take `ROW_GLYPH_SLOT`; the web-search streaming row's gap drops from 2.5
to 2 so its text meets the name's edge like the rest.
2026-09-06 11:31:35 -04:00
Danny Avila
b3b8986752
🪠 fix: Unblock Failed OpenID Publication Flights (#15641)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* fix: settle failed OpenID authentication publications

* test: simplify recovery dependency assertion

* test: provide a complete typed recovery fixture

* fix: distinguish preparation errors from completion dispatch
2026-09-05 23:58:08 -04:00
Danny Avila
aeac7a2065
🪪 feat: Add Per-Agent Git Identity (#15628)
* feat: Add per-agent Git identity

* fix: annotate git identity schema

* fix: complete git identity controls

* fix: clear git identity on version restore

* fix: Require a complete agent Git identity

* fix: Complete Git Identity Across Bash And Form Validation

* fix: Discard Incomplete Hidden Git Identity Edits

* fix: validate Git identity across panel lifecycle

* fix: preserve saved Git identity when discarding drafts
2026-09-05 23:56:23 -04:00
James Todaro
c9b9644163
🔔 fix: Honor Toast Duration and Support Persistent Toasts (#15049)
* 🔔 fix: Honor Toast Duration and Support Persistent Toasts

* 🔔 fix: Annotate the single-run effect in the toast spec

* 🔔 fix: Give each toast its own close deadline

- a toast replacing one still open inherited the superseded Radix deadline and could close early
- the shown-toast id keys the Radix root and gates `onOpenChange`, so a stale timer is a no-op
- drops the competing hide timer, leaving Radix as the single dismissal owner

* fix: align and emphasize persistent toast close button

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2026-09-06 04:38:15 +02:00
Mihidum
54511b2b8c
🧠 fix: Return 400 for Invalid Memory Keys (#14618)
* fix: return 400 for invalid memory key format instead of 500

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: trim PATCH rename key to match POST normalization

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: return 400 when PATCH rename key is not a string

A non-string `key` in the PATCH body (e.g. {"key": 123}) previously fell
back to the URL key and updated the memory with a 200, hiding the invalid
field. Reject any defined non-string key with 400 before normalization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: share memory key validation between schema and routes

* fix: reject blank memory rename keys

* test: use real data-provider exports in memory route suite

* fix: share memory key validation with ID updates

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2026-09-06 04:34:06 +02:00
Marco Beretta
971e076015
🎛 refactor: Replace Agent Skills Toggles With a Three-Way Mode Control (#15520)
* refactor: replace agent skills toggles with a three-way mode control

The Skills section in the agent builder exposed one dependent choice as two
peer switches. "Use all skills" only meant anything while "Enable skills" was
on, and turning it on hid the skill list, so the user could not see what "all"
covered. Two Add affordances competed for the same action and both labels
leaned on a tooltip to explain themselves.

Replace both switches with a single segmented control over the SkillsScope
enum that already existed. Off, All, and Selected now write skills_enabled and
skills_scope outright, so the mode is the only source of truth for catalog
exposure and the allowlist survives a round trip through All.

Extract the section into SkillsSection. That drops the badgeText, showAdd,
showBody, and children props SelectedSection only carried to bend the tools
layout into a skills layout, and it retires skillsSelectionTransition, which
inferred the capability flags from the allowlist length and flipped skills off
whenever the last skill was removed.

All mode reports how many skills the deployment exposes and expands a
read-only list, so the catalog is never hidden. Selected mode keeps one Add
affordance: the dashed card while nothing is chosen, a header button once rows
exist.

Legacy agents keep loading through resolveAgentSkillsScope. The one ambiguous
persisted shape, skills_enabled with no explicit scope, is normalized on load
without dirtying the form so the next save records the mode explicitly.

Also give the shared Radio the roving tabindex and arrow key handling a
radiogroup owes its users; every segment was previously its own tab stop with
no way to change the selection from the keyboard.

* chore: add temporary PR comparison screenshots

* chore: drop temporary PR comparison screenshots

* fix: address PR review bot findings on the skills mode control

Codex (P1):
- Compare the resolved mode, not the raw persisted scope, before treating a
  segment click as a no-op. An agent left with `skills_enabled: false` and a
  retained `skills_scope` renders as Off, so matching on the raw field
  swallowed the click that re-enables it while the shared Radio moved its own
  selection, leaving the form clean and the disabled state persisted.
- Exempt explicit scopes from the legacy allowlist normalization in
  AgentSelect. Off deliberately keeps the allowlist so returning to Selected
  restores it, and flipping `skills_enabled` there persisted skills-enabled on
  an agent shown as Off, which skillDeps reads as permission to inject the
  skill-authoring tools.

Codex (P2):
- Report a failed catalog request instead of rendering it as an empty catalog.
  All mode showed "0 skills available" over an empty list when /api/skills
  failed; it now renders the picker's alert with a retry.
- Stop paging the whole catalog before the list is opened. Merely viewing an
  All-scoped agent walked every cursor at 100 records a page and retained each
  summary. Pagination now waits for the expansion, and the header reports the
  loaded count as a floor while more pages remain.
- Honor an explicit `skills_scope` in both skill cleanup paths. Deleting the
  last allowlisted skill, or pruning a dangling id on save, forced
  `skills_enabled` to false without consulting the scope, so an All-scoped
  agent that had once picked a single skill was silently switched off. The
  fail-closed inference now applies only to legacy agents that persist no
  scope at all.

Copilot:
- Skip the onChange when Home or End lands on the already-checked segment.
  Focus still follows the key; only a selection that does not move stops
  firing a form write.

* fix: address second round of PR review bot findings

Codex (P1):
- Normalize authoring left on under a resolved Off. An agent carrying
  `skills_enabled: false` with `skill_authoring_enabled: true` renders as Off,
  yet skillDeps enables the skill-authoring tools for either flag, so it ran
  with skills the section reported as disabled. Clicking the already-selected
  Off segment is a no-op, so nothing ever cleared it. The section now clears
  the flag on load without dirtying the form, matching what selecting Off
  already writes.

Codex (P2):
- Apply the scope-aware pruning in createAgent and revertAgentVersion too, not
  only updateAgent. Restoring an All-scoped version whose skills have since
  been deleted silently came back as Off, and createAgent had the same
  unconditional branch for ids that go stale before the write lands.
- Collapse the duplicate first-page skills request. ToolsSection listed skills
  through useListSkillsQuery while the section and the picker used
  useSkillsInfiniteQuery, so the same endpoint was fetched under two React
  Query keys and could not be deduplicated. All three consumers now share the
  infinite query, which leaves one cache entry instead of two.

Also sorts the imports the repo's checker flagged in SkillsSection,
ToolsSection and the section's spec.

* fix: clear the skills master flag under a resolved Off

`skills_enabled: true` with `skills_scope: none` is a shape the API accepts,
and the resolver reports it as Off. `skillDeps` reads the master flag on its
own as permission to expose the skill-authoring tools, so the agent ran with a
capability the section said was disabled, and because clicking the
already-selected Off segment is a no-op there was no way to clear it without
cycling through another mode.

The normalization that already cleared `skill_authoring_enabled` now clears
both flags whenever the resolved mode is Off, still without dirtying the form.
Neither write fires while a catalog mode is active.

* fix: disable skills for an explicit none scope in allowlist cleanup

The scope-aware fail-closed rule exempted every explicit scope, which spared
one shape it should not have: `skills_enabled: true` with `skills_scope: none`.
The API accepts that combination, the builder renders it as Off, and skillDeps
reads the master flag on its own as permission to expose the skill-authoring
tools. Cleanup runs server side without the builder mounted, so the frontend
normalization cannot repair it.

Only `all` and `selected` opt out now, since each already defines what an
empty allowlist means. The three self-heal paths in agent.ts share a
`requiresSkillsDisable` predicate, and the Mongo filter in skill.ts uses
`$nin: [all, selected]`, which still matches an absent field for legacy
agents.

* test: align the skills e2e spec with the explicit-mode contract

Three assertions still encoded the removed toggle behavior:

- The remove button is labelled `Remove {name}` in the new section, not
  `Remove from agent`, so both removal steps waited 180s for a button that no
  longer exists. That was the timeout, not a failed expectation.
- Emptying the allowlist no longer infers Off. The save writes `skills: []`
  and leaves the mode at Selected, which resolves to no skills at runtime, so
  the spec now asserts `skills_enabled: true` with `skills_scope: 'selected'`
  and keeps the existing proof that the picker and the run expose nothing.
- All leaves `skills` untouched, so a fresh agent serializes no allowlist at
  all rather than an empty one: `composeAgentUpdatePayload` always carries the
  four skill fields and `JSON.stringify` drops the undefined ones. Create and
  Off now assert `skills_scope` instead of an empty array.

Adds `skills_scope` to the spec's payload and detail types.

* fix: address third round of PR review bot findings

All three are P2 accuracy and cost issues in All mode:

- Count only the skills the runtime would inject. The list endpoint returns
  every skill the user can VIEW, while catalog resolution filters each through
  `resolveSkillActive`, so a shared skill left inactive was counted and listed
  as available even though the agent never receives it. The section now shares
  the `isActive` predicate the chat-side picker already filters on, so the
  count cannot promise more than the runtime delivers.
- Report loading instead of an empty catalog. With a cold query `data` is
  undefined and the count derived to "0 skills available", presenting an empty
  deployment while it was merely loading.
- Skip the allowlist lookups outside Selected. `useResolvedSkills` issues one
  `getSkill` per allowlisted id missing from the first catalog page, and since
  All now retains previous picks, merely viewing such an agent could fire
  hundreds of requests for rows that mode never renders. The fallback is gated
  on Selected, where the rows exist.

* fix: gate the All catalog on resolved skill states

`useSkillActiveState` substitutes an empty override map while its own request
is in flight and after it fails, so `isActive` reports every skill active in
both states. The section consumed the predicate but discarded the status, so a
deliberately deactivated owned or deployment skill was counted and listed as
available: briefly during load, and permanently after an error.

The count now waits for both requests, and a failure in either renders the
alert instead of a count the runtime would not honor. Retry refetches both,
which needed the hook to expose its `refetch`.

* fix: resolve the config behind defaultActiveOnShare before filtering

`useSkillActiveState` reads `defaultActiveOnShare` from the interface config
and falls back to `false` while that request is pending or after it fails, so
a deployment configured with `true` had its shared skills filtered out of the
All count until the config landed, and permanently on error. The section only
gated on the skills and skill-states queries.

Both dependencies feed `isActive`, so the hook now reports them as one
signal: `isLoading` and `isError` cover the override map and the config
alike, and `refetch` retries both. A consumer gating on the pair can no
longer miss a dependency, which is how the previous gap arose. The section is
the only consumer of those flags.

Also composes the retry from the shared `Button` (`outline`, `sm`), matching
the picker's own retry instead of a feature-local recipe.

* fix: report what the All catalog loaded, not a lower bound

`hasNextPage` tracks raw pagination while the rows are filtered to the
runtime-active skills, so the two disagree: 30 active skills followed only by
deactivated ones made the header read "30+ skills available" when 30 was the
true total, and it stayed wrong until the user expanded the list and finished
paging.

While pages remain the header now describes what has been loaded rather than
claiming a floor on what is available, so it cannot overstate the catalog. The
exact "N skills available" phrasing still lands once the catalog is complete.
`com_ui_skills_available_count_more` is replaced by singular and plural loaded
keys.
2026-09-06 03:32:56 +02:00
Marco Beretta
8144574b43
♿ feat: High Contrast Light and Dark Appearance Modes (#15178)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* refactor: read status fill labels from the theme token

Feature call sites paint the label on bg-surface-destructive,
bg-surface-submit and the bg-status-*-strong fills with the raw
text-white utility, while the shared Button, IconButton and Toast
primitives already use text-text-on-status. The token exists so the
label colour can differ per theme mode, and hard-coding white pins it.

Point every call site at text-text-on-status, and compose
DeleteIconButton onto the Button destructive variant rather than
re-declaring the fill classes. Five call sites paired a status fill with
the wrong token entirely (text-text-secondary, text-text-inverted) or
used the non-strong hue as a solid fill; those move to the -strong fill
with the on-status label. Tooltip.css and the app stylesheet copy of the
same rule read colour from --text-primary instead of literal black and
white, and the dead outline: var(--bg-surface-hover) in Dropdown.css
becomes a real 2px token outline, since that variable is not declared
anywhere and the focus ring never rendered.

--text-on-status is 255 255 255 in both shipped themes, so this is a
no-op for the light and dark palettes.

* feat: add high contrast light and dark appearance modes

Adds two appearance modes to the theme setting for users who need more
than the standard palettes, plus contrast resolution for the system mode.

The palettes are complete IThemeRGB maps rather than partial overrides,
because a token left to fall back to defaultTheme or darkTheme would
reintroduce a mid-grey the mode exists to eliminate. They resolve as a
built-in ThemeDefinition that outranks a deployment's custom theme, since
a contrast choice is an accessibility need rather than a branding
preference.

Text clears WCAG AAA on every surface it can render on, including the
hover and active fills. Borders, rings and series marks clear the 3:1
non-text floor. Solid fills clear AAA against both their label and the
page, which is what the per-mode text-on-status token buys: each mode
paints its fills on the far side of its own canvas, dark fills under a
white label on white and bright fills under a black label on black, so
the two ratios coincide instead of pulling apart.

The system mode now follows prefers-contrast the same way it already
follows prefers-color-scheme, so a user who has switched on Increase
contrast or Contrast themes gets the accessible palette without first
finding this setting. isDark reports high-contrast-dark as dark;
isHighContrast answers what the user picked, which is what the theme
toggle preserves when it flips the scheme; resolvesToHighContrast answers
whether the palette applies.

The icon toggle keeps a contrast choice when it flips the scheme, and
announces which of the four states it landed in.

* feat: cover the non-tokenized chrome in high contrast

The token layer reaches every IThemeRGB surface, but a good deal of the
stylesheet predates it and holds literals the palette cannot move. Under
html.high-contrast:

The typography variable block is 32 hard-coded hex values, and message
content carries prose dark:prose-invert, so the invert half is what dark
mode reads. Both halves now resolve from tokens: prose links were 3.77:1
on a black canvas and 5.40:1 on a white one, and every rule and border in
the set sat below the 3:1 non-text floor in one mode or the other.

The highlight.js palette is raw hex and tops out at AA, worst cases
4.84:1 for dark attributes and 5.32:1 for the alpha-blended dark comment;
these reuse the palette's own hues at AAA on each canvas.

Focus perimeters go to 3px and read the ink token rather than literal
black and white. Selection was left to UA system colours and is now an
explicit inversion. Ring opacity is pinned at full strength for the
controls that halve their own. Popover and tooltip elevation trades a
black shadow, which separates against nothing on a black surface, for a
token border. Prose links carry an underline so they are not distinguished
by colour alone. The switch track, the scrollbar thumb and track, the
markdown structure borders and the body background that .dark pins to
#212121 all move onto tokens.

* feat: draw panel and dialog edges in high contrast

Adds a high-contrast: Tailwind variant to the shared preset, so styling
can be scoped to the contrast modes the way dark: is scoped to the dark
class. Deliberately not named contrast-more, which Tailwind already ships
as the raw prefers-contrast media query: this follows the resolved app
mode, so it covers an explicit choice as well as the OS preference.

Two surfaces rely on a colour step that high contrast removes. The
sidebar's right edge is a resize separator that is transparent until
hovered, which is fine where surface-primary-alt carries the boundary but
leaves no edge at all when both sides are the same pure white or black.
Dialogs are borderless and lean on a black shadow, which separates
against nothing on a black canvas.

Both now draw a real edge in the contrast modes only. The dialog change
sits in the Radix content primitives and the two shared templates so the
feature dialogs inherit it, plus the settings panel, which is Headless UI
and bypasses them. OGDialogTemplate carries border-none, so the variant
restores the border style as well as the width.

* fix: address PR review bot findings

chatgpt-codex-connector:

Move the syntax palette into semantic theme tokens. The highlight.js
colours were raw hex in style.css, so a palette change had to be made
twice and neither copy was covered by the registry's completeness check
or any contrast test. They are eight rgb-syntax-* tokens now, declared
across all four theme maps and consumed as CSS variables, which lets the
whole high-contrast block for highlight.js go away. Two guardrails come
with them: the contrast modes assert AAA for every syntax role on the
code surface, and the stylesheet is pinned to the runtime themes with no
hard-coded hex allowed in an .hljs rule.

Two values could not be carried over literally: the dark comment and meta
colours were alpha-blended whites, so they are stored as the flattened
equivalents over the #212121 code surface. Everything else, including the
dark code text, renders byte-identical to before.

Declare Tailwind as a preset dependency. The preset ships as a raw file
through the ./tailwind-preset export while tailwindcss is only a
devDependency, so require('tailwindcss/plugin') could fail to resolve for
a consumer under pnpm or Yarn PnP. Dropped the require instead of adding
a peer dependency, since Tailwind accepts a plain function in plugins and
the wrapper only adds option handling this variant does not use.

Route remaining solid controls through semantic status roles. The
assistant builder kept bg-red-600/bg-red-700 and bg-green-500 with
text-white, which the built-in palette cannot reach, leaving them below
the mode's contract at roughly 2.28:1 for the green controls. All five
now use the destructive and submit surfaces with the on-status label, and
the two that also suppressed their focus ring get a semantic one.

* fix: address second round of PR review bot findings

chatgpt-codex-connector:

Migrate the builder submit to semantic status colors. AssistantPanel's
submit button kept btn-primary, which hardcodes #10a37f and white in
style.css, alongside a raw green hover and focus border. It uses the
submit surface with the on-status label now, and drops btn-primary rather
than fighting its specificity. Left .btn-primary itself alone: restyling
every primary button in the app is not this PR's business.

Give provider avatars contrast-safe brand colors. The white glyph sat at
2.30:1 on OpenAI green, 2.45:1 on Anthropic tan and 3.41:1 on GPT-4
purple. brands was a single theme-wide set, and a brand fill has to carry
a glyph and stand out from the canvas, both of which flip between modes,
so one set cannot serve both at enhanced contrast. ThemeModeDefinition
takes an optional brands block now, resolved after the theme-wide set, and
high contrast declares dark tints under a white glyph for light and bright
tints under a black glyph for dark. Worst pair is 8.76:1 for both the
glyph and the silhouette, with provider hues still recognisable. Brand
validation is shared between the two levels rather than duplicated.

Raise active-surface contrast against the canvas. The active fills sat at
1.48:1 and 1.98:1 on white, 1.93:1 and 2.91:1 on black, and since every
ink token collapses to one colour here, dozens of call sites were left
conveying the selected state with a sub-3:1 fill alone. The fills now
clear 1.4.11 at 3.14:1 to 4.29:1.

That costs the AAA label on those two fills, and the trade is unavoidable:
above the luminance where a fill clears the 3:1 state floor, its own label
cannot reach 7:1, and below it the state disappears. No integer grey
satisfies both. 1.4.11 is Level AA and 1.4.6 is Level AAA, so the state
wins and the labels land at 4.89:1 to 6.69:1. The spec now separates
canvas and hover surfaces, which keep AAA, from the active fills, which
assert the state floor and an AA label.

* fix: address third round of PR review bot findings

chatgpt-codex-connector:

Drop the overriding btn-primary class from Select. My previous round
assumed Tailwind utilities would win here by source order; they do not.
In the built stylesheet .btn-primary sits at byte 136886 and
.bg-surface-submit at 69233, equal specificity, so the hardcoded green won
and that button had never followed the palette. Removed btn-primary, the
same way as the submit button beside it.

Preserve contrast mode when theming Mermaid output. isDark collapses a
contrast mode onto the same boolean, so the cache key was unchanged across
light to high-contrast light and an already-rendered diagram survived the
switch untouched. Both diagram paths take the resolved contrast now: the
cache key gains a suffix, and the config carries themeVariables built from
the contrast palette, since Mermaid's own neutral and dark themes hold a
palette no theme token can reach. Diagrams render on the canvas with ink
nodes, ink edges and ink labels, which is 21:1 for every mark and label.

Artifact diagrams go through the same helper: getMermaidFiles takes the
flag, switches the generated document to Mermaid's base theme, which is
the only built-in that honours themeVariables, and paints the page with
the contrast canvas instead of the fixed #212121 or #FFFFFF. The helper
returns undefined outside the contrast modes, so standard themes keep
their current rendering exactly.

* fix: publish resolved contrast through theme context

chatgpt-codex-connector: notify consumers when system contrast changes.

Under `system` the contrast comes from a media query, so `theme` stays
`system` when the OS preference flips and React has nothing to rerender
on. The listener I added last round only touched the root element, which
is enough for CSS but not for anything computing from the resolved value:
the Mermaid cache key and the artifact files would have kept the old
palette while the rest of the app switched.

ThemeContext exposes `highContrast` now, held as state and set wherever
the mode is applied, which covers both an explicit choice and a media
change. `useMermaid` and `useArtifactProps` read it from context instead
of deriving it locally. `resolvesToHighContrast` stays exported as the
predicate the provider itself resolves with.

The existing media-change test now also asserts the context value moves,
not just the class on the root, since the DOM assertion alone passed
before this fix.

* fix: publish the resolved scheme through theme context too

chatgpt-codex-connector: publish resolved scheme changes through context.

Same staleness as the contrast case, one field over. With the mode at
`system` and contrast already on, an OS colour-scheme change reapplied the
DOM palette but `setHighContrast(true)` was a no-op and `theme` stayed
`system`, so no consumer rerendered and the diagram hooks kept the
previous `isDark` result. This half predates the PR: those hooks derived
the scheme from `theme` before it too, so a scheme flip under `system` has
never reached them.

ThemeContext exposes `resolvedMode` alongside `highContrast`, both set
wherever the mode is applied. The two diagram hooks read the scheme from
context instead of calling isDark themselves.

The other isDark call sites are left alone: none of them cache, so they
recompute whenever something else rerenders them, and sweeping every
consumer onto the context value is a wider change than this PR should
carry.

* fix: address sixth round of PR review bot findings

chatgpt-codex-connector:

Restore a visible focus cue for dropdown options. `.select-item` blanks
its own outline with `outline: none !important`, so the contrast focus
rule never landed and the navigated option was marked only by a
surface-hover fill at 1.48:1 on white and 1.93:1 on black. Hover stays
deliberately soft in these palettes, since it is transient and can afford
an AAA label, so the cue has to be a perimeter: a 3px inset outline on
the active and focused option, matching the !important it has to beat.
Verified with real keyboard navigation that the outline resolves to the
ink colour in both modes.

Apply contrast tokens to artifact controls. getMermaidFiles took the
contrast flag last round but still built its controls from the standard
palettes, whose fixed greys leave the zoom label below AAA and whose
10%-alpha border and divider sit below the 3:1 non-text floor against a
pure canvas. The controls now come from the same tokens as the diagram:
canvas fill, ink border and label, the active surface for hover, and no
shadow, since a black shadow separates against nothing there.

* fix: ignore undefined brand overrides when resolving a theme

chatgpt-codex-connector: ignore undefined mode brand overrides.

Partial<IThemeBrands> lets a key be present with undefined, and the brand
validator accepts it, so spreading a mode block over the inherited set
could overwrite a brand with nothing. Colors survive this because
mapColors skips undefined, but every brand token is written to the DOM
unconditionally, so the avatar would have lost its fill instead of falling
back the way a partial theme promises.

Both spreads now drop undefined entries, which covers the theme-wide set
as well as the mode block, and keeps ResolvedThemeDefinition.brands honest
about being fully populated.

* fix: address eighth round of PR review bot findings

chatgpt-codex-connector:

Guard media queries during server rendering. The state initializers I
added read prefers-color-scheme and prefers-contrast synchronously during
render, so a server-rendered provider threw before producing markup. That
was a regression: nothing in the provider read a media query during render
before. Both reads go through a helper that returns false without a
window, the way the storage helpers already tolerate its absence, so the
server renders the light palette with no contrast override and hydration
applies the real values. The subscription effect is guarded too, which
also covers a host that has no matchMedia at all.

Keep Mermaid pie slices distinct in contrast mode. My own regression: the
contrast variables set primaryColor, secondaryColor and tertiaryColor to
the canvas, and mermaid derives pie1 through pie3 from those, so every
slice collapsed into the background and into its neighbours. The pie slots
come from the palette's series ramp now, which is the categorical scale
already built to clear the mark floor on that canvas and stay separable
under simulated deuteranopia. Twelve slots wrap over seven, as mermaid's
own palettes do.

Remove the raw white override from bookmark submit. The save button passed
text-white into the shared submit variant, which tailwind-merge resolved
in favour of the local class, leaving white on the bright submit fill at
1.40:1. Dropped the override so the variant's token applies.

Preserve the high-contrast canvas in screenshot exports. The export chose
between two fixed colours, so a contrast export was filled with #171717
instead of the mode's canvas. It reads the resolved canvas token now, with
the old pair as the fallback.

Theme markdown artifact previews in contrast modes. useArtifactProps
forwarded the appearance only to the mermaid branch. getMarkdownFiles
takes it now and appends a contrast block derived from the palette; being
unconditional it also outranks the sheet's own prefers-color-scheme query,
so an explicit contrast choice is honoured whatever the OS reports.

* fix: migrate agent version controls to semantic status roles

chatgpt-codex-connector: migrate agent-version controls to semantic status
roles.

The active-version marker and the Restore action used raw green fills with
text-white, about 2.28:1 and 3.30:1, and the built-in palette cannot reach
either. The marker takes the strong success fill with the on-status label,
and Restore takes the submit surface, matching the assistant builder
migration.

Also finished the rest of that component's active-version treatment rather
than leaving it half migrated: the card border and tint, the title, and
the Current chip now use the success subtle, border and strong roles. The
same indicator was expressing one state through both raw and semantic
colour otherwise.

ToolCard's native badge and selected tick were the same shape, raw emerald
under white, so they move to the strong success fill too.

Left alone: the HTTP method badges in ActionsTable/Columns.tsx, the
tool-kind colours in items/icons.ts, and CategoryFilter's active border.
Those are categorical identity rather than status, so mapping them onto
status roles would be wrong; they need the treatment provider brands got,
which is its own change.

* fix: address tenth round of PR review bot findings

chatgpt-codex-connector:

Use opposing ink for pie-section labels. My own regression from the slice
fix: the series ramp deliberately sits on the far side of the canvas so
slices are visible, which makes the canvas ink the wrong colour for a
label drawn inside one. Section labels take the on-status ink now, the
same colour the palette uses for a label on any solid fill, while the
title and legend keep the canvas ink because they are drawn on the canvas.

Preserve the blue initializing status in standard themes. Also mine, from
the first round: I moved this dot to bg-status-info-strong, and the strong
info slot is a neutral grey in both standard palettes, so a blue
indicator turned grey. Back on bg-status-info, with the pulse taking the
on-status ink so it inverts with the fill rather than being a fixed white.

Route action method badges through contrast tokens. I declined this last
round as categorical identity rather than status; the reviewer's framing
is better. The series ramp is precisely the home for categorical identity,
it is contrast-checked per mode, and its slots are separable under
simulated deuteranopia, so the verbs map onto it while staying close to
their conventional colours. `delete` keeps a red by taking the error role,
since destructive genuinely is a status.

* fix: seed published theme values without reading a media query

chatgpt-codex-connector: defer media-query resolution until after
hydration.

Guarding window stopped the crash but left the server and the first client
render disagreeing: the server produced light and false while the client
resolved the OS preferences during that same first render, so any child
reading the published values hydrated against different context.

Both are seeded from the mode string alone now. An explicit mode needs no
query, so it publishes immediately and stays consistent with isDark; only
system starts at the light palette with no contrast override, on both
sides, and applyThemeMode publishes the resolved values from its effect
after hydration.

The pre-existing localStorage read in getInitialTheme has the same shape
of problem for `theme` itself, but changing where the stored mode is read
is a wider change than this and belongs on its own.

* fix: address eleventh round of PR review bot findings

chatgpt-codex-connector:
- keep the active version timeline rail opaque so it clears the 3:1 mark
  floor in both high contrast modes instead of compositing to ~2:1
- balance the dark success fill (#088759) so the selected-tool marker,
  the timeline rail and the prompt-version chip keep a visible silhouette
  on the #212121 panel while the white label stays at WCAG AA
- include the resolved scheme and contrast in the mermaid cache key even
  when a custom theme is supplied, so a contrast switch re-renders

* fix: address twelfth round of PR review bot findings

chatgpt-codex-connector:
- select mermaid's base theme ahead of a caller-supplied theme in the
  contrast modes, after the config spread, so the contrast variables
  actually reach the diagram
- ship the tooltip and popover contrast edges in the package stylesheets
  they belong to, so a consumer importing @librechat/client gets them

* chore: sort imports in the theme registry, provider and specs

* fix: override the markdown preview table header in contrast modes

The appended contrast block answered the ink, the canvas and the cell
borders but not the thead tint, so an explicit contrast choice on the
opposite OS scheme left white text on #f6f8fa or black text on #161b22.
The header now takes the palette canvas and reads through its bold cells
and border, and a new guardrail asserts every colour the base sheet sets
inside prefers-color-scheme is answered by the override.

* fix: address thirteenth round of PR review bot findings

chatgpt-codex-connector:
- keep an OS-requested contrast when the scheme toggle runs under the
  system mode, by reading the resolved contrast rather than the stored
  appearance name
- move the unchecked switch track into the theme token registry so the
  shared Switch keeps its track for consumers of @librechat/client, and
  drop the SPA-only high-contrast overrides it no longer needs

* fix: address fourteenth round of PR review bot findings

chatgpt-codex-connector:
- migrate agent and assistant validation states off raw red utilities and
  onto the destructive text and border roles, so the messages, the field
  outlines and the required markers follow the contrast palettes
- theme the file-source badges with series slots and the status label ink
  instead of fixed blue and yellow fills behind an alpha

* fix: paint the loading canvas from the resolved appearance

The bootstrap in index.html read the stored mode's scheme but not the OS
contrast, so system plus prefers-contrast: more flashed the standard-dark
#0d0d0d behind the pure-black palette for the whole application load. An
unset mode also painted white on a dark OS, though getInitialTheme
resolves it as system. Both branches now resolve the same way the theme
provider does, and a new spec runs the shipped script to prove it.

* fix: theme the markdown renderer failure message

The notice painted when the marked CDN does not load carried an inline
color:#e53e3e, the one colour the appended contrast block could not
reach, leaving it at 4.13:1 on the high-contrast light canvas. It is a
.markdown-error rule now, which the base sheet declares with the current
red and the contrast block overrides with the palette's destructive ink.

* fix: address PR review bot findings

chatgpt-codex-connector:

- Route shortcut, steer, warning, and help-link states through semantic theme roles.
- Add contrast-safe Mermaid Gantt variables for task, status, section, grid, and label states.
- Theme Mermaid artifact render failures with destructive text colors.
- Apply native color-scheme behavior through ThemeProvider and restore host values.

* fix: address follow-up PR review findings

chatgpt-codex-connector:

- Route conflicting shortcuts through the semantic warning border.
- Give checked composer capabilities opaque semantic series borders.
- Move search results and their fade onto the presentation token.
- Restore a visible semantic focus ring on the virtualized results list.

* fix: address third-round review findings

* fix: address fourth-round review findings

* fix: align accessibility theme after dev rebase

* fix: address PR review bot findings

chatgpt-codex-connector:
- Route the steer and queue identities through semantic `status-warning` /
  `status-info` roles in `Chat/Steering/identity.ts`, so the receipt label,
  its pulse dot and every Zap/Clock mark move with the theme instead of
  staying pinned to amber-500/600 and cyan-500. Fixes the high-contrast
  light failures (3.19:1 label text, 1.97:1 and 2.43:1 marks) and the
  default light label, which was also below the 4.5:1 text floor.
- Pair each Monaco theme with the canvas it paints in `ArtifactCodeEditor`,
  so the transparent `@monaco-editor/react` loading view no longer flashes
  the light `surface-code` fill before the `vs-dark` editor mounts outside
  high contrast.

* fix: declare the new dev theme tokens in the high contrast palettes

dev added rgb-text-muted and the two chart widget tokens after this branch
last rebased. The high contrast palettes did not name them, so the widget
canvas and muted text fell back to the standard themes' greys, which the
palette coverage spec caught. Both modes now take the plain canvas, pure
ink and a pure-ink edge, and the contrast tables cover the new tokens.

* style: sort imports in the specs this branch touched

The added imports put ArtifactCodeEditor, ShortcutRecorder and OptionToggle
out of the repo's import order, which the static-checks import-sort gate
rejected.

* fix: address fifth-round review findings

chatgpt-codex-connector:
- Preserve the pressed treatment on a disabled OptionToggle, so a tool made
  programmatic-only keeps showing the Intent state aria-pressed still reports.
- Expose high contrast from the auth appearance control: the scheme toggle
  preserves contrast but never introduces it, so a logged-out user had no way
  to reach the palette. Adds a contrast toggle beside it, and the scheme toggle
  now shows the scheme rather than duplicating the contrast glyph.
- Resolve system contrast from prefers-contrast: custom and forced-colors:
  active as well as more, since a Windows Contrast Theme never reports more.
  Mirrored in the index.html loading bootstrap and the theme README.
- Route the remaining raw palette utilities through theme roles: agent success
  checks (Action, AdvancedPanel, ToolSection) to status-success, the favorited
  tool star to series-4, the orchestration Beta pill to brand-purple, the
  temporary-composer accent to series-6, and the pending quote marks to
  status-info.
- Override the legacy btn-neutral chrome in the contrast block, whose
  hard-coded edge and hover fill sat at 1.1:1 and 2.09:1 on the new canvases.

Rebased onto dev.

* fix: mark the selected tab and theme placeholder ink in contrast modes

Two states rendered invisibly under the high contrast palettes, both verified
in a browser before and after:

- Radix tab triggers mark selection with `surface-tertiary`, which is the plain
  canvas here, so the selected Settings section computed to the same
  rgb(0 0 0) fill as every unselected sibling. Give it the same inset perimeter
  `.select-item` already uses for its navigated option.
- Tailwind preflight paints `::placeholder` with the raw gray-400 #999696, and
  a field that omits a `placeholder:` utility inherits it: the Custom
  Instructions textarea measured 4.06:1 on the black canvas and 3.08:1 on the
  white one, both under the 1.4.3 floor for what is ordinary text. Route it
  through `text-secondary`, which is pure ink in both palettes.

* fix: mark selection with a perimeter and lighten the contrast selected fills

The selected fill was carrying WCAG 1.4.11 on its own, which forced it dark
enough (#8c8c8c light, #5c5c5c dark) that its own label capped at 6.25:1 and
6.69:1 — under the AAA these modes exist for, and visibly heavy on the light
canvas. The `.select-item` rule already showed the better answer: mark the
state with a `text-primary` perimeter at 21:1 and let the fill stay light.

Generalise that rule to the two families it missed — every other Ariakit menu,
whose navigated row carried only a `surface-hover` fill at 1.48:1 on white, and
any row painted with the selected-surface role — then lighten the fills behind
it: #e3e3e3 / #d4d4d4 light and #2b2b2b / #3d3d3d dark, all four AAA under
their label. `-alt` meets `surface-hover`, as it does in the standard palettes.
Inline `span` is excluded so a highlighted citation passage is not boxed per
wrapped fragment.

The palette contract moves with it: `activeFills` is now held to an AAA label
rather than to 3:1 against the canvas, which the perimeter discharges.

* fix: address sixth-round review findings

chatgpt-codex-connector:
- Ship the selected-state contrast with the package. The lightened
  `surface-active` fills only work because a perimeter marks the selection, and
  that perimeter lived in the SPA stylesheet, which `tsdown` never publishes —
  so `TimePicker`, `SelectDropDown`, `MultiSelect`, `InputWithDropDown`, `Badge`
  and `DataTable` gave an external consumer a 1.28:1 fill and nothing else. The
  perimeters and the placeholder ink now live in
  `packages/client/src/theme/highContrast.css`, imported by `ThemeProvider`;
  `style.css` keeps only what is genuinely SPA-only. The Ariakit selector also
  widened from four menu roles to `[data-active-item]`, and picked up
  `[data-state='selected']` for package data-table rows.
- Give Monaco selections a contrast-safe highlight: a focused selection now
  inverts the canvas (`text-primary` on `surface-primary`), matching
  `::selection`, because Monaco draws no perimeter. Weaker emphases share
  `surface-hover-alt` behind the ink find-match border.
- Populate Mermaid's separate `git0`-`git7` branch palette from the series ramp.
  Eight slots against a seven-slot ramp, so the last takes the ink rather than
  repeating branch one; labels take the canvas, since the ramp sits on its far
  side.
- Debounce the two appearance controls separately. They are independent
  settings, so light -> high-contrast dark is one flip of each, and the shared
  500ms window swallowed the second click.
- Route sharing principal badges (USER/GROUP/ROLE) through series-1/7/6.
- Apply the selected contrast mode to Office artifacts. The backend document
  picks its palette from `prefers-color-scheme` alone, so an explicit mode
  inverted whenever the OS disagreed. `withOfficeContrast` re-declares its
  `:root` tokens and pins `color-scheme` from inside the head.

* fix: address seventh-round review findings

chatgpt-codex-connector:
- Restore the citation highlight. The perimeter rule skips `span` so a wrapped
  passage is not boxed per fragment, which left the inline highlight on the
  lightened fill alone. It inverts now instead — the same treatment the mode
  gives `::selection`, 21:1 and with no geometry to wrap.
- Underline non-prose links. The 1.4.1 rule only reached `.markdown` and
  `.prose`, so the auth resend actions and in-dialog help links were colour-only
  against ink that is under 3:1 from them. It now also covers the shared
  `text-link` role, including the ones rendered as a `button`, and moved to the
  package stylesheet since that role is shared. MCP server descriptions style
  their anchors through an `[&_a]` variant rather than a class of their own, so
  they get the underline at the call site — running text, so it applies in every
  theme, not just the contrast ones.
- Route the selected tool card through `status-success` instead of
  `emerald-500/60` over a 6% fill, which composited to about 1.7:1 and left a
  selected card with a weaker edge than an unselected one.

* fix: keep helper hints opaque in the contrast modes

chatgpt-codex-connector: the `opacity-40` default-value hints composite pure
ink to about #999 on the white canvas (2.85:1) and #666 on the black one
(3.66:1), so small active labels missed even AA under a palette that contracts
for AAA. They take `high-contrast:opacity-100` now — the variant this branch
already added and `MessageNav` already uses — rather than a new token: every
text role is the same pure ink in these palettes, so there is no muted colour
to fall back to and the hierarchy is carried by size and the parentheses.

The mobile artifact drag handle gets the same treatment: it is a control, so
1.4.11 applies, and a 40% ink bar does not clear 3:1. The two remaining
`opacity-40` sites are left alone deliberately — `AssistantTool` dims a
disabled row, which WCAG exempts, and the `MarketplaceCatalog` empty-state
glyph is `aria-hidden` decoration.

* style: sort imports in the two parameter controls this branch touched

* fix: soften the temporary-chat accent outside the contrast modes

Moving the accent to `series-6` swapped a 60%-alpha `violet-800` edge for a
full-strength one, and series-6 is a saturated #7e23cd on the light palette and
#ab68fe on the dark one — the composer read as a warning rather than a quiet
mode hint. Half alpha in the standard palettes, opaque only under
`high-contrast:`, which is the one place the edge has to clear the 3:1
non-text floor.
2026-09-05 22:20:33 +02:00
Marco Beretta
1c115cdebf
🐛 fix: improve sidebar title clarity and hover scrolling (#15644)
* fix: improve sidebar title clarity and hover scrolling

* fix: address PR review bot findings

Copilot:
- Drive the title fade from CSS so autoprefixer emits -webkit-mask-size for
  WebKit; Web Animations silently drops prefixed keyframe properties.
- Guard MediaQueryList subscription so legacy addListener/removeListener
  engines neither miss the change handler nor throw during cleanup.
2026-09-05 19:27:54 +02:00
Danny Avila
a9da01fbe7
📐 fix: Keep the Live Slot Under a Phase Card One Row Tall (#15639)
Some checks are pending
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* ♻️ refactor: Share the Tool Row Box Across Call Cards

Eight call cards carried the same literal for their header row's box —
`relative my-1.5 flex h-5 shrink-0 items-center gap-2.5` — one per card kind.
It is one design decision (a 20px line with 6px above and below), and the
next commit needs a ninth element to match it exactly, so it becomes
`TOOL_ROW_CLASSES` in `rows.ts`. No rendered output changes.

* 📐 fix: Keep the Live Slot Under a Phase Card One Row Tall

While a run streams, the slot beneath a collapsed synthesized phase card
alternates between two things: the streaming cursor, rendered the moment a
label fills and its row folds into the card, and the next call's row, rendered
the moment that call starts. Replaying a real export through `ContentParts`
step by step showed those two boxes differ: the cursor sat in a bare
`Container` (20px, no margins) while every call row is `my-1.5 h-5` (32px).
Everything beneath the card therefore stepped up on each absorb and back down
on each call — measured at 6px in Chromium with the app's generated CSS, and
larger in the live column where the row's bottom margin cannot collapse.

The cursor now takes `TOOL_ROW_CLASSES`, the exact box of the row it stands
in for, so the swap is height-neutral; a 2px inset centers the 12px dot on
the 16px glyph rail. The test asserts the cursor carries every token of the
row box, and fails against the previous container.
2026-09-05 08:47:19 -04:00
Marco Beretta
f4e967bf22
🩹 fix: Restore the Client Build by Parsing Single-Dollar Math via Micromark (#15638)
`markdownToHtml` (#15087) imported `preprocessLaTeX` from `client/src/utils/latex.ts`,
but #15181 replaced that preprocessing pass with the currency-safe `singleDollarMath`
micromark construct and removed the function. The symbol exists nowhere in the repo, so
`dev` fails `tsc --noEmit` with TS2305 and the Vite build with
`[MISSING_EXPORT] "preprocessLaTeX" is not exported by "src/utils/latex.ts"`, which reds
out the client typecheck, the build verification, and every job downstream of the client
build on `dev` and on every open pull request.

Register `singleDollarMath` in the extension list when `mode.latex` is set instead. That
is exactly what the message renderers do — `markdownConfig.ts` adds `remarkSingleDollarMath`
only when `latexParsing` is on, and that plugin does nothing but push `singleDollarMath`
into `micromarkExtensions` — so clipboard HTML keeps mirroring the rendered message, which
is the parity this helper is built around. `markdownToHtml` parses with `fromMarkdown`
directly rather than through unified, so the extension array is the equivalent seam.

Both specs already pinned the intended behavior and pass unchanged: `$E=mc^2$` becomes
inline math under `latex: true`, `$5 to $10` stays currency. Their titles said
"preprocesses", which no longer names a mechanism that exists; they now say "parses".
2026-09-05 14:16:17 +02:00
Marco Beretta
f174da6159
📋 feat: Copy Messages as Rich Text (#15087)
* feat: copy messages as rich text

Adds an opt-in setting that places a rendered HTML flavor on the
clipboard next to the markdown, so pasting an assistant response into an
app that does not understand Markdown (Teams, Outlook, Word) keeps its
headings, lists, tables and code formatting.

The HTML is serialized from the same mdast pipeline the message renderer
already uses, with inline styles because those paste targets strip
stylesheets. Raw HTML in a message is escaped rather than passed
through, matching the renderer, and link and image URLs go through the
existing isSafeUrl allowlist.

The plain copies (share links, API keys, OAuth callback URLs) keep using
the base hook, so they are never wrapped in markup. The setting is off by
default: markdown-aware targets prefer the HTML flavor when it is
present, so making it unconditional would regress them.

Closes #13973

* fix: address review feedback on rich text clipboard copy

Mirror the renderer's `singleDollarTextMath: false` so paired currency
such as "$5 to $10" is not read as math and stripped of its dollar
signs in the HTML flavor.

Pair every background the serializer sets with its own foreground, and
let blockquotes and table cells inherit the destination's colors. The
paste target's theme is unknowable, so a lone light background could
land under a dark editor's light text.

Resolve reference-style links and images against their definitions
rather than dropping the hyperlink.

Skip the HTML flavor for user messages while `enableUserMsgMarkdown` is
off. Those render as literal text on screen, so formatting them on paste
would not match what was copied.

* fix: mirror the message renderers more closely in rich text copies

Apply the same `remarkApproxTilde` and `remark-supersub` transforms the
renderers apply to the parsed tree, so `x^2^` arrives as a superscript
instead of literal markers.

Select the renderer to mirror per message: `Markdown` (assistant) enables
directives, `MarkdownLite` (user) does not, so a user message showing
literal `:::` markers keeps them in the HTML flavor.

Resolve path-relative URLs against the app's base URI. A generated file
or image link resolves against LibreChat on screen but against the
destination document once pasted. Absolute URLs are passed through
verbatim rather than normalized.

Serialize each text content part as its own document. Each part renders
through its own Markdown instance on screen, so a construct must not span
two of them; a private-use sentinel carries the boundary through citation
processing, which only touches its own markers.

* fix: match the renderer on line breaks, relative URLs and LaTeX

Message text renders under white-space: pre-wrap, so a soft line break is
a visible line on screen. Emit those as <br /> instead of a raw newline,
which every HTML paste target would collapse to a space.

Screen URLs through react-markdown's own transform rather than the
artifact template's isSafeUrl, which rejected bare relative targets like
docs/guide.html and dropped links the conversation shows as working.
Everything relative is then resolved against the app's base URI.

Apply preprocessLaTeX to assistant segments when LaTeXParsing is on, as
the assistant renderer does, so single-dollar math is read as math rather
than left as literal dollar signs.

* fix: align rich text copies with directives, footnotes and search rendering

Show an inline directive by name, as the artifact plugin displays it,
rather than dropping the marker and keeping only its children.

Number footnotes by the order their references appear and omit a
definition nothing references, matching what the renderer displays
instead of emitting the source label.

Keep a generated citation marker from resolving against a reference
definition that happens to share its number, which linked the citation to
the wrong URL. An unresolved reference now falls back to its own source
form, as remark-rehype reverts one.

Let a caller state the renderer its row was displayed with. SearchContent
falls back to MarkdownLite for a message without content parts whatever
its author, so search results and shared links pass that through.

Replace the part-boundary sentinel with a citation processor that carries
its numbering across parts, so each part is formatted on its own without
a private-use character that message text could itself contain.

* fix: close remaining gaps between rich text copies and the rendered message

Route rooted /images/ sources through apiBaseUrl the way the renderer's
image component does, so a subdirectory deployment's images survive the
paste instead of losing the base path to the leading slash.

Show an artifact directive by its title, which is what its button
displays, rather than pasting the implementation the conversation hides.

Gather footnote definitions into a footer ordered by first reference,
matching where and in what order the renderer shows them.

Keep a loose list's paragraph wrappers. mdast records looseness on the
list, not on every item, so the parent decides whether an item is tight.

Emit the citation footer as its own segment. Appended to a segment ending
in an unclosed fence, the sources were swallowed into the code block.

Pass the search results MessageParts already loads into useMessageHelpers,
which was copying assistant responses with their citation markers stripped
and no sources appended.

* fix: transform MCP markers and default an untitled artifact

Run mcpUIResourcePlugin over assistant segments as the full renderer
does, so a UI resource marker leaves as nothing rather than pasting the
protocol text. An embedded interactive resource has no static form, and
MarkdownLite does not transform the markers, so user turns keep them
literal exactly as they are displayed.

Fall back to the Artifact component's own default title when a directive
carries none, instead of dropping the artifact from the copy.

* fix: honor definition precedence, file link rewrites and error rows

Keep the first of two reference definitions sharing a label, as
CommonMark does, instead of letting the last one win.

Route a user's own generated-file links through the app's file endpoint
the way the anchor component does, so a pasted link points where the one
in the conversation points rather than at the provider URL.

Skip the HTML flavor for an errored row. ErrorMessage renders those, so
serializing the raw payload as markdown would paste something the
conversation never showed.

Scope reserved citation labels to the segment that generated them. Each
part is its own document, so a legitimate reference in one part is no
longer suppressed by a citation numbered the same in another.

* fix: resolve file citations against the references collection

File-search sources live under references, as useCitation reads them,
but the clipboard's type map had no entry for file. Their markers were
therefore stripped without a source, so a citation visible in the
response went missing from the copy.
2026-09-05 13:36:07 +02:00
Danny Avila
6733257391
🧮 fix: Scale Code Environment Reconciliation (#15625)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* fix: Scale code environment reconciliation

* fix: Bound reservation cleanup index scan

* fix: Bound reservation expiry index scans

* fix: Make reservation cleanup compatible with rolling upgrades

* fix: Fence cleanup sweeps against continuous inserts
2026-09-04 22:27:39 -04:00
Ravi Kumar L
000349681e
🩻 feat(insights): Scope Insights to Authorized Agents (#15549)
Some checks failed
Docker Dev Images Build / publish (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
* feat(insights): add agent-scoped access

* fix(insights): exclude unattributed assistant messages

* fix(sharing): key role insight toggles by principal

* fix(sharing): preserve insight grant snapshot

* fix(permissions): preserve insight bits atomically

* fix(permissions): guard insight audit rollback

* fix(insights): keep admin grant controls accessible

* fix(permissions): reconcile duplicate principal updates

* fix(insights): show automatic admin access

* fix(permissions): harden insights access updates

* fix(insights): simplify authorization and preserve client state
2026-09-04 20:16:58 -04:00
Danny Avila
57c52de33c
🛎️ fix: Start Code Environment Reconciliation After Mongo Connects (#15620) 2026-09-04 19:08:44 -04:00
Danny Avila
3d085b227d
🛰️ feat: Show BYOM Worker Readiness (#15618)
* feat: Show BYOM worker readiness

* test: Exercise approved BYOM commands end to end

* test: Wait for BYOM worker readiness
2026-09-04 19:02:35 -04:00
Danny Avila
a3f80e8a92
🗓️ fix: Classify Scheduled Run Duplicate Conflicts on DocumentDB (#15622) 2026-09-04 19:01:50 -04:00
Danny Avila
d9dfdeb643
💫 style: Align the Phase Summary Rail and Simplify its Fold (#15619)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 💫 style: Align the Phase Summary Rail and Simplify its Fold

A phase summary was the only row in a transcript with no icon rail, so its
text sat at 13px while the tool rows it stood for sat at 24px and the rows it
swallowed moved out to 37px behind the card's `px-3` — three left edges in one
block, with every folded row stepping 13px sideways as the box materialized.

- The header takes the same 16px glyph slot every tool row has (`Check`, or
  `TriangleAlert` when the phase failed), which lands its text on the one rail.
- The card chrome is gone: border, background, radius, body padding and
  divider. The summary is a row among rows, so nothing moves horizontally when
  it forms, and the entrance is the two grid rows trading places rather than
  four properties resolving at once.
- The label is a ticker. A synthesized card re-titles itself every time it
  absorbs another finished block, and swapping that text instantly is what made
  the absorbed row look like it simply vanished; the retired summary now rises
  out of the clipped row while the new one comes up from below.
- The label uses `tool-status-text`, so a summary scales with the reader's
  font-size setting like the rows around it instead of sitting at a fixed 14px.

`ToolCallGroup`'s category glyph was a fourth rail at 28px (`h-5 w-5`); it now
matches `ToolIcon` at `size-4`.

`ease-[cubic-bezier(0.16,1,0.3,1)]` emitted nothing: `tailwindcss-animate`
registers its own `ease` utility for `animation-timing-function` alongside
Tailwind's `transition-timing-function` one, an arbitrary value matches both,
and Tailwind resolves that ambiguity by dropping the class. The curve had never
reached the chrome — the two-easings problem the comment warns about, caused by
its own fix. It is written as an arbitrary property now, and it was the only
such usage in the repo.

* 🩹 fix: Settle the Phase Label in One Commit and Drop a Duplicate Media Query

Review follow-ups on the phase ticker.

- `useSmoothStreaming` already resolves to `smoothStreaming && !reducedMotion`;
  subscribing to the same media query again installed one `matchMedia` listener
  per phase card without changing the answer.
- The label copied `text` into state from a passive effect, so a swap with no
  animation could paint the previous summary for a frame while the button's
  `aria-label` already carried the new one. It is adjusted during render now,
  which settles both paths in the same commit.
- The incoming line kept its `animate-in` class only while the retired line
  existed, so clearing that line on its `animationend` could strip the class
  from a slide still in flight. The class now rides its own flag.
- The test's label factory no longer needs `as unknown as`: every field on the
  ACTIVITY_LABEL part but `type` is optional, and the double cast was only
  there because a computed key widens to `string`.

* ♿ fix: State the Phase Header's Inset Focus Ring Locally

The header sits inside a permanent `overflow-hidden` wrapper — the 0fr/1fr
grid needs it — so its focus ring has to be inset or it is drawn outside the
border box and clipped away. That holds today only because the shared ghost
variant supplies `ring-inset`; a change in `packages/client` could remove the
indicator from here with no signal. It is declared on the element now, with a
test that asserts both the clip and the inset ring.
2026-09-04 17:44:23 -04:00
Danny Avila
5e3cdd0b2c
📮 fix: Consume an Invite Only Once the Account Exists (#15611)
* 📮 fix: Consume an Invite Only Once the Account Exists

`checkInviteUser` deleted the invite token and then called `next()`, but everything
that can still reject a registration runs after it: the schema, the allowed-domain
check, and the email-already-in-use check. A mistyped password confirmation therefore
destroyed the invite — the invitee corrected it, resubmitted, and got "Invalid invite
token" with no way back short of an admin re-inviting them.

The deletion moves to `registrationController`, after `registerUser` reports success.

That report needed a new signal. `registerUser` returns the same 200 and the same
generic message whether it created an account or found the email already in use —
deliberately, so the response cannot be used to enumerate accounts — so the status
alone cannot say whether an account exists. It now also returns `userCreated` on the
creation path, which the controller reads and never forwards; the response body is
unchanged.

A failed deletion is logged rather than surfaced. By that point the account exists,
and leaving a usable invite behind is recoverable in a way that telling the user their
registration failed is not.

Fixes #15541

* ♻️ test: Fold the Invite Tests Into the Existing AuthController Spec

From Copilot's review. The new spec reached for `jest.requireActual` on
`@librechat/data-schemas`, `~/server/services/AuthService` and `~/models`, which pulls
real implementations into a unit test — `~/models` builds the data-schemas methods
against mongoose — and diverges from `AuthController.spec.js`, which stubs each module
outright.

Rather than restate that harness with narrower stubs, the tests move into the spec that
already has it. `deleteTokens` joins its `~/models` mock, which also keeps that shared
mock in step with the controller's imports: a stale mock there hands the controller an
undefined function, and the omission only surfaces when some later test happens to
exercise the path.
2026-09-04 15:38:32 -04:00