KatexRenderer rendered the raw math source through {@html} whenever renderToString threw. throwOnError only suppresses KaTeX ParseError, so a RangeError (maximum call stack size exceeded, reachable with deeply-nested brace input) escaped into the catch and re-exposed the unescaped source. Because the math tokenizer captures everything between the delimiters verbatim, that source can carry an HTML/JS payload which then executed in the viewer's browser on the application origin, a stored, cross-user XSS reachable through normal chat/channel/shared-chat rendering. Escape the fallback so the source is shown as text and is never injected as HTML. Valid math is unaffected, it still renders through the success path.
Co-authored-by: maxntv <maxntv@users.noreply.github.com>
Adds a multiselect input type for Valves and UserValves so plugin authors can let users pick multiple values from static or runtime-resolved options instead of maintaining comma-separated text fields with hardcoded allowed-value lists in the description.
ENABLED_ITEMS: list[str] = Field(
default=["foo"],
json_schema_extra={"input": {"type": "multiselect", "options": "get_item_options"}},
)
@classmethod
def get_item_options(cls):
return [{"value": "foo", "label": "Foo"}, {"value": "bar", "label": "Bar"}]
Options accept the same shapes as the existing select input: either a static list (strings or {value, label} dicts) or a classmethod name resolved at request time (including __user__ context for UserValves). No backend changes are needed because resolve_valves_schema_options already resolves options independently of the input type.
The new MultiSelect component follows the existing Select portal dropdown pattern and renders checkbox rows that stay open while toggling, with the selected labels shown in the trigger. Values bind as a real string array end to end: the array-to-comma-string conversions in the chat controls valves panel and the valves modal are skipped for multiselect fields, so the stored valve is a native list[str] validated by Pydantic.
Requested in #26848.
* chore: drop redundant background repaints so surfaces inherit their parent
Four spots repaint the exact color their parent surface already provides
(bg-white / dark:bg-gray-900 rows inside same-colored pages and modals,
and the selectClass dark repaint inside the connection modals — the
sibling input const is already fully transparent). Visually identical in
stock light and dark; removing them lets instance theming show through
instead of leaving opaque boxes:
- .tiptap tr (app.css) — table rows in notes/editors
- Edit User Group Users tab body rows (common Modal surface)
- AddToolServerModal + AddTerminalServerModal selectClass dark repaint
The matching repaints inside the ModelUsage/UserUsage components are
not part of this change — those files were dead code and were removed
entirely in #27574.
* chore: catch remaining redundant surface repaints missed in the first pass
Same rule as the previous commit — every one of these repaints the exact
color its parent surface already provides, so removal is stock-identical
in light and dark while letting instance theming show through:
- Analytics Dashboard's inline Model Usage / User Activity row markup
(the Analytics tab renders these tables from Dashboard.svelte itself;
the unreferenced ModelUsage/UserUsage component files were removed
in #27574)
- Evaluations Feedbacks + Leaderboard body rows (settings modal surface)
- admin UserList body rows (app page surface)
- chat markdown tables (MarkdownTokens): thead and body rows — unlike
the tiptap header (gray-850 contrast, untouched), this thead painted
the page's own color
- CitationsModal source rows (common Modal surface)
- AddConnectionModal selectClass dark repaint — third copy of the same
const already fixed in AddToolServerModal / AddTerminalServerModal
* feat: add default upload mode setting
Add user setting to configure the default upload mode for files, allowing users to choose between "Using Entire Document" (full context) and "Using Focused Retrieval" (RAG processing) as the default behavior.
* i18n: sync locale catalogs for the new upload mode strings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: re-trigger CI (previous run hit the pre-existing Node heap OOM, see #27254)
* fix: apply the default upload mode at upload time so the payload carries it
The previous approach only pre-set the modal toggle's visual state on
mount; item.context is written solely by the Switch's on:change, so the
sent files kept context: undefined and the backend never saw 'full'. It
also showed a misleading ON state for legacy context-less files, since
FileItemModal mounts with every FileItem chip render.
Stamp context on the fileItem in uploadFileHandler instead (before
...itemData, so callers passing an explicit context still win) and
revert the FileItemModal hunk — the modal already renders from
item.context alone.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
* fix: show download preparation toast and prevent duplicate zip jobs
Downloading a file or folder from the file navigator gave no feedback while the server prepared the response, which can take 30 seconds or more for large folders that are zipped server-side. Users assumed the click did nothing and pressed Download again, starting additional zip jobs on the server. Both downloadFile and bulkDownload, the two functions every download control funnels through, now show a persistent "Preparing download..." loading toast while the request is in flight and dismiss it once the download starts or fails. A shared downloading flag makes repeated clicks no-ops until the current download finishes, so a single click starts exactly one server-side job. Fixes#27055
* fix: report terminal download failures instead of dismissing the toast
A failed download dismissed the preparation toast without saying anything, which reads as the download silently disappearing. Both download paths now report the failure. The two helpers also declare a nullable return but could still reject once the response body started streaming, so an interrupted transfer escaped as an unhandled rejection and left the same silent dismissal. They now return null in that case, which also stops an interrupted preview from leaving its spinner running.
When the same model is selected multiple times in a side-by-side chat,
each response is created with a distinct modelIdx (0,1,2,3) that
identifies its column. The backend now owns message persistence, but it
built the assistant placeholders without modelIdx, so the field was
never saved. On reload MultiResponseMessages groups responses by
modelIdx and falls back to grouping by model id when modelIdx is
missing; with duplicate models that fallback lumps every response into
each column, so all columns render the first response (and show a bogus
'1/N' pager).
Send modelIdx with each message_ids entry from the frontend and persist
it on the assistant placeholders in both the new-chat and existing-chat
paths. The message_ids list is now forwarded for every send (not just
multi-model ones) so single-column regenerations in a duplicate-model
chat also keep their column identity across reloads.
When navigating from a chat to a non-chat route (e.g. the admin panel),
the previously-viewed chat stayed selected in the sidebar and
deleting/archiving it wrongly redirected back to the new-chat page.
Cloning a chat also left the source chat highlighted alongside the new
clone, so two chats appeared selected at once.
Two independent sources kept the stale selection:
- The chatId store was never cleared when the Chat component unmounted,
so $chatId still pointed at the last-viewed chat (this drove the
delete/archive redirect). Clear chatId/chatTitle in Chat's onDestroy.
- The sidebar's optimistic selectedChatId highlight, set on click, was
only cleared on window blur (hence it appeared to fix itself after a
tab switch) and never followed programmatic navigation. Bind it to the
chatId store so it tracks the active chat for leave, delete and clone.
The VoiceRecording component stays mounted (hidden) between recordings, and the web speech engine accumulated every session's transcript into the never-reset transcription variable. Each new recording therefore confirmed all previous utterances again, so the inserted text repeated once per session and previously deleted text reappeared in the input.
Additionally, cancelling a recording (X button, Escape or a recognition error) called stopRecording(), which stops the SpeechRecognition instance and fires its onend handler, which unconditionally confirms the transcription. Cancelled recordings therefore still inserted the accumulated transcript.
Reset the transcription at the start of each web speech session and detach the onend handler on cancel so cancelled recordings no longer confirm.
Fixes#26784
Since v0.10.0 chat messages are virtualized with content-visibility: auto to skip rendering of off-screen messages. Safari's implementation of content-visibility has known paint bugs (WebKit bugs 277573, 281570 and 283846) that can leave the contents of a message unpainted even when it is on screen. On iOS this makes assistant responses render as empty, both in Safari and as a PWA, while the same chats render fine in Chromium and Firefox. This matches the regression window reported in #26712, which appeared when upgrading from 0.9.6 to 0.10.2.
Detect Safari (including all iOS browsers, which use WebKit) with the same user agent check already used in MessageInput and ShareChatModal, and skip the virtualization class there. Safari falls back to rendering all messages like before v0.10.0, while other engines keep the optimization. Verified with a spoofed Safari user agent that messages render without the virtualization class and with content-visibility resolving to visible, while Chromium keeps content-visibility: auto.
Fixes#26712
Assistant responses are now stored as structured output items on message.output, with message.content left empty. The action payload built in chatActionHandler still sent message.content only, so action functions received assistant messages with an empty content property. Derive the content from the structured output via getOutputText, falling back to message.content, matching how the rest of Chat.svelte resolves assistant text.
Fixes#26672