From adcc50d3370301afd5561e0f58ff6f3ab3750818 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Mar 2026 15:56:43 -0500 Subject: [PATCH 001/734] refac --- backend/open_webui/utils/middleware.py | 38 +++++++++++++++----------- src/lib/components/chat/Chat.svelte | 1 + 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index ae1b557da75..3ff6f551e91 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2146,24 +2146,30 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Check if the request has chat_id and is inside of a folder # Uses lightweight column query — only fetches folder_id, not the full chat JSON blob chat_id = metadata.get('chat_id', None) + folder_id = None if chat_id and user: folder_id = Chats.get_chat_folder_id(chat_id, user.id) - if folder_id: - folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id) - - if folder and folder.data: - if 'system_prompt' in folder.data: - form_data = apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user) - if 'files' in folder.data: - if metadata.get('params', {}).get('function_calling') != 'native': - form_data['files'] = [ - *folder.data['files'], - *form_data.get('files', []), - ] - else: - # Native FC: skip RAG injection, builtin tools - # will read folder knowledge from metadata. - metadata['folder_knowledge'] = folder.data['files'] + + # Fallback: use folder_id from metadata (temporary chats have no DB record) + if not folder_id: + folder_id = metadata.get('folder_id', None) + + if folder_id and user: + folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id) + + if folder and folder.data: + if 'system_prompt' in folder.data: + form_data = apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user) + if 'files' in folder.data: + if metadata.get('params', {}).get('function_calling') != 'native': + form_data['files'] = [ + *folder.data['files'], + *form_data.get('files', []), + ] + else: + # Native FC: skip RAG injection, builtin tools + # will read folder knowledge from metadata. + metadata['folder_knowledge'] = folder.data['files'] # Model "Knowledge" handling user_message = get_last_user_message(form_data['messages']) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 41b25606a54..97cb481c510 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2221,6 +2221,7 @@ session_id: $socket?.id, chat_id: $chatId, + folder_id: $selectedFolder?.id ?? undefined, id: responseMessageId, parent_id: userMessage?.id ?? null, From 694fb3776f9c99f668546c07bd85e3d5065edfdc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Mar 2026 17:56:05 -0500 Subject: [PATCH 002/734] refac --- src/lib/utils/connections.ts | 124 +++++++++++++++++++++++++++++++++++ src/routes/+layout.svelte | 50 ++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 src/lib/utils/connections.ts diff --git a/src/lib/utils/connections.ts b/src/lib/utils/connections.ts new file mode 100644 index 00000000000..7c7bc0408dd --- /dev/null +++ b/src/lib/utils/connections.ts @@ -0,0 +1,124 @@ +/** + * Shared helpers for managing system-level connections. + * Used by both the admin settings UI and the desktop event handler + * to ensure consistent add/remove logic. + */ + +import { getOpenAIConfig, updateOpenAIConfig } from '$lib/apis/openai'; +import { + getTerminalServerConnections, + setTerminalServerConnections +} from '$lib/apis/configs'; + +// ─── OpenAI Connections ───────────────────────────────── + +/** + * Add an OpenAI-compatible API connection at the system level. + * Mirrors the logic in admin/Settings/Connections.svelte. + */ +export const addOpenAIConnection = async ( + token: string, + connection: { url: string; key?: string; config?: object } +) => { + const current = await getOpenAIConfig(token); + const urls = current?.OPENAI_API_BASE_URLS ?? []; + const keys = current?.OPENAI_API_KEYS ?? []; + const configs = current?.OPENAI_API_CONFIGS ?? {}; + + const normalizedUrl = connection.url.replace(/\/$/, ''); + + // Don't add duplicates + if (urls.map((u: string) => u.replace(/\/$/, '')).includes(normalizedUrl)) { + return current; + } + + urls.push(normalizedUrl); + keys.push(connection.key ?? ''); + if (connection.config) { + configs[(urls.length - 1).toString()] = connection.config; + } + + return await updateOpenAIConfig(token, { + ENABLE_OPENAI_API: current?.ENABLE_OPENAI_API ?? true, + OPENAI_API_BASE_URLS: urls, + OPENAI_API_KEYS: keys, + OPENAI_API_CONFIGS: configs + }); +}; + +/** + * Remove an OpenAI-compatible API connection by URL at the system level. + * Re-indexes OPENAI_API_CONFIGS to match the admin delete pattern. + */ +export const removeOpenAIConnection = async (token: string, url: string) => { + const current = await getOpenAIConfig(token); + const urls: string[] = current?.OPENAI_API_BASE_URLS ?? []; + const keys: string[] = current?.OPENAI_API_KEYS ?? []; + const configs: Record = current?.OPENAI_API_CONFIGS ?? {}; + + const normalizedUrl = url.replace(/\/$/, ''); + const idx = urls.findIndex((u: string) => u.replace(/\/$/, '') === normalizedUrl); + if (idx === -1) return current; + + const newUrls = urls.filter((_: string, i: number) => i !== idx); + const newKeys = keys.filter((_: string, i: number) => i !== idx); + + // Re-index configs (mirrors admin/Settings/Connections.svelte onDelete) + const newConfigs: Record = {}; + newUrls.forEach((_: string, newIdx: number) => { + newConfigs[newIdx] = configs[newIdx < idx ? newIdx : newIdx + 1]; + }); + + return await updateOpenAIConfig(token, { + ENABLE_OPENAI_API: current?.ENABLE_OPENAI_API ?? true, + OPENAI_API_BASE_URLS: newUrls, + OPENAI_API_KEYS: newKeys, + OPENAI_API_CONFIGS: newConfigs + }); +}; + +// ─── Terminal Server Connections ──────────────────────── + +/** + * Add a terminal server connection at the system level. + * Mirrors the logic in admin/Settings/Integrations.svelte. + */ +export const addTerminalConnection = async ( + token: string, + connection: { url: string; key?: string; name?: string; auth_type?: string } +) => { + const current = await getTerminalServerConnections(token); + const servers = current?.TERMINAL_SERVER_CONNECTIONS ?? []; + + // Don't add duplicates + if (servers.find((s: any) => s.url === connection.url)) { + return current; + } + + servers.push({ + url: connection.url, + key: connection.key ?? '', + auth_type: connection.auth_type ?? 'bearer', + name: connection.name ?? 'Open Terminal', + enabled: true + }); + + return await setTerminalServerConnections(token, { + TERMINAL_SERVER_CONNECTIONS: servers + }); +}; + +/** + * Remove a terminal server connection by URL at the system level. + */ +export const removeTerminalConnection = async (token: string, url: string) => { + const current = await getTerminalServerConnections(token); + const servers = current?.TERMINAL_SERVER_CONNECTIONS ?? []; + + const filtered = servers.filter((s: any) => s.url !== url); + if (filtered.length === servers.length) return current; // nothing to remove + + return await setTerminalServerConnections(token, { + TERMINAL_SERVER_CONNECTIONS: filtered + }); +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 36fd7b22d59..12962cbcbbb 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -53,6 +53,12 @@ import { getSessionUser, userSignOut } from '$lib/apis/auths'; import { getAllTags, getChatList } from '$lib/apis/chats'; import { chatCompletion } from '$lib/apis/openai'; + import { + addOpenAIConnection, + removeOpenAIConnection, + addTerminalConnection, + removeTerminalConnection + } from '$lib/utils/connections'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants'; import { bestMatchingLanguage, displayFileHandler } from '$lib/utils'; @@ -692,6 +698,45 @@ } }; + const desktopEventHandler = async (event) => { + // Events that don't require auth + if (event.type === 'page:reload') { + location.reload(); + return; + } + + const token = localStorage.token; + if (!token) return; + + // Only admins can modify system-level connections + if ($user?.role !== 'admin') return; + + try { + if (event.type === 'connections:terminal') { + if (event.data.action === 'add') { + await addTerminalConnection(token, { + url: event.data.url, + key: event.data.key, + name: 'Local Open Terminal' + }); + } else if (event.data.action === 'remove') { + await removeTerminalConnection(token, event.data.url); + } + } else if (event.type === 'connections:openai') { + if (event.data.action === 'add') { + await addOpenAIConnection(token, { + url: event.data.url, + key: event.data.key + }); + } else if (event.data.action === 'remove') { + await removeOpenAIConnection(token, event.data.url); + } + } + } catch (e) { + console.error('Desktop connection update failed:', e); + } + }; + const windowMessageEventHandler = async (event) => { if ( !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( @@ -767,6 +812,11 @@ appData.set(data); } } + + // Listen for desktop service lifecycle events (scalable protocol) + if (window.electronAPI.onEvent) { + window.electronAPI.onEvent(desktopEventHandler); + } } // Listen for messages on the BroadcastChannel From 9793315e0bbf0d309f71f31eb1501d68dc656637 Mon Sep 17 00:00:00 2001 From: Victor Nieto Date: Thu, 19 Mar 2026 23:57:53 +0100 Subject: [PATCH 003/734] i18n: correct Spanish (es-ES) translation errors (#22870) Co-authored-by: Tim Baek Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> --- src/lib/i18n/locales/es-ES/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/i18n/locales/es-ES/translation.json b/src/lib/i18n/locales/es-ES/translation.json index 4b8b8ac5d63..289d5f38d66 100644 --- a/src/lib/i18n/locales/es-ES/translation.json +++ b/src/lib/i18n/locales/es-ES/translation.json @@ -1670,8 +1670,8 @@ "Select a reranking model engine": "Seleccionar un motor de modelos de reclasificación", "Select a role": "Seleccionar un rol", "Select a theme": "Seleccionar un tema", - "Select a tool": "Seleccioanr una herramienta", - "Select a voice": "Seleccioanr una voz", + "Select a tool": "Seleccionar una herramienta", + "Select a voice": "Seleccionar una voz", "Select an auth method": "Seleccionar un método de autentificación", "Select an embedding model engine": "Seleccionar un motor de modelos de incrustación", "Select an engine": "Seleccionar un motor", From c53cd78dcb601af989185072d29ae4881dc32fd6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Mar 2026 18:06:43 -0500 Subject: [PATCH 004/734] refac --- src/routes/+layout.svelte | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 12962cbcbbb..0c539db0365 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -49,7 +49,7 @@ import '../app.css'; import 'tippy.js/dist/tippy.css'; - import { executeToolServer, getBackendConfig, getVersion } from '$lib/apis'; + import { executeToolServer, getBackendConfig, getModels, getVersion } from '$lib/apis'; import { getSessionUser, userSignOut } from '$lib/apis/auths'; import { getAllTags, getChatList } from '$lib/apis/chats'; import { chatCompletion } from '$lib/apis/openai'; @@ -704,6 +704,24 @@ location.reload(); return; } + if (event.type === 'page:navigate' && event.data?.path) { + await goto(event.data.path); + return; + } + if (event.type === 'models:refresh') { + const token = localStorage.token; + if (token) { + models.set( + await getModels( + token, + $config?.features?.enable_direct_connections + ? ($settings?.directConnections ?? null) + : null + ) + ); + } + return; + } const token = localStorage.token; if (!token) return; From de5e0fbc00e7abcd84e1272c301b0707f8ea5ac6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 20 Mar 2026 15:47:22 -0500 Subject: [PATCH 005/734] refac --- src/lib/i18n/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index d7361a5e798..520f9337a27 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -14,8 +14,11 @@ const createI18nStore = (i18n: i18nType) => { i18nWritable.set(i18n); }); i18n.on('added', () => i18nWritable.set(i18n)); - i18n.on('languageChanged', () => { + i18n.on('languageChanged', (lang) => { i18nWritable.set(i18n); + if (typeof document !== 'undefined') { + document.documentElement.setAttribute('lang', lang); + } }); return i18nWritable; }; @@ -67,9 +70,6 @@ export const initI18n = (defaultLocale?: string | undefined) => { escapeValue: false // not needed for svelte as it escapes by default } }); - - const lang = i18next?.language || defaultLocale || 'en-US'; - document.documentElement.setAttribute('lang', lang); }; const i18n = createI18nStore(i18next); From c81b3ef9ce9e3ea74332e9c6d3ffd5b938957f19 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:47:50 +0100 Subject: [PATCH 006/734] sec (#22897) --- docs/SECURITY.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 415e09f16e8..1e310a9b792 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -76,16 +76,16 @@ Your remediation guidance can include, for example: > > **Using CVE Precedents:** If you cite other CVEs to support your report, ensure they are **genuinely comparable** in vulnerability type, threat model, and attack vector. Citing CVEs from different product categories, different vulnerability classes or different deployment models will lead us to suspect the use of AI in your report. -9. **Admin Actions Are Out of Scope:** Vulnerabilities that require an administrator to actively perform unsafe actions are **not considered valid vulnerabilities**. **Admins have full system control and are expected to understand the security implications of their actions and configurations**. This includes but is not limited to: adding malicious external servers (models, tools, webhooks), pasting untrusted code into Functions/Tools, or intentionally weakening security settings. **Reports requiring admin negligence or social engineering of admins may be rejected.** +9. **Admin Actions Are Out of Scope:** Vulnerabilities that require an administrator to actively perform unsafe actions are **not considered valid vulnerabilities**. **Admins have full system control and are expected to understand the security implications of their actions and configurations**. This includes but is not limited to: adding malicious external servers (models, tools, webhooks, functions), pasting untrusted code into Functions/Tools, or intentionally weakening security settings. **Reports requiring admin negligence or social engineering of admins may be rejected.** > [!NOTE] > Similar to rule "Default Configuration Testing": If you believe you have found a vulnerability that affects admins and is NOT caused by admin negligence or intentionally malicious actions, > **then we absolutely want to hear about it.** This policy is intended to filter social engineering attacks on admins, malicious plugins being deployed by admins and similar malicious actions, not to discourage legitimate security research. -10. **Tools & Functions Code Execution Is Intended Behavior:** Open WebUI's Tools and Functions feature is **designed** to execute user-provided Python code on the server. This is core, intentional functionality — not a vulnerability. Function creation is **restricted to administrators only**. Tool creation is controlled by the `workspace.tools` permission, which is **disabled by default** for non-admin users and should only be granted to fully trusted users who are equivalent to system administrators in terms of trust. Granting a user the ability to create Tools is equivalent to giving them shell access to the server. Reports that describe the expected behavior of `exec()` in the Tools/Functions pipeline as a vulnerability will be closed as **not a vulnerability / intended behavior**. This applies to both direct code execution and frontmatter-based package installation (`pip install`). +10. **Tools & Functions Code Execution Is Intended Behavior:** Open WebUI's Tools and Functions feature is **designed** to execute user-provided Python code on the server. This is core, intentional functionality — not a vulnerability (see also rule 7, [Threat Model Understanding](#threat-model-understanding-required)). Function creation is **restricted to administrators only**. Tool creation is controlled by the `workspace.tools` permission, which is **disabled by default** for non-admin users and should only be granted to fully trusted users who are equivalent to system administrators in terms of trust. **Granting a user the ability to create Tools is equivalent to giving them shell access to the server**. If an administrator grants this permission to untrusted users, this constitutes intentional misconfiguration and is additionally covered by rule 9 ([Admin Actions Are Out of Scope](#admin-actions-are-out-of-scope)). More generally, **reports describing ANY attack chain that involves Tools or Functions — including but not limited to code execution, file access, network requests, or environment variable access — will be closed as not a vulnerability / intended behavior.** This applies to both direct code execution and frontmatter-based package installation (`pip install`). > [!IMPORTANT] -> **For administrators:** Treat the `workspace.tools` permission as **root-equivalent access**. Only grant it to users you would trust with direct access to your server. If you enable this permission for untrusted users, you are accepting the risk of arbitrary code execution on your host. +> **For administrators:** Treat the `workspace.tools` permission as **root-equivalent access**. Only grant it to users you would trust with direct access to your server. If you enable this permission for untrusted users, you are accepting the risk of arbitrary code execution on your host. For more details, see our [Plugin Security documentation](https://docs.openwebui.com/features/extensibility/plugin/). 11. **AI report transparency:** Due to an extreme spike in AI-aided vulnerability reports **you MUST DISCLOSE if AI was used in any capacity** - whether for writing the report, generating the PoC, or identifying the vulnerability. If AI helped you in any way shape or form in the creation of the report, PoC or finding the vulnerability, you MUST disclose it. @@ -112,6 +112,24 @@ Your remediation guidance can include, for example: If you want to report a vulnerability and can meet the outlined requirements, [open a vulnerability report here](https://github.com/open-webui/open-webui/security/advisories/new). If you feel like you are not able to follow ALL outlined requirements for vulnerability-specific reasons, still do report it, we will check every report either way. +## Expected Response Timeframe + +Due to the volume of incoming vulnerability reports, issues, discussions, pull requests, and general project maintenance — lately compounded by a large number of invalid AI-generated reports (see [AI report transparency](#ai-report-transparency)) — our capacity to respond is limited. Open WebUI is a community-driven project maintained by a small team, and security reports are handled alongside all other project responsibilities. + +**Please expect several weeks** for your report to be triaged, investigated, fixed, and published. While we aim to respond to every report as quickly as possible, it is normal to experience periods of silence lasting up to several weeks. **This does not mean your report has been ignored** — it means we have not yet had the capacity to address it. The entire process can realistically take multiple weeks from initial submission to final publication. We appreciate your patience and understanding. + +## Confidential Disclosure + +Vulnerability reports submitted through GitHub Security Advisories are **private and confidential**. Public disclosure of **ANY** details related to a submitted vulnerability report is **STRICTLY PROHIBITED** until the advisory has been **fully published** — not merely when a CVE ID has been assigned, but when the advisory itself is publicly visible. + +This prohibition applies to **all channels**, including but not limited to: + +- Comments on pull requests, issues, or discussions (on GitHub or elsewhere) +- Social media, blogs, forums, or any other website +- Discord, Reddit, or any other platform, website or service + +Premature disclosure undermines the security of all Open WebUI users and **violates the trust** inherent in the responsible disclosure process. **Reporters who publicly disclose vulnerability details before official publication may be permanently banned from future reporting.** + ## Product Security And For Non-Vulnerability Related Security Concerns: If your concern does not meet the vulnerability requirements outlined above, is not a vulnerability, **but is still related to security concerns**, then use the following channels instead: @@ -131,7 +149,7 @@ If your concern does not meet the vulnerability requirements outlined above, is - Feature requests for optional security enhancements (2FA, audit logging, etc.) - General security questions about production deployment -Please use the adequate channel for your specific issue - e.g. best-practice guidance or **dditional documentation needs into the [Documentation Repository](https://github.com/open-webui/docs)**, and **feature requests into the Main Repository as an issue or discussion**. +Please use the adequate channel for your specific issue - e.g. best-practice guidance or additional documentation needs into the [Documentation Repository](https://github.com/open-webui/docs), and feature requests into the Main Repository as an issue or discussion. We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon. @@ -139,4 +157,4 @@ For any other immediate concerns and questions, please create an issue in our [i --- -_Last updated on **2026-03-15**._ +_Last updated on **2026-03-20**._ From 6089a55da628b8ecd2174d370b9a9cc89a89c583 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:48:25 -0400 Subject: [PATCH 007/734] fix(ui): chat input triggers correctly re-trigger on backspace (#22899) --- src/lib/components/common/RichTextInput/suggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/common/RichTextInput/suggestions.ts b/src/lib/components/common/RichTextInput/suggestions.ts index 92111f1f19a..7793b99337b 100644 --- a/src/lib/components/common/RichTextInput/suggestions.ts +++ b/src/lib/components/common/RichTextInput/suggestions.ts @@ -22,7 +22,7 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { component: Component, target: container, props: { - char: props?.text, + char: props?.text?.charAt(0), query: props?.query, command: (item) => { props.command({ id: item.id, label: item.label }); From 8f3144adb567ffed44082b2d00b25dccb834496a Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:48:50 -0400 Subject: [PATCH 008/734] fix(ui): close thread sidebar on parent message deletion (#22890) --- src/lib/components/channel/Channel.svelte | 4 ++++ src/lib/components/channel/Thread.svelte | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte index e5301d30c6a..77626a1016c 100644 --- a/src/lib/components/channel/Channel.svelte +++ b/src/lib/components/channel/Channel.svelte @@ -142,6 +142,10 @@ } } else if (type === 'message:delete') { messages = messages.filter((message) => message.id !== data.id); + + if (threadId === data.id) { + threadId = null; + } } else if (type === 'message:reply') { const idx = messages.findIndex((message) => message.id === data.id); diff --git a/src/lib/components/channel/Thread.svelte b/src/lib/components/channel/Thread.svelte index 48028bcc23d..5049337312c 100644 --- a/src/lib/components/channel/Thread.svelte +++ b/src/lib/components/channel/Thread.svelte @@ -84,6 +84,10 @@ } } } else if (type === 'message:delete') { + if (data.id === threadId) { + onClose(); + } + if (messages) { messages = messages.filter((message) => message.id !== data.id); } From 10f06a64fed474e9958b96295a953e0eebf9e4be Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 20 Mar 2026 16:10:00 -0500 Subject: [PATCH 009/734] refac --- src/lib/components/chat/MessageInput.svelte | 9 +++++++++ .../chat/MessageInput/Commands/Knowledge.svelte | 16 ++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index e9cbac366cb..1c05089c25b 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -904,6 +904,9 @@ } ]; } else { + if (files.find((f) => f.url === data || f.name === data)) { + return; + } onUpload(e); } } @@ -939,6 +942,9 @@ } ]; } else { + if (files.find((f) => f.url === data || f.name === data)) { + return; + } onUpload(e); } } @@ -974,6 +980,9 @@ } ]; } else { + if (files.find((f) => f.url === data || f.name === data)) { + return; + } onUpload(e); } } diff --git a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte index 99854aaaf28..3bc9141fe92 100644 --- a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte @@ -138,19 +138,7 @@ await tick(); }); - const onKeyDown = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - select(); - } - }; - onMount(() => { - window.addEventListener('keydown', onKeyDown); - }); - onDestroy(() => { - window.removeEventListener('keydown', onKeyDown); - }); {#if filteredItems.length > 0 || query.startsWith('http')} @@ -220,7 +208,7 @@ + + + + + +
+ +
+ diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte index 924b8b399d0..cac7cdde669 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte @@ -23,6 +23,7 @@ import HtmlToken from './HTMLToken.svelte'; import Clipboard from '$lib/components/icons/Clipboard.svelte'; + import ColonFenceBlock from './ColonFenceBlock.svelte'; export let id: string; export let tokens: Token[]; @@ -434,6 +435,17 @@ {#if token.text} {/if} + {:else if token.type === 'colonFence'} + {:else if token.type === 'space'}
{:else} diff --git a/src/lib/utils/marked/colon-fence-extension.ts b/src/lib/utils/marked/colon-fence-extension.ts new file mode 100644 index 00000000000..c9dca5ee4b1 --- /dev/null +++ b/src/lib/utils/marked/colon-fence-extension.ts @@ -0,0 +1,56 @@ +/** + * Marked extension for colon-fence blocks (:::type ... :::) + * + * Used by newer OpenAI chat models to wrap semantically distinct content: + * :::writing – reusable prose (letters, articles, docs) + * :::code_execution – code execution output + * :::search_results – web search results + * + * The extension is generic and will tokenize any ::: block. + */ + +function colonFenceTokenizer(this: any, src: string) { + // Match :::type at the start of a line, optionally followed by content, then closing ::: + const match = /^:::([\w-]+)\n([\s\S]*?)(?:\n:::(?:\s*$|\n))/m.exec(src); + if (match) { + const fenceType = match[1]; + const text = match[2].trim(); + const raw = match[0]; + + const tokens: any[] = []; + this.lexer.blockTokens(text, tokens); + + return { + type: 'colonFence', + raw, + fenceType, + text, + tokens + }; + } +} + +function colonFenceStart(src: string) { + const idx = src.match(/^:::\w/m); + return idx ? idx.index! : -1; +} + +function colonFenceRenderer(token: any) { + return `
${token.text}
`; +} + +function colonFenceExtension() { + return { + name: 'colonFence', + level: 'block' as const, + start: colonFenceStart, + tokenizer: colonFenceTokenizer, + renderer: colonFenceRenderer + }; +} + +export default function (options = {}) { + return { + extensions: [colonFenceExtension()] + }; +} From 4f0e57420154800946394bc986b2c691462b2782 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Mar 2026 17:26:30 -0500 Subject: [PATCH 026/734] refac --- src/lib/utils/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 9b2318ab00b..48ee42a6c69 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -889,13 +889,19 @@ export const removeDetails = (content, types) => { ); } return segment; - }); + }).trim(); }; export const removeAllDetails = (content) => { + // First pass: strip
blocks on the full string before code-fence + // splitting, so blocks whose body contains triple backticks are caught. + // (replaceOutsideCode splits on ``` fences, which breaks the
+ // regex when the opening and closing tags land in different segments.) + content = content.replace(/]*>[\s\S]*?<\/details>/gi, ''); + // Second pass: catch any remaining blocks that live outside code fences return replaceOutsideCode(content, (segment) => { return segment.replace(/]*>.*?<\/details>/gis, ''); - }); + }).trim(); }; export const processDetails = (content) => { From b44eacbc5a6c8edf7b8baf4cdc7f2a77c7a16ab1 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Mar 2026 17:35:41 -0500 Subject: [PATCH 027/734] refac --- src/lib/components/layout/Sidebar/ChatMenu.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/components/layout/Sidebar/ChatMenu.svelte b/src/lib/components/layout/Sidebar/ChatMenu.svelte index 6b19a530971..83ed89c3e60 100644 --- a/src/lib/components/layout/Sidebar/ChatMenu.svelte +++ b/src/lib/components/layout/Sidebar/ChatMenu.svelte @@ -384,6 +384,7 @@ draggable="false" class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full" on:click={() => { + show = false; cloneChatHandler(); }} > From 8b4ea5bb785211c19148fe1ec756f6d9e78cb1ff Mon Sep 17 00:00:00 2001 From: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:37:23 +0300 Subject: [PATCH 028/734] fix: guard chat:tasks:cancel handler with message_id check (#22743) --- src/lib/components/chat/Chat.svelte | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 79284751e67..a4e2829f823 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -438,11 +438,14 @@ } else if (type === 'chat:completion') { chatCompletionEventHandler(data, message, event.chat_id); } else if (type === 'chat:tasks:cancel') { - taskIds = null; - const responseMessage = history.messages[history.currentId]; - // Set all response messages to done - for (const messageId of history.messages[responseMessage.parentId].childrenIds) { - history.messages[messageId].done = true; + if (event.message_id === history.currentId) { + taskIds = null; + // Set all response messages to done + for (const messageId of history.messages[message.parentId].childrenIds) { + history.messages[messageId].done = true; + } + } else { + message.done = true; } } else if (type === 'chat:message:delta' || type === 'message') { message.content += data.content; From 4d67c817ec47c0e5d7c8c87bb54fde569393ad06 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Mar 2026 17:41:22 -0500 Subject: [PATCH 029/734] refac --- src/lib/components/chat/Messages/UserMessage.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index 201c04de54f..375994b1cf6 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -135,7 +135,7 @@ {#if !($settings?.chatBubble ?? true)}
@@ -147,8 +147,8 @@ {#if message.user} {$i18n.t('You')} {message?.user ?? ''} - {:else if $settings.showUsername || $_user.name !== user.name} - {user.name} + {:else if $settings.showUsername || $_user?.name !== user?.name} + {user?.name ?? $i18n.t('You')} {:else} {$i18n.t('You')} {/if} From 85411e4867af858dbd0feece42c586de87ffbb47 Mon Sep 17 00:00:00 2001 From: Shamil Date: Sun, 22 Mar 2026 01:42:37 +0300 Subject: [PATCH 030/734] chore: align black with Ruff backend formatting (#22766) --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 64e69032f39..65a23662300 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,6 +216,10 @@ dev = [ "ruff>=0.15.5", ] +[tool.black] +line-length = 120 +skip-string-normalization = true + [tool.ruff] line-length = 120 From 4c8615f01c1e5f591829872b09d5e02845f49062 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Mar 2026 17:45:36 -0500 Subject: [PATCH 031/734] refac --- src/lib/components/ImportModal.svelte | 4 ++-- .../admin/Functions/FunctionEditor.svelte | 3 ++- .../workspace/Tools/ToolkitEditor.svelte | 3 ++- src/lib/utils/index.ts | 16 ++++++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/lib/components/ImportModal.svelte b/src/lib/components/ImportModal.svelte index 2163ce26c85..043c0367997 100644 --- a/src/lib/components/ImportModal.svelte +++ b/src/lib/components/ImportModal.svelte @@ -6,7 +6,7 @@ import Spinner from '$lib/components/common/Spinner.svelte'; import Modal from '$lib/components/common/Modal.svelte'; import XMark from '$lib/components/icons/XMark.svelte'; - import { extractFrontmatter } from '$lib/utils'; + import { extractFrontmatter, nameToId } from '$lib/utils'; export let show = false; @@ -42,7 +42,7 @@ toast.success(successMessage); let func = res; - func.id = func.id || func.name.replace(/\s+/g, '_').toLowerCase(); + func.id = func.id || nameToId(func.name); const frontmatter = extractFrontmatter(res.content); // Ensure frontmatter is extracted diff --git a/src/lib/components/admin/Functions/FunctionEditor.svelte b/src/lib/components/admin/Functions/FunctionEditor.svelte index 1ef7bddc16f..a6f70dc3b5e 100644 --- a/src/lib/components/admin/Functions/FunctionEditor.svelte +++ b/src/lib/components/admin/Functions/FunctionEditor.svelte @@ -4,6 +4,7 @@ const i18n = getContext('i18n'); + import { nameToId } from '$lib/utils'; import CodeEditor from '$lib/components/common/CodeEditor.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import Badge from '$lib/components/common/Badge.svelte'; @@ -36,7 +37,7 @@ }; $: if (name && !edit && !clone) { - id = name.replace(/\s+/g, '_').toLowerCase(); + id = nameToId(name); } let codeEditor; diff --git a/src/lib/components/workspace/Tools/ToolkitEditor.svelte b/src/lib/components/workspace/Tools/ToolkitEditor.svelte index 90e8e436a74..bc8ad91dcf4 100644 --- a/src/lib/components/workspace/Tools/ToolkitEditor.svelte +++ b/src/lib/components/workspace/Tools/ToolkitEditor.svelte @@ -8,6 +8,7 @@ import { user } from '$lib/stores'; import { updateToolAccessGrants } from '$lib/apis/tools'; + import { nameToId } from '$lib/utils'; import CodeEditor from '$lib/components/common/CodeEditor.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; @@ -45,7 +46,7 @@ }; $: if (name && !edit && !clone) { - id = name.replace(/\s+/g, '_').toLowerCase(); + id = nameToId(name); } let codeEditor; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 48ee42a6c69..8f9d68eb05e 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1411,6 +1411,22 @@ export const slugify = (str: string): string => { ); }; +/** + * Convert a display name into a safe, underscore-delimited identifier. + * Strips emojis, accents, and any non-alphanumeric characters so the + * result is always accepted by backend validation. + * + * e.g. "My Tool 😄" → "my_tool" + */ +export const nameToId = (name: string): string => { + return name + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^\w]+/g, '_') + .replace(/^_+|_+$/g, '') + .toLowerCase(); +}; + export const extractInputVariables = (text: string): Record => { const regex = /{{\s*([^|}\s]+)\s*\|\s*([^}]+)\s*}}/g; const regularRegex = /{{\s*([^|}\s]+)\s*}}/g; From 17c819a3c227208908b39df8f90739dc1b860535 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:48:30 -0400 Subject: [PATCH 032/734] feat: add confirmation dialog for single memory entry deletion (#22888) * feat(ui): add confirmation dialog for memory deletion * fix --- .../Personalization/ManageModal.svelte | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/lib/components/chat/Settings/Personalization/ManageModal.svelte b/src/lib/components/chat/Settings/Personalization/ManageModal.svelte index 2f5b78529a5..a4515cc1ff4 100644 --- a/src/lib/components/chat/Settings/Personalization/ManageModal.svelte +++ b/src/lib/components/chat/Settings/Personalization/ManageModal.svelte @@ -49,6 +49,7 @@ let selectedMemory = null; let showClearConfirmDialog = false; + let showDeleteConfirm = false; $: filteredMemories = query ? memories.filter((m) => m.content?.toLowerCase().includes(query.toLowerCase())) @@ -238,20 +239,10 @@ - {#if $user?.role === 'admin' || ($user?.permissions?.chat?.tts ?? true)} + {#if !readOnly && ($user?.role === 'admin' || ($user?.permissions?.chat?.tts ?? true))}
+ + {#if hasPublicReadGrant(accessGrants ?? []) && accessRoles.includes('write')} +
+
+ {$i18n.t('Allow everyone to edit')} +
+ { + togglePublicWrite(); + }} + /> +
+ {/if} {#if share} From 59171daa352fe106d2a42c1322ff024638928660 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 22 Mar 2026 06:59:56 -0500 Subject: [PATCH 060/734] refac --- src/lib/components/workspace/common/AccessControl.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/workspace/common/AccessControl.svelte b/src/lib/components/workspace/common/AccessControl.svelte index bd8c5dca593..3eca4afb09c 100644 --- a/src/lib/components/workspace/common/AccessControl.svelte +++ b/src/lib/components/workspace/common/AccessControl.svelte @@ -519,7 +519,7 @@ {#if hasPublicReadGrant(accessGrants ?? []) && accessRoles.includes('write')}
- {$i18n.t('Allow everyone to edit')} + {$i18n.t('Allow public write access')}
Date: Sun, 22 Mar 2026 21:36:45 -0500 Subject: [PATCH 061/734] refac --- src/lib/components/chat/Chat.svelte | 18 +++++------ src/lib/utils/index.ts | 46 ++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 6fa1fa3ff7a..ac23a7ffbe2 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -961,13 +961,12 @@ if (message?.role !== 'user' && message?.content) { const { codeBlocks: codeBlocks, - html: htmlContent, - css: cssContent, - js: jsContent + htmlGroups: htmlGroups } = getCodeBlockContents(message.content); - if (htmlContent || cssContent || jsContent) { - const renderedContent = ` + if (htmlGroups && htmlGroups.length > 0) { + htmlGroups.forEach((group) => { + const renderedContent = ` @@ -978,19 +977,20 @@ background-color: white; /* Ensure the iframe has a white background */ } - ${cssContent} + ${group.css} - ${htmlContent} + ${group.html} <${''}script> - ${jsContent} + ${group.js} `; - contents = [...contents, { type: 'iframe', content: renderedContent }]; + contents = [...contents, { type: 'iframe', content: renderedContent }]; + }); } else { // Check for SVG content for (const block of codeBlocks) { diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 8f9d68eb05e..de999413197 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1729,9 +1729,18 @@ export const getCodeBlockContents = (content: string): object => { let codeBlocks = []; - let htmlContent = ''; - let cssContent = ''; - let jsContent = ''; + // Groups of related HTML/CSS/JS blocks. Each HTML block starts a new group; + // CSS and JS blocks attach to the current (most recent) group. + // This preserves the existing behaviour for "dumb" models that output + // separate html/css/js blocks meant to form a single page, while also + // allowing multiple distinct HTML blocks to produce separate artifacts. + let htmlGroups: Array<{ html: string; css: string; js: string }> = []; + + const initDefaultGroup = () => { + if (htmlGroups.length === 0) { + htmlGroups.push({ html: '', css: '', js: '' }); + } + }; if (codeBlockContents) { codeBlockContents.forEach((block) => { @@ -1744,11 +1753,14 @@ export const getCodeBlockContents = (content: string): object => { const { lang, code } = block; if (lang === 'html') { - htmlContent += code + '\n'; + // Each HTML block starts a new group + htmlGroups.push({ html: code + '\n', css: '', js: '' }); } else if (lang === 'css') { - cssContent += code + '\n'; + initDefaultGroup(); + htmlGroups[htmlGroups.length - 1].css += code + '\n'; } else if (lang === 'javascript' || lang === 'js') { - jsContent += code + '\n'; + initDefaultGroup(); + htmlGroups[htmlGroups.length - 1].js += code + '\n'; } }); } else { @@ -1763,28 +1775,42 @@ export const getCodeBlockContents = (content: string): object => { if (inlineHtml) { inlineHtml.forEach((block) => { const content = block.replace(/<\/?html>/gi, ''); // Remove tags - htmlContent += content + '\n'; + htmlGroups.push({ html: content + '\n', css: '', js: '' }); }); } if (inlineCss) { inlineCss.forEach((block) => { const content = block.replace(/<\/?style>/gi, ''); // Remove From 1dc647f43b1929f5c4d1af393a90a47f56cb745e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 23 Mar 2026 22:00:37 -0500 Subject: [PATCH 073/734] refac --- .../chat/FileNav/PortPreview.svelte | 130 ++++-------------- 1 file changed, 23 insertions(+), 107 deletions(-) diff --git a/src/lib/components/chat/FileNav/PortPreview.svelte b/src/lib/components/chat/FileNav/PortPreview.svelte index 0e029aa0835..9ca44260270 100644 --- a/src/lib/components/chat/FileNav/PortPreview.svelte +++ b/src/lib/components/chat/FileNav/PortPreview.svelte @@ -5,6 +5,7 @@ const i18n = getContext('i18n'); + export let baseUrl: string; export let port: number; export let path: string = ''; @@ -23,7 +24,6 @@ $: canGoForward = historyIndex < history.length - 1; const pushHistory = (newPath: string) => { - // Trim forward history when navigating from a non-tip position if (historyIndex < history.length - 1) { history = history.slice(0, historyIndex + 1); } @@ -35,6 +35,7 @@ if (!canGoBack) return; historyIndex -= 1; path = history[historyIndex]; + syncUrlBar(); iframeKey += 1; }; @@ -42,28 +43,26 @@ if (!canGoForward) return; historyIndex += 1; path = history[historyIndex]; + syncUrlBar(); iframeKey += 1; }; // ── URLs ───────────────────────────────────────────────────────────── $: proxyUrl = getPortProxyUrl(baseUrl, port, path); - // The proxy path prefix, e.g. "/proxy/8080/" $: proxyPathPrefix = (() => { try { - return new URL(getPortProxyUrl(baseUrl, port, '')).pathname; + return new URL(getPortProxyUrl(baseUrl, port, ''), window.location.origin).pathname; } catch { return `/proxy/${port}/`; } })(); - // Display-friendly localhost URL for the address bar - $: displayUrl = `localhost:${port}${path ? '/' + path : ''}`; - $: urlInput = displayUrl; + const makeDisplayUrl = (p: string) => `localhost:${port}${p ? '/' + p : ''}`; + const syncUrlBar = () => { urlInput = makeDisplayUrl(path); }; + urlInput = makeDisplayUrl(path); - const refresh = () => { - iframeKey += 1; - }; + const refresh = () => { iframeKey += 1; }; const openExternal = () => { window.open(proxyUrl, '_blank', 'noopener,noreferrer'); @@ -84,113 +83,36 @@ path = newPath; pushHistory(path); } + syncUrlBar(); iframeKey += 1; }; /** - * After each iframe load: - * 1. Sync the address bar with the current iframe path - * 2. Inject a click interceptor to rewrite links BEFORE navigation - * 3. Redirect if the page already escaped the proxy (fallback) + * Read the iframe's current location and sync the URL bar. + * If the iframe escaped the proxy prefix, redirect it back. */ const onIframeLoad = () => { isLoading = false; if (!iframeEl) return; try { - const doc = iframeEl.contentDocument; - const win = iframeEl.contentWindow; - if (!doc || !win) return; - - const loc = win.location; - const iframePath = loc?.pathname ?? ''; - const iframeSearch = loc?.search ?? ''; - const iframeHash = loc?.hash ?? ''; + const loc = iframeEl.contentWindow?.location; + if (!loc) return; + const iframePath = loc.pathname ?? ''; + const iframeSearch = loc.search ?? ''; + const iframeHash = loc.hash ?? ''; if (iframePath.startsWith(proxyPathPrefix)) { - // Still inside the proxy — sync address bar const relativePath = iframePath.slice(proxyPathPrefix.length) + iframeSearch + iframeHash; if (relativePath !== path) { path = relativePath; pushHistory(path); - urlInput = displayUrl; + syncUrlBar(); } - - // Inject click interceptor to rewrite links before navigation - injectLinkInterceptor(doc, win); - } else if (iframePath && iframePath !== 'about:blank') { - // Escaped the proxy — redirect back through it - const escapedPath = iframePath.replace(/^\//, '') + iframeSearch + iframeHash; - const correctedUrl = getPortProxyUrl(baseUrl, port, escapedPath); - loc?.replace(correctedUrl); } } catch { // Cross-origin — can't access } }; - - /** - * Inject a click interceptor into the iframe document that rewrites - * anchor hrefs to stay within the proxy path. - */ - const injectLinkInterceptor = (doc: Document, win: Window) => { - // Skip if we already injected - if ((win as any).__portPreviewIntercepted) return; - (win as any).__portPreviewIntercepted = true; - - const prefix = proxyPathPrefix; - - // Intercept all click events on anchors - doc.addEventListener('click', (e: MouseEvent) => { - const anchor = (e.target as HTMLElement)?.closest?.('a'); - if (!anchor || !anchor.href) return; - - try { - const url = new URL(anchor.href, win.location.href); - - // Only intercept same-origin links that escaped the proxy - if (url.origin === win.location.origin && !url.pathname.startsWith(prefix)) { - e.preventDefault(); - e.stopPropagation(); - // Rewrite to go through proxy - const rewrittenPath = prefix + url.pathname.replace(/^\//, '') + url.search + url.hash; - win.location.href = url.origin + rewrittenPath; - } - } catch { - // Invalid URL — let browser handle it - } - }, true); // Use capture phase to intercept before SPA routers - - // Also intercept form submissions - doc.addEventListener('submit', (e: Event) => { - const form = e.target as HTMLFormElement; - if (!form?.action) return; - - try { - const url = new URL(form.action, win.location.href); - if (url.origin === win.location.origin && !url.pathname.startsWith(prefix)) { - form.action = url.origin + prefix + url.pathname.replace(/^\//, '') + url.search; - } - } catch {} - }, true); - - // Intercept window.open and programmatic navigation - const origOpen = win.open.bind(win); - win.open = function(url?: string | URL, ...args: any[]) { - if (url && typeof url === 'string') { - try { - const parsed = new URL(url, win.location.href); - if (parsed.origin === win.location.origin && !parsed.pathname.startsWith(prefix)) { - url = parsed.origin + prefix + parsed.pathname.replace(/^\//, '') + parsed.search + parsed.hash; - } - } catch {} - } - return origOpen(url, ...args); - }; - }; - - const onIframeLoadStart = () => { - isLoading = true; - };
@@ -230,22 +152,16 @@ - + @@ -275,7 +191,7 @@ - +
diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index ee242fb7359..250c75a9ff9 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -69,6 +69,8 @@ let temporaryChatByDefault = false; let chatFadeStreamingText = true; let collapseCodeBlocks = false; + let renderMarkdownInUserMessages = true; + let renderMarkdownInAssistantMessages = true; let expandDetails = false; let renderMarkdownInPreviews = true; let showChatTitleInTab = true; @@ -232,6 +234,8 @@ copyFormatted = $settings?.copyFormatted ?? false; collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false; + renderMarkdownInUserMessages = $settings?.renderMarkdownInUserMessages ?? true; + renderMarkdownInAssistantMessages = $settings?.renderMarkdownInAssistantMessages ?? true; expandDetails = $settings?.expandDetails ?? false; renderMarkdownInPreviews = $settings?.renderMarkdownInPreviews ?? true; @@ -776,6 +780,44 @@ +
+
+
+ {$i18n.t('Render Markdown in User Messages')} +
+ +
+ { + saveSettings({ renderMarkdownInUserMessages }); + }} + /> +
+
+
+ +
+
+
+ {$i18n.t('Render Markdown in Assistant Messages')} +
+ +
+ { + saveSettings({ renderMarkdownInAssistantMessages }); + }} + /> +
+
+
+
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index e3a291a9563..49e1c6f56e0 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1697,6 +1697,8 @@ "Remove Model": "", "Rename": "", "Renamed to {{name}}": "", + "Render Markdown in Assistant Messages": "", + "Render Markdown in User Messages": "", "Render Markdown in Previews": "", "Reorder Models": "", "Repeats": "", From 2dbf7b6764a7922458d3b0139687ad6dcd7596d9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:12:38 +0900 Subject: [PATCH 711/734] refac --- backend/open_webui/routers/folders.py | 31 ++++++++-------- backend/open_webui/routers/knowledge.py | 18 ++++++++++ .../open_webui/utils/access_control/files.py | 35 +++++++++++++++++++ backend/open_webui/utils/middleware.py | 9 +++-- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index ebd0c0cb17b..7dda918821c 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -16,8 +16,6 @@ Folders, ) from open_webui.models.chats import Chats -from open_webui.models.files import Files -from open_webui.models.knowledge import Knowledges from open_webui.config import UPLOAD_DIR @@ -32,6 +30,7 @@ from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control.files import get_accessible_folder_files log = logging.getLogger(__name__) @@ -75,20 +74,10 @@ async def get_folders( if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db): folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db) - if folder.data: - if 'files' in folder.data: - valid_files = [] - for file in folder.data['files']: - if file.get('type') == 'file': - if await Files.check_access_by_user_id(file.get('id'), user.id, 'read', db=db): - valid_files.append(file) - elif file.get('type') == 'collection': - if await Knowledges.check_access_by_user_id(file.get('id'), user.id, 'read', db=db): - valid_files.append(file) - else: - valid_files.append(file) - - folder.data['files'] = valid_files + if folder.data and 'files' in folder.data: + accessible_files = await get_accessible_folder_files(folder.data['files'], user, db=db) + if len(accessible_files) != len(folder.data.get('files', [])): + folder.data['files'] = accessible_files await Folders.update_folder_by_id_and_user_id( folder.id, user.id, FolderUpdateForm(data=folder.data), db=db ) @@ -173,6 +162,16 @@ async def update_folder_name_by_id( detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), ) + # Validate read access to every file/collection being attached. + # Folder files are consumed by chat middleware as RAG context. + if form_data.data and isinstance(form_data.data.get('files'), list): + accessible_files = await get_accessible_folder_files(form_data.data['files'], user, db=db) + if len(accessible_files) != len(form_data.data['files']): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + try: folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db) return folder diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index f503169fc0f..8ff987b6103 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -31,6 +31,7 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.utils.auth import get_verified_user, get_admin_user from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.utils.access_control.files import has_access_to_file from open_webui.models.access_grants import AccessGrants @@ -656,6 +657,14 @@ async def add_file_to_knowledge_by_id( detail=ERROR_MESSAGES.FILE_NOT_PROCESSED, ) + # KB write-access alone is not enough — caller must also be able to read the file. + if file.user_id != user.id and user.role != 'admin': + if not await has_access_to_file(file.id, 'read', user, db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + # Add content to the vector database try: await process_file( @@ -1017,6 +1026,15 @@ async def add_files_to_knowledge_batch( detail=f'File {missing_ids[0]} not found', ) + # Per-file read-access check — same gate as the single-file endpoint. + if user.role != 'admin': + for file in files: + if file.user_id != user.id and not await has_access_to_file(file.id, 'read', user, db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + # Process files try: result = await process_files_batch( diff --git a/backend/open_webui/utils/access_control/files.py b/backend/open_webui/utils/access_control/files.py index a48dfeb0f10..fb318e3c66f 100644 --- a/backend/open_webui/utils/access_control/files.py +++ b/backend/open_webui/utils/access_control/files.py @@ -87,3 +87,38 @@ async def has_access_to_file( return True return False + + +async def get_accessible_folder_files( + entries: list[dict] | None, + user: UserModel, + db: AsyncSession | None = None, +) -> list[dict]: + """Filter folder.data['files'] entries to those the caller can read. + + Each entry is expected to have 'type' ('file' or 'collection') and 'id'. + Admins bypass all checks. Unknown types are kept as-is. + """ + if not entries: + return [] + if user.role == 'admin': + return list(entries) + + accessible: list[dict] = [] + for entry in entries: + if not isinstance(entry, dict): + continue + entry_type = entry.get('type') + entry_id = entry.get('id') + if not entry_id: + accessible.append(entry) + continue + if entry_type == 'file': + if await has_access_to_file(entry_id, 'read', user, db=db): + accessible.append(entry) + elif entry_type == 'collection': + if await Knowledges.check_access_by_user_id(entry_id, user.id, 'read', db=db): + accessible.append(entry) + else: + accessible.append(entry) + return accessible diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 3e25effa9ae..60796fa22a1 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -106,6 +106,7 @@ get_terminal_tools, ) from open_webui.utils.access_control import has_connection_access +from open_webui.utils.access_control.files import get_accessible_folder_files from open_webui.utils.plugin import load_function_module_by_id from open_webui.utils.filter import ( get_sorted_filter_ids, @@ -2407,15 +2408,17 @@ async def process_chat_payload(request, form_data, user, metadata, model): if 'system_prompt' in folder.data: form_data = await apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user) if 'files' in folder.data: + # Defensive: filter to entries the caller can still read. + allowed_files = await get_accessible_folder_files(folder.data['files'], user) if metadata.get('params', {}).get('function_calling') != 'native': form_data['files'] = [ - *folder.data['files'], + *allowed_files, *form_data.get('files', []), ] else: # Native FC: skip RAG injection, builtin tools # will read folder knowledge from metadata. - metadata['folder_knowledge'] = folder.data['files'] + metadata['folder_knowledge'] = allowed_files # Model "Knowledge" handling user_message = get_last_user_message(form_data['messages']) @@ -2615,7 +2618,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id) if folder and folder.data and 'files' in folder.data: files = [f for f in files if f.get('id', None) != folder_id] - files = [*files, *folder.data['files']] + files = [*files, *await get_accessible_folder_files(folder.data['files'], user)] # files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]] # Remove duplicate files based on their content From 3a21b334cce30226750c5c537345dc51bb8bef17 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:15:46 +0900 Subject: [PATCH 712/734] refac --- backend/open_webui/routers/chats.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 8bfe5dfc51e..9c4609477c0 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -1412,19 +1412,16 @@ async def update_shared_chat_access_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + else: + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if not chat: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if chat.user_id != user.id and user.role != 'admin': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - form_data.access_grants = await filter_allowed_access_grants( request.app.state.config.USER_PERMISSIONS, user.id, @@ -1449,19 +1446,16 @@ async def get_shared_chat_access_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + else: + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if not chat: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if chat.user_id != user.id and user.role != 'admin': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db) return [ { From 15e696691cad98692c329de62ed8a5bdb3a26d4e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:25:11 +0900 Subject: [PATCH 713/734] refac --- backend/open_webui/env.py | 9 +++++++++ backend/open_webui/routers/users.py | 12 +++++++++--- backend/open_webui/utils/validate.py | 14 +++++--------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 1ab18fe1c77..903b3effcc1 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -260,6 +260,15 @@ def parse_section(section): # controlled origins) and fall through to the default image instead. ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true' +PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( + t.strip() + for t in os.environ.get( + 'PROFILE_IMAGE_ALLOWED_MIME_TYPES', + 'image/png,image/jpeg,image/gif,image/webp', + ).split(',') + if t.strip() +) + #################################### # WEBUI_BUILD_HASH #################################### diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 7fe5fcd2dcb..33d1cd425cd 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -29,7 +29,7 @@ ) from open_webui.constants import ERROR_MESSAGES -from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, STATIC_DIR +from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR from open_webui.internal.db import get_async_session @@ -494,12 +494,18 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u header, base64_data = user.profile_image_url.split(',', 1) image_data = base64.b64decode(base64_data) image_buffer = io.BytesIO(image_data) - media_type = header.split(';')[0].lstrip('data:') + media_type = header.split(';')[0].lstrip('data:').lower() + + if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: + return FileResponse(f'{STATIC_DIR}/user.png') return StreamingResponse( image_buffer, media_type=media_type, - headers={'Content-Disposition': 'inline'}, + headers={ + 'Content-Disposition': 'inline', + 'X-Content-Type-Options': 'nosniff', + }, ) except Exception as e: pass diff --git a/backend/open_webui/utils/validate.py b/backend/open_webui/utils/validate.py index 1e98b411059..68a56dfadc9 100644 --- a/backend/open_webui/utils/validate.py +++ b/backend/open_webui/utils/validate.py @@ -3,17 +3,13 @@ import re from urllib.parse import urlparse -# Matches the OWUI-generated profile image route. ``[^/?#]+`` accepts -# any user-ID without allowing path-traversal or query/fragment injection, -# and the ``$`` anchor rejects trailing path components. +from open_webui.env import PROFILE_IMAGE_ALLOWED_MIME_TYPES + _USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$') -# Validates MIME type and structure of base64 data URIs. Only the prefix -# is checked — validating the full base64 payload would mean running a -# regex across megabytes of data on every Pydantic instantiation for zero -# security benefit (corrupt base64 simply renders a broken image, same as -# a 404 URL). SVG is intentionally excluded: it can carry embedded scripts. -_SAFE_DATA_URI_RE = re.compile(r'^data:image/(png|jpeg|gif|webp);base64,', re.IGNORECASE) +# Data-URI prefix validator derived from PROFILE_IMAGE_ALLOWED_MIME_TYPES. +_mime_suffixes = '|'.join(re.escape(t.split('/')[-1]) for t in sorted(PROFILE_IMAGE_ALLOWED_MIME_TYPES)) +_SAFE_DATA_URI_RE = re.compile(rf'^data:image/({_mime_suffixes});base64,', re.IGNORECASE) # Exact relative paths accepted as profile images. These are the only # static-asset paths OWUI itself assigns; no prefix/wildcard matching is From 39777e35d87dc88e446c8859f552ad4e2d24d5ca Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:28:38 +0900 Subject: [PATCH 714/734] doc: changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1930256bf..1ea4efac177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 🛡️ **Redirect-based SSRF protection.** All outbound HTTP requests now block 3xx redirects by default via a new `AIOHTTP_CLIENT_ALLOW_REDIRECTS` environment variable, preventing redirect-based SSRF where a public URL silently redirects to internal addresses (RFC 1918, loopback, cloud-metadata endpoints). Affected call sites include web fetch, image loading, OAuth discovery, tool server execution, and code interpreter login. [#24491](https://github.com/open-webui/open-webui/pull/24491) +- 🛡️ **Iframe content security policy.** Administrators can now configure a Content-Security-Policy for all srcdoc iframes (Artifacts, tool embeds, file previews, citation modals) via the `IFRAME_CSP` environment variable, restricting what LLM-generated or user-uploaded HTML can load and execute inside previews. [Commit](https://github.com/open-webui/open-webui/commit/3bba1c227059a44c7eeefa97b8c69a63bf4f3454) +- 🎛️ **Granular markdown rendering controls.** Users can now independently disable Markdown rendering for user messages and assistant responses from Interface settings, preventing unintended formatting when pasting text that contains Markdown-sensitive characters. [Commit](https://github.com/open-webui/open-webui/commit/4a1064cefd6f48a8b3b02cd31f77838c8802b635) +- 🔧 **Terminal proxy response headers.** Administrators can now inject custom response headers into terminal proxy responses via the `TERMINAL_PROXY_HEADERS` environment variable (JSON object), enabling deployment-specific security headers like sandbox policies for proxied content. [Commit](https://github.com/open-webui/open-webui/commit/8d3133fe2835122bffaa4f2ce584730bc9c78981) ### Fixed - 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486) - 🔐 **Skill public sharing permission enforcement.** Creating or updating skills now filters access grants through the `sharing.public_skills` permission, preventing non-admin users from making skills publicly accessible without the required permission. [#24494](https://github.com/open-webui/open-webui/pull/24494) - 🔐 **Calendar public sharing permission enforcement.** Creating or updating calendars now filters access grants through a new `sharing.public_calendars` permission, preventing users from making calendars publicly readable or writable without explicit admin-granted sharing permission. [#24493](https://github.com/open-webui/open-webui/pull/24493) +- 🔐 **Feedback user attribution spoofing.** Submitting evaluation feedback can no longer forge the `user_id` field through mass-assignment, preventing authenticated users from attributing ratings to other users and corrupting Elo leaderboard rankings and admin feedback exports. [#24508](https://github.com/open-webui/open-webui/pull/24508) +- 🛡️ **Image URL redirect-based SSRF.** Chat messages containing image URLs no longer follow 3xx redirects to internal addresses during base64 conversion, closing the most reachable redirect-based SSRF variant that required no special permissions or feature flags. [#24524](https://github.com/open-webui/open-webui/pull/24524) +- 🛡️ **Collection write access on file processing.** The `process_file` and `process_files_batch` retrieval endpoints now enforce collection write-access checks before embedding content, preventing authenticated users from injecting file content into another user's knowledge-base collection. [#24524](https://github.com/open-webui/open-webui/pull/24524) +- 🔐 **Tool source code update authorization.** Updating a tool's Python source code now requires `workspace.tools` or `workspace.tools_import` permission, preventing users with only a write-access grant from overwriting executable tool code while still allowing metadata edits. [#24513](https://github.com/open-webui/open-webui/pull/24513) +- 🔐 **Channel message ownership enforcement.** Updating or deleting messages in group and DM channels now requires message ownership, preventing channel members from tampering with or silently removing other members' messages. [#24506](https://github.com/open-webui/open-webui/pull/24506) +- 🔐 **Channel pin write permission.** Pinning and unpinning messages on standard channels now requires write permission instead of read permission, preventing read-only users from modifying pinned content. [#24521](https://github.com/open-webui/open-webui/pull/24521) +- 🛡️ **Image generation URL validation.** Generated image URLs are now validated through `validate_url()` before fetching, aligning the defense-in-depth posture with sibling image-loading paths. [#24518](https://github.com/open-webui/open-webui/pull/24518) +- 🔐 **Model params exposure for read-only users.** The per-model API endpoint now strips the `params` dict (including system prompts) from responses to callers without write access, preventing read-only users from viewing admin-curated model configuration. [#24525](https://github.com/open-webui/open-webui/pull/24525) +- 🛡️ **URL parser SSRF bypass.** URL validation now rejects backslash, tab, CR, and LF characters that cause urllib and requests/aiohttp to disagree on the target host, closing a parser-confusion SSRF bypass. [#24534](https://github.com/open-webui/open-webui/pull/24534) +- 🛡️ **Profile image MIME-type allowlist.** Serving profile images from data URIs now enforces a strict MIME-type allowlist (PNG, JPEG, GIF, WEBP by default, configurable via `PROFILE_IMAGE_ALLOWED_MIME_TYPES`) and sets `X-Content-Type-Options: nosniff`, preventing stored-XSS through SVG or other executable content types. [Commit](https://github.com/open-webui/open-webui/commit/15e696691cad98692c329de62ed8a5bdb3a26d4e) +- 🔐 **File ownership in folder and knowledge attachments.** Attaching files to folders or knowledge bases now verifies per-file read access, and folder file lists in chat middleware are filtered to entries the caller can read, preventing unauthorized file content from being injected into RAG context. [Commit](https://github.com/open-webui/open-webui/commit/2dbf7b6764a7922458d3b0139687ad6dcd7596d9) +- 🔐 **Shared chat access for owners and admins.** Chat owners can now view and clone their own shared chats without requiring an explicit access grant, and administrators can manage shared chat access controls on any chat. [Commit](https://github.com/open-webui/open-webui/commit/3a21b334cce30226750c5c537345dc51bb8bef17), [Commit](https://github.com/open-webui/open-webui/commit/315566064aedeff071854b023d09e5f1ea0eb950) +- 🧵 **Legacy chat history self-healing.** Loading legacy conversations now automatically detects broken parent-link graphs in migrated message records, merges missing messages from the embedded JSON history, and backfills them to the normalized table so future loads use the fast path without data loss. [Commit](https://github.com/open-webui/open-webui/commit/1388f4568b8f508c26542673dd01f1fa049e798a) +- 🎛️ **Filter selector reactivity.** Model filter checkboxes now derive state reactively from the current filter list and selected IDs instead of capturing a one-time snapshot at mount, so checkboxes update correctly when model contexts or filter configurations change at runtime. [Commit](https://github.com/open-webui/open-webui/commit/d1ef5382377f590f97a6dbaee88f369e6d7c5f6f) +- 🌐 **Portuguese (Brazil) translation updates.** Translations for newly added UI items were added along with a consistency pass across existing entries. [#24503](https://github.com/open-webui/open-webui/pull/24503) ### Changed From c951b4f26226033e7f47d21292c125be4628fb74 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:29:13 +0900 Subject: [PATCH 715/734] chore: format --- backend/open_webui/models/chats.py | 10 ++++------ backend/open_webui/routers/models.py | 15 ++++++--------- backend/open_webui/routers/tools.py | 4 +--- src/lib/components/chat/Artifacts.svelte | 5 ++++- .../chat/Messages/ContentRenderer.svelte | 6 +----- .../components/chat/Messages/UserMessage.svelte | 4 +++- src/lib/i18n/locales/ar-BH/translation.json | 2 ++ src/lib/i18n/locales/ar/translation.json | 2 ++ src/lib/i18n/locales/az-AZ/translation.json | 2 ++ src/lib/i18n/locales/bg-BG/translation.json | 2 ++ src/lib/i18n/locales/bn-BD/translation.json | 2 ++ src/lib/i18n/locales/bo-TB/translation.json | 2 ++ src/lib/i18n/locales/bs-BA/translation.json | 2 ++ src/lib/i18n/locales/ca-ES/translation.json | 2 ++ src/lib/i18n/locales/ceb-PH/translation.json | 2 ++ src/lib/i18n/locales/cs-CZ/translation.json | 2 ++ src/lib/i18n/locales/da-DK/translation.json | 2 ++ src/lib/i18n/locales/de-DE/translation.json | 2 ++ src/lib/i18n/locales/dg-DG/translation.json | 2 ++ src/lib/i18n/locales/el-GR/translation.json | 2 ++ src/lib/i18n/locales/en-GB/translation.json | 2 ++ src/lib/i18n/locales/en-US/translation.json | 2 +- src/lib/i18n/locales/es-ES/translation.json | 2 ++ src/lib/i18n/locales/et-EE/translation.json | 2 ++ src/lib/i18n/locales/eu-ES/translation.json | 2 ++ src/lib/i18n/locales/fa-IR/translation.json | 2 ++ src/lib/i18n/locales/fi-FI/translation.json | 2 ++ src/lib/i18n/locales/fil-PH/translation.json | 2 ++ src/lib/i18n/locales/fr-CA/translation.json | 2 ++ src/lib/i18n/locales/fr-FR/translation.json | 2 ++ src/lib/i18n/locales/gl-ES/translation.json | 2 ++ src/lib/i18n/locales/he-IL/translation.json | 2 ++ src/lib/i18n/locales/hi-IN/translation.json | 2 ++ src/lib/i18n/locales/hr-HR/translation.json | 2 ++ src/lib/i18n/locales/hu-HU/translation.json | 2 ++ src/lib/i18n/locales/id-ID/translation.json | 2 ++ src/lib/i18n/locales/ie-GA/translation.json | 2 ++ src/lib/i18n/locales/it-IT/translation.json | 2 ++ src/lib/i18n/locales/ja-JP/translation.json | 2 ++ src/lib/i18n/locales/ka-GE/translation.json | 2 ++ src/lib/i18n/locales/kab-DZ/translation.json | 2 ++ src/lib/i18n/locales/ko-KR/translation.json | 2 ++ src/lib/i18n/locales/lt-LT/translation.json | 2 ++ src/lib/i18n/locales/lv-LV/translation.json | 2 ++ src/lib/i18n/locales/ms-MY/translation.json | 2 ++ src/lib/i18n/locales/nb-NO/translation.json | 2 ++ src/lib/i18n/locales/nl-NL/translation.json | 2 ++ src/lib/i18n/locales/pa-IN/translation.json | 2 ++ src/lib/i18n/locales/pl-PL/translation.json | 2 ++ src/lib/i18n/locales/pt-BR/translation.json | 2 ++ src/lib/i18n/locales/pt-PT/translation.json | 2 ++ src/lib/i18n/locales/ro-RO/translation.json | 2 ++ src/lib/i18n/locales/ru-RU/translation.json | 2 ++ src/lib/i18n/locales/sk-SK/translation.json | 2 ++ src/lib/i18n/locales/sr-RS/translation.json | 2 ++ src/lib/i18n/locales/sv-SE/translation.json | 2 ++ src/lib/i18n/locales/ta-IN/translation.json | 2 ++ src/lib/i18n/locales/th-TH/translation.json | 2 ++ src/lib/i18n/locales/tk-TM/translation.json | 2 ++ src/lib/i18n/locales/tr-TR/translation.json | 2 ++ src/lib/i18n/locales/ug-CN/translation.json | 2 ++ src/lib/i18n/locales/uk-UA/translation.json | 2 ++ src/lib/i18n/locales/ur-PK/translation.json | 2 ++ src/lib/i18n/locales/uz-Cyrl-UZ/translation.json | 2 ++ src/lib/i18n/locales/uz-Latn-Uz/translation.json | 2 ++ src/lib/i18n/locales/vi-VN/translation.json | 2 ++ src/lib/i18n/locales/zh-CN/translation.json | 2 ++ src/lib/i18n/locales/zh-TW/translation.json | 2 ++ src/lib/utils/csp.ts | 4 +--- 69 files changed, 143 insertions(+), 29 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 51f2f121b3a..957492d8173 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -471,9 +471,7 @@ def get_unresolved_parent_ids(messages_map: dict) -> set[str]: if msg.get('parentId') and msg['parentId'] not in messages_map } - async def backfill_messages_by_chat_id( - self, chat_id: str, user_id: str, messages: dict[str, dict] - ) -> None: + async def backfill_messages_by_chat_id(self, chat_id: str, user_id: str, messages: dict[str, dict]) -> None: """Write messages to the ``chat_message`` table so future lookups use the fast path. Errors are logged but never raised. """ @@ -509,9 +507,9 @@ async def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: # Graph has gaps — enrich from the legacy embedded history. log.info( - 'Chat %s: %d unresolved parent reference(s) in chat_message — ' - 'enriching from legacy history', - id, len(unresolved_ids), + 'Chat %s: %d unresolved parent reference(s) in chat_message — enriching from legacy history', + id, + len(unresolved_ids), ) chat = await self.get_chat_by_id(id) if chat: diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index cef44297606..2a78daa94d0 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -425,15 +425,12 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes ) ) - if ( - write_access - or await AccessGrants.has_access( - user_id=user.id, - resource_type='model', - resource_id=model.id, - permission='read', - db=db, - ) + if write_access or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='read', + db=db, ): model_dict = model.model_dump() # Strip params (system prompt and other admin-curated config) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 3eddefdee69..cd11bcde5e1 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -484,9 +484,7 @@ async def update_tools_by_id( if form_data.content != tools.content: if user.role != 'admin' and not ( await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) - or await has_permission( - user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db - ) + or await has_permission(user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db) ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/src/lib/components/chat/Artifacts.svelte b/src/lib/components/chat/Artifacts.svelte index f8483d122ed..f3ac74897d6 100644 --- a/src/lib/components/chat/Artifacts.svelte +++ b/src/lib/components/chat/Artifacts.svelte @@ -244,7 +244,10 @@