From ecba37070d6eb3cb033195a070b6c4ab5f396415 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 20 Mar 2026 17:05:47 -0500 Subject: [PATCH 01/54] refac --- backend/open_webui/utils/middleware.py | 17 ++++++++++++++++- src/lib/components/chat/Chat.svelte | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 3ff6f551e9..cae2ceed67 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -3027,6 +3027,7 @@ async def non_streaming_chat_response_handler(response, ctx): metadata['chat_id'], metadata['message_id'], { + 'done': True, 'role': 'assistant', 'content': content, 'output': response_output, @@ -4376,6 +4377,7 @@ async def streaming_chat_response_handler(response, ctx): metadata['chat_id'], metadata['message_id'], { + 'done': True, 'content': serialize_output(output), 'output': output, **({'usage': usage} if usage else {}), @@ -4385,7 +4387,13 @@ async def streaming_chat_response_handler(response, ctx): Chats.upsert_message_to_chat_by_id_and_message_id( metadata['chat_id'], metadata['message_id'], - {'usage': usage}, + {'done': True, 'usage': usage}, + ) + else: + Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + {'done': True}, ) # Send a webhook notification if the user is not active @@ -4422,10 +4430,17 @@ async def streaming_chat_response_handler(response, ctx): metadata['chat_id'], metadata['message_id'], { + 'done': True, 'content': serialize_output(output), 'output': output, }, ) + else: + Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + {'done': True}, + ) if response.background is not None: await response.background() diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 97cb481c51..8b3fecc55f 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1239,7 +1239,7 @@ if (history.currentId) { for (const message of Object.values(history.messages)) { - if (message && message.role === 'assistant') { + if (message && message.role === 'assistant' && message.done !== false) { message.done = true; } } From fe772d95e2a931ee89f4c7d6026f7415e9773b7a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 20 Mar 2026 18:43:17 -0500 Subject: [PATCH 02/54] refac --- .../chat/Settings/Personalization/ManageModal.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Settings/Personalization/ManageModal.svelte b/src/lib/components/chat/Settings/Personalization/ManageModal.svelte index a4c08c2d8a..2f5b78529a 100644 --- a/src/lib/components/chat/Settings/Personalization/ManageModal.svelte +++ b/src/lib/components/chat/Settings/Personalization/ManageModal.svelte @@ -225,7 +225,8 @@ + + + + + +
+ +
+ diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte index 924b8b399d..cac7cdde66 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 0000000000..c9dca5ee4b --- /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 16/54] 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 9b2318ab00..48ee42a6c6 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 17/54] 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 6b19a53097..83ed89c3e6 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 18/54] 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 79284751e6..a4e2829f82 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 19/54] 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 201c04de54..375994b1cf 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 20/54] 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 64e69032f3..65a2366230 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 21/54] 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 2163ce26c8..043c036799 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 1ef7bddc16..a6f70dc3b5 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 90e8e436a7..bc8ad91dcf 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 48ee42a6c6..8f9d68eb05 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 22/54] 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 2f5b78529a..a4515cc1ff 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 50/54] 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 bd8c5dca59..3eca4afb09 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 51/54] 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 6fa1fa3ff7..ac23a7ffbe 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 8f9d68eb05..de99941319 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