From 97b7ed73c84767611d3a26d594c36f63619d92ea Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 14 Oct 2026 23:09:31 +0400 Subject: [PATCH 01/20] refac --- .../components/common/DiffEditorModal.svelte | 42 ++++++++++++++++--- src/lib/components/common/diffEditor.ts | 16 +++++++ 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/fo-FO/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/sl-SI/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 + .../i18n/locales/uz-Cyrl-UZ/translation.json | 2 + .../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 + 66 files changed, 181 insertions(+), 5 deletions(-) diff --git a/src/lib/components/common/DiffEditorModal.svelte b/src/lib/components/common/DiffEditorModal.svelte index afe6b4fec7..e802f22880 100644 --- a/src/lib/components/common/DiffEditorModal.svelte +++ b/src/lib/components/common/DiffEditorModal.svelte @@ -23,6 +23,7 @@ } from './diffEditorAlignment'; import { copyChunk, + keepBothChunk, selectedChunk, chunkOverview, changedLineCounts, @@ -65,6 +66,7 @@ let comparing = false; let merge: MergeView | null = null; let activeChunk = 0; + let canKeepBoth = false; let overview: ReturnType = []; let chunkCount = 0; let lineNumberDigits = 1; @@ -101,6 +103,7 @@ proposal = session.proposal; comparing = proposedContent !== undefined; activeChunk = chunkCount = 0; + canKeepBoth = false; overview = []; lineChanges = { removed: 0, added: 0 }; copied = null; @@ -200,6 +203,11 @@ if (index >= 0) activeChunk = index; } activeChunk = Math.max(0, Math.min(activeChunk, chunkCount - 1)); + const chunk = merge.chunks[activeChunk]; + canKeepBoth = + session?.mode === 'edit' && + !!chunk && + keepBothChunk(merge.a.state.doc, merge.b.state.doc, chunk) !== null; }; const reveal = (index: number) => { if (!merge || !chunkCount) return; @@ -215,6 +223,7 @@ effects: EditorView.scrollIntoView(position, { y: 'center' }) }); } + refresh(); }; const navigateOverview = (event: MouseEvent) => { const bounds = @@ -253,6 +262,21 @@ refresh(); }; + const keepBoth = () => { + if (session?.mode !== 'edit' || loading || !merge) return; + const chunk = merge.chunks[activeChunk]; + if (!chunk) return; + const changes = keepBothChunk(merge.a.state.doc, merge.b.state.doc, chunk); + if (!changes) return; + merge.b.dispatch({ + changes, + annotations: isolateHistory.of('full'), + userEvent: 'input.merge' + }); + refresh(); + merge.b.focus(); + }; + const mountMerge = (node: HTMLElement) => { let disposed = false; let view: MergeView | null = null; @@ -403,7 +427,7 @@ >
-
+

{session.title || $i18n.t('Compare revision')}

@@ -415,7 +439,7 @@ > {/if} {#if comparing} -
+
{loading @@ -469,6 +493,13 @@ title={$i18n.t('Copy change to proposal')} on:click={() => copy('to-proposal')}>{$i18n.t('Proposed revision')} → + {:else} change.fromA < change.toA) || + !chunk.changes.some((change) => change.fromB < change.toB) + ) + return null; + const original = a.sliceString(chunk.fromA, Math.min(chunk.toA, a.length)); + const proposed = b.sliceString(chunk.fromB, Math.min(chunk.toB, b.length)); + return { + from: chunk.fromB, + to: Math.min(chunk.toB, b.length), + insert: original + (original.endsWith('\n') ? '' : '\n') + proposed + }; +} + export function selectedChunk(chunks: readonly Chunk[], side: 'a' | 'b', position: number) { return chunks.findIndex((chunk) => { const from = side === 'a' ? chunk.fromA : chunk.fromB; diff --git a/src/lib/i18n/locales/ar-BH/translation.json b/src/lib/i18n/locales/ar-BH/translation.json index f216befd3c..d42adb5f2f 100644 --- a/src/lib/i18n/locales/ar-BH/translation.json +++ b/src/lib/i18n/locales/ar-BH/translation.json @@ -1502,6 +1502,8 @@ "JSON Spec": "المواصفات بصيغة JSON", "July": "يوليو", "June": "يونيو", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "الاحتفاظ بعنوان URL الحالي لخطاف الويب", "Keep in Sidebar": "إبقاء في الشريط الجانبي", "Keep tools listed even while your shell is closed.": "إبقاء الأدوات مدرجة حتى أثناء إغلاق الطرفية.", diff --git a/src/lib/i18n/locales/ar/translation.json b/src/lib/i18n/locales/ar/translation.json index 46f61f45db..b0c6c2a42f 100644 --- a/src/lib/i18n/locales/ar/translation.json +++ b/src/lib/i18n/locales/ar/translation.json @@ -1502,6 +1502,8 @@ "JSON Spec": "المواصفات بصيغة JSON", "July": "يوليو", "June": "يونيو", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "الاحتفاظ بعنوان URL الحالي لخطاف الويب", "Keep in Sidebar": "إبقاء في الشريط الجانبي", "Keep tools listed even while your shell is closed.": "إبقاء الأدوات مدرجة حتى أثناء إغلاق الطرفية.", diff --git a/src/lib/i18n/locales/az-AZ/translation.json b/src/lib/i18n/locales/az-AZ/translation.json index adb97f0ca6..5905d5d5c4 100644 --- a/src/lib/i18n/locales/az-AZ/translation.json +++ b/src/lib/i18n/locales/az-AZ/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON Spesifikasiyası", "July": "İyul", "June": "İyun", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Cari Webhook URL-ini saxla", "Keep in Sidebar": "Yan paneldə saxla", "Keep tools listed even while your shell is closed.": "Əmr qabığı bağlı olduqda belə alətləri siyahıda saxlayın.", diff --git a/src/lib/i18n/locales/bg-BG/translation.json b/src/lib/i18n/locales/bg-BG/translation.json index 8286ad7064..123c0f24d0 100644 --- a/src/lib/i18n/locales/bg-BG/translation.json +++ b/src/lib/i18n/locales/bg-BG/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON спецификация", "July": "Юли", "June": "Юни", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Запазване на текущия URL адрес на уебкуката", "Keep in Sidebar": "Запазване в страничната лента", "Keep tools listed even while your shell is closed.": "Инструментите остават в списъка, дори когато командната обвивка е затворена.", diff --git a/src/lib/i18n/locales/bn-BD/translation.json b/src/lib/i18n/locales/bn-BD/translation.json index 443d21b483..da91cdb67e 100644 --- a/src/lib/i18n/locales/bn-BD/translation.json +++ b/src/lib/i18n/locales/bn-BD/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON স্পেসিফিকেশন", "July": "জুলাই", "June": "জুন", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "বর্তমান ওয়েবহুক URL রাখুন", "Keep in Sidebar": "সাইডবারে রাখুন", "Keep tools listed even while your shell is closed.": "শেল বন্ধ থাকলেও টুলগুলো তালিকায় রাখুন।", diff --git a/src/lib/i18n/locales/bo-TB/translation.json b/src/lib/i18n/locales/bo-TB/translation.json index 5f1ac1613c..73e0ccabda 100644 --- a/src/lib/i18n/locales/bo-TB/translation.json +++ b/src/lib/i18n/locales/bo-TB/translation.json @@ -1437,6 +1437,8 @@ "JSON Spec": "JSON ཚད་གཞི།", "July": "ཟླ་བ་བདུན་པ།", "June": "ཟླ་བ་དྲུག་པ།", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "ད་ལྟའི་ webhook URL ཉར་བ།", "Keep in Sidebar": "གཞོགས་སྡེར་ཉར་བ།", "Keep tools listed even while your shell is closed.": "ཁྱེད་ཀྱི་བཀའ་བརྡའི་འགྲེལ་བཤད་ཆས་ཁ་བརྒྱབ་ཡོད་སྐབས་ཀྱང་ལག་ཆ་ཐོ་ནང་འཇོག་པ།", diff --git a/src/lib/i18n/locales/bs-BA/translation.json b/src/lib/i18n/locales/bs-BA/translation.json index 5c1ca6af47..9e946c1822 100644 --- a/src/lib/i18n/locales/bs-BA/translation.json +++ b/src/lib/i18n/locales/bs-BA/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "JSON specifikacija", "July": "Juli", "June": "Juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Zadrži trenutni URL webhooka", "Keep in Sidebar": "Zadrži na bočnoj traci", "Keep tools listed even while your shell is closed.": "Alati ostaju navedeni čak i dok je vaša komandna ljuska zatvorena.", diff --git a/src/lib/i18n/locales/ca-ES/translation.json b/src/lib/i18n/locales/ca-ES/translation.json index eb208bdb19..c5f9ca7378 100644 --- a/src/lib/i18n/locales/ca-ES/translation.json +++ b/src/lib/i18n/locales/ca-ES/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Especificacions del JSON", "July": "Juliol", "June": "Juny", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Mantenir l'URL actual del webhook", "Keep in Sidebar": "Mantenir a la barra lateral", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/ceb-PH/translation.json b/src/lib/i18n/locales/ceb-PH/translation.json index baa7f87ad1..0f9bc34148 100644 --- a/src/lib/i18n/locales/ceb-PH/translation.json +++ b/src/lib/i18n/locales/ceb-PH/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "Espesipikasyon sa JSON", "July": "Hulyo", "June": "Hunyo", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Hupti ang kasamtangang URL sa webhook", "Keep in Sidebar": "Hupti sa sidebar", "Keep tools listed even while your shell is closed.": "Ipadayon ang paglista sa mga himan bisan kon sirado ang imong tighubad sa mga sugo.", diff --git a/src/lib/i18n/locales/cs-CZ/translation.json b/src/lib/i18n/locales/cs-CZ/translation.json index 56099b5853..9a37e69603 100644 --- a/src/lib/i18n/locales/cs-CZ/translation.json +++ b/src/lib/i18n/locales/cs-CZ/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "Specifikace JSON", "July": "Červenec", "June": "Červen", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Zachovat aktuální URL webhooku", "Keep in Sidebar": "Ponechat v postranním panelu", "Keep tools listed even while your shell is closed.": "Ponechat nástroje v seznamu, i když je váš příkazový interpret zavřený.", diff --git a/src/lib/i18n/locales/da-DK/translation.json b/src/lib/i18n/locales/da-DK/translation.json index a40520b387..bcca9167b7 100644 --- a/src/lib/i18n/locales/da-DK/translation.json +++ b/src/lib/i18n/locales/da-DK/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON spec", "July": "Juli", "June": "Juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Behold den aktuelle webhook-URL", "Keep in Sidebar": "Behold i sidebaren", "Keep tools listed even while your shell is closed.": "Bevar værktøjer på listen, selv når din kommandofortolker er lukket.", diff --git a/src/lib/i18n/locales/de-DE/translation.json b/src/lib/i18n/locales/de-DE/translation.json index 4168c80ac8..cb3e62bb02 100644 --- a/src/lib/i18n/locales/de-DE/translation.json +++ b/src/lib/i18n/locales/de-DE/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-Spezifikation", "July": "Juli", "June": "Juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Aktuelle Webhook-URL beibehalten", "Keep in Sidebar": "In Seitenleiste anzeigen", "Keep tools listed even while your shell is closed.": "Behält Werkzeuge in der Liste, auch wenn Ihre Shell geschlossen ist.", diff --git a/src/lib/i18n/locales/dg-DG/translation.json b/src/lib/i18n/locales/dg-DG/translation.json index 936a223c28..d6d3f632a1 100644 --- a/src/lib/i18n/locales/dg-DG/translation.json +++ b/src/lib/i18n/locales/dg-DG/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "Such JSON Spec", "July": "July", "June": "June", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Keep such current webhook URL", "Keep in Sidebar": "Keep in Sidebar", "Keep tools listed even while your shell is closed.": "Keep tools listed even while such shell is closed. So handy.", diff --git a/src/lib/i18n/locales/el-GR/translation.json b/src/lib/i18n/locales/el-GR/translation.json index fbdbbb52b4..0ede1f4cd7 100644 --- a/src/lib/i18n/locales/el-GR/translation.json +++ b/src/lib/i18n/locales/el-GR/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "Προδιαγραφή JSON", "July": "Ιούλιος", "June": "Ιούνιος", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Διατήρηση τρέχοντος URL webhook", "Keep in Sidebar": "Κρατήστε στην πλευρική μπάρα", "Keep tools listed even while your shell is closed.": "Τα εργαλεία παραμένουν στη λίστα ακόμη και όταν το κέλυφός σας είναι κλειστό.", diff --git a/src/lib/i18n/locales/en-GB/translation.json b/src/lib/i18n/locales/en-GB/translation.json index b3293aa173..09c800097e 100644 --- a/src/lib/i18n/locales/en-GB/translation.json +++ b/src/lib/i18n/locales/en-GB/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "", "July": "", "June": "", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "", "Keep in Sidebar": "", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index aa5ba814cf..9b8e1765f8 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "", "July": "", "June": "", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "", "Keep in Sidebar": "", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/es-ES/translation.json b/src/lib/i18n/locales/es-ES/translation.json index 66629db827..cb3e0fb6e5 100644 --- a/src/lib/i18n/locales/es-ES/translation.json +++ b/src/lib/i18n/locales/es-ES/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Especif. JSON", "July": "Julio", "June": "Junio", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Mantener la URL actual del webhook", "Keep in Sidebar": "Mantener en Barra Lateral", "Keep tools listed even while your shell is closed.": "Mantiene las herramientas en la lista incluso mientras tu intérprete de comandos está cerrado.", diff --git a/src/lib/i18n/locales/et-EE/translation.json b/src/lib/i18n/locales/et-EE/translation.json index 9f6c7b685b..7f6f0ac8f2 100644 --- a/src/lib/i18n/locales/et-EE/translation.json +++ b/src/lib/i18n/locales/et-EE/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON spetsifikatsioon", "July": "Juuli", "June": "Juuni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Säilitage praegune webhook'i URL", "Keep in Sidebar": "Hoia külgribal", "Keep tools listed even while your shell is closed.": "Tööriistad jäävad loendisse ka siis, kui kest on suletud.", diff --git a/src/lib/i18n/locales/eu-ES/translation.json b/src/lib/i18n/locales/eu-ES/translation.json index c9bd301dd7..82b9e109c8 100644 --- a/src/lib/i18n/locales/eu-ES/translation.json +++ b/src/lib/i18n/locales/eu-ES/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON espezifikazioa", "July": "Uztaila", "June": "Ekaina", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Mantendu uneko webhook URLa", "Keep in Sidebar": "Mantendu alboko barran", "Keep tools listed even while your shell is closed.": "Mantendu tresnak zerrendan, komando-interpretea itxita dagoenean ere.", diff --git a/src/lib/i18n/locales/fa-IR/translation.json b/src/lib/i18n/locales/fa-IR/translation.json index 7896388660..059d78bf8f 100644 --- a/src/lib/i18n/locales/fa-IR/translation.json +++ b/src/lib/i18n/locales/fa-IR/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "مشخصات JSON", "July": "ژوئیه", "June": "ژوئن", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "حفظ آدرس وب\u200cهوک فعلی", "Keep in Sidebar": "نگه داشتن در نوار کناری", "Keep tools listed even while your shell is closed.": "ابزارها حتی زمانی که شل شما بسته است در فهرست باقی می\u200cمانند.", diff --git a/src/lib/i18n/locales/fi-FI/translation.json b/src/lib/i18n/locales/fi-FI/translation.json index e408070d04..c760f58717 100644 --- a/src/lib/i18n/locales/fi-FI/translation.json +++ b/src/lib/i18n/locales/fi-FI/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON Spec", "July": "heinäkuu", "June": "kesäkuu", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Käytä nykyistä webhook-verkko-osoitetta", "Keep in Sidebar": "Pidä sivupalkissa", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/fil-PH/translation.json b/src/lib/i18n/locales/fil-PH/translation.json index dd5674b6f5..8dad9f8247 100644 --- a/src/lib/i18n/locales/fil-PH/translation.json +++ b/src/lib/i18n/locales/fil-PH/translation.json @@ -1451,6 +1451,8 @@ "JSON Spec": "JSON Spec", "July": "Hulyo", "June": "Hunyo", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Panatilihin ang kasalukuyang webhook URL", "Keep in Sidebar": "Panatilihin sa Sidebar", "Keep tools listed even while your shell is closed.": "Panatilihing nakalista ang mga tool kahit sarado ang iyong shell.", diff --git a/src/lib/i18n/locales/fo-FO/translation.json b/src/lib/i18n/locales/fo-FO/translation.json index d3358d4c84..4a82d121e4 100644 --- a/src/lib/i18n/locales/fo-FO/translation.json +++ b/src/lib/i18n/locales/fo-FO/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-forskrift", "July": "juli", "June": "juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Varðveit núverandi webhook-URL", "Keep in Sidebar": "Varðveit í síðustikuni", "Keep tools listed even while your shell is closed.": "Lat amboð vera á listanum, eisini meðan Shell hjá tær er afturlatið.", diff --git a/src/lib/i18n/locales/fr-CA/translation.json b/src/lib/i18n/locales/fr-CA/translation.json index 670592c64c..45803de96b 100644 --- a/src/lib/i18n/locales/fr-CA/translation.json +++ b/src/lib/i18n/locales/fr-CA/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Spécification JSON", "July": "Juillet", "June": "Juin", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Conserver l'URL actuelle du webhook", "Keep in Sidebar": "Epingler dans la barre latérale", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/fr-FR/translation.json b/src/lib/i18n/locales/fr-FR/translation.json index 36f4de1e65..0b43b70f87 100644 --- a/src/lib/i18n/locales/fr-FR/translation.json +++ b/src/lib/i18n/locales/fr-FR/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Spécification JSON", "July": "Juillet", "June": "Juin", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Conserver l'URL actuelle du webhook", "Keep in Sidebar": "Epingler dans la barre latérale", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/gl-ES/translation.json b/src/lib/i18n/locales/gl-ES/translation.json index be73859610..78cab7780f 100644 --- a/src/lib/i18n/locales/gl-ES/translation.json +++ b/src/lib/i18n/locales/gl-ES/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "Especificación JSON", "July": "Xullo", "June": "Xuño", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Manter a URL actual do webhook", "Keep in Sidebar": "Manter na barra lateral", "Keep tools listed even while your shell is closed.": "Manter as ferramentas listadas mesmo cando o seu intérprete de ordes estea pechado.", diff --git a/src/lib/i18n/locales/he-IL/translation.json b/src/lib/i18n/locales/he-IL/translation.json index b88f35fcbc..d1eca417ea 100644 --- a/src/lib/i18n/locales/he-IL/translation.json +++ b/src/lib/i18n/locales/he-IL/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "מפרט JSON", "July": "יולי", "June": "יוני", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "השאר את כתובת ה-URL הנוכחית של ה-Webhook", "Keep in Sidebar": "הצג בסרגל הצד", "Keep tools listed even while your shell is closed.": "השארת הכלים ברשימה גם כשהמעטפת שלך סגורה.", diff --git a/src/lib/i18n/locales/hi-IN/translation.json b/src/lib/i18n/locales/hi-IN/translation.json index a97ad68529..8d798e8cb0 100644 --- a/src/lib/i18n/locales/hi-IN/translation.json +++ b/src/lib/i18n/locales/hi-IN/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON विनिर्देश", "July": "जुलाई", "June": "जून", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "वर्तमान वेबहुक URL रखें", "Keep in Sidebar": "साइडबार में रखें", "Keep tools listed even while your shell is closed.": "आपका शेल बंद होने पर भी टूल सूची में रखें.", diff --git a/src/lib/i18n/locales/hr-HR/translation.json b/src/lib/i18n/locales/hr-HR/translation.json index b32b2d8c05..8dbfee7811 100644 --- a/src/lib/i18n/locales/hr-HR/translation.json +++ b/src/lib/i18n/locales/hr-HR/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "JSON specifikacija", "July": "Srpanj", "June": "Lipanj", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Zadrži trenutačni URL webhooka", "Keep in Sidebar": "Zadrži na bočnoj traci", "Keep tools listed even while your shell is closed.": "Zadrži alate na popisu i dok je vaša ljuska zatvorena.", diff --git a/src/lib/i18n/locales/hu-HU/translation.json b/src/lib/i18n/locales/hu-HU/translation.json index 0a69cf62d5..105c3661ef 100644 --- a/src/lib/i18n/locales/hu-HU/translation.json +++ b/src/lib/i18n/locales/hu-HU/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-specifikáció", "July": "Július", "June": "Június", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Jelenlegi webhook URL megtartása", "Keep in Sidebar": "Megtartás az oldalsávon", "Keep tools listed even while your shell is closed.": "Az eszközök akkor is a listában maradnak, ha a shell be van zárva.", diff --git a/src/lib/i18n/locales/id-ID/translation.json b/src/lib/i18n/locales/id-ID/translation.json index a4dd8e5586..82e957a326 100644 --- a/src/lib/i18n/locales/id-ID/translation.json +++ b/src/lib/i18n/locales/id-ID/translation.json @@ -1449,6 +1449,8 @@ "JSON Spec": "Spesifikasi JSON", "July": "Juli", "June": "Juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Pertahankan URL webhook saat ini", "Keep in Sidebar": "Pertahankan di Bilah Sisi", "Keep tools listed even while your shell is closed.": "Tetap cantumkan alat meskipun shell Anda ditutup.", diff --git a/src/lib/i18n/locales/ie-GA/translation.json b/src/lib/i18n/locales/ie-GA/translation.json index 9a867d983d..0731bebecf 100644 --- a/src/lib/i18n/locales/ie-GA/translation.json +++ b/src/lib/i18n/locales/ie-GA/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "Sonraíocht JSON", "July": "Iúil", "June": "Meitheamh", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Coinnigh URL an chrúca gréasáin reatha", "Keep in Sidebar": "Coinnigh sa Bharra Taobh", "Keep tools listed even while your shell is closed.": "Coinnigh na huirlisí ar an liosta fiú nuair atá do bhlaosc dúnta.", diff --git a/src/lib/i18n/locales/it-IT/translation.json b/src/lib/i18n/locales/it-IT/translation.json index 15bc8c39f7..df78c01b3f 100644 --- a/src/lib/i18n/locales/it-IT/translation.json +++ b/src/lib/i18n/locales/it-IT/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Specifica JSON", "July": "Luglio", "June": "Giugno", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Mantieni l'URL del webhook attuale", "Keep in Sidebar": "Mantieni nella barra laterale", "Keep tools listed even while your shell is closed.": "Mantieni gli strumenti elencati anche quando la shell è chiusa.", diff --git a/src/lib/i18n/locales/ja-JP/translation.json b/src/lib/i18n/locales/ja-JP/translation.json index 0336c54cac..78005daf52 100644 --- a/src/lib/i18n/locales/ja-JP/translation.json +++ b/src/lib/i18n/locales/ja-JP/translation.json @@ -1449,6 +1449,8 @@ "JSON Spec": "JSON仕様", "July": "7月", "June": "6月", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "現在のウェブフックURLを保持", "Keep in Sidebar": "サイドバーに残す", "Keep tools listed even while your shell is closed.": "シェルが閉じている間もツールを一覧に残します。", diff --git a/src/lib/i18n/locales/ka-GE/translation.json b/src/lib/i18n/locales/ka-GE/translation.json index 6cc954c4eb..e9a93fa685 100644 --- a/src/lib/i18n/locales/ka-GE/translation.json +++ b/src/lib/i18n/locales/ka-GE/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-ის სპეციფიკაცია", "July": "ივლისი", "June": "ივნისი", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "მიმდინარე ვებჰუკის URL-ის შენარჩუნება", "Keep in Sidebar": "დატოვება გვერდით პანელზე", "Keep tools listed even while your shell is closed.": "ხელსაწყოებს სიაში ტოვებს მაშინაც, როცა თქვენი გარსი დახურულია.", diff --git a/src/lib/i18n/locales/kab-DZ/translation.json b/src/lib/i18n/locales/kab-DZ/translation.json index aef66aa7cd..a93b8f57c5 100644 --- a/src/lib/i18n/locales/kab-DZ/translation.json +++ b/src/lib/i18n/locales/kab-DZ/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "Ilugan s JSON", "July": "Yulyu", "June": "Yunyu", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Eǧǧ tansa URL tamirant n webhook", "Keep in Sidebar": "Senṭeḍ-it deg ufeggag n yidis", "Keep tools listed even while your shell is closed.": "Eǧǧ ifecka deg tebdart ula ma yella shell-ik·im yemdel.", diff --git a/src/lib/i18n/locales/ko-KR/translation.json b/src/lib/i18n/locales/ko-KR/translation.json index 1899c1f160..482766adf6 100644 --- a/src/lib/i18n/locales/ko-KR/translation.json +++ b/src/lib/i18n/locales/ko-KR/translation.json @@ -1438,6 +1438,8 @@ "JSON Spec": "JSON 스펙", "July": "7월", "June": "6월", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "현재 웹훅 URL 유지", "Keep in Sidebar": "사이드바에 유지", "Keep tools listed even while your shell is closed.": "셸이 닫혀 있는 동안에도 도구를 목록에 유지합니다.", diff --git a/src/lib/i18n/locales/lt-LT/translation.json b/src/lib/i18n/locales/lt-LT/translation.json index 298074981a..57fe0d6e16 100644 --- a/src/lib/i18n/locales/lt-LT/translation.json +++ b/src/lib/i18n/locales/lt-LT/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "JSON specifikacija", "July": "liepa", "June": "birželis", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Paliekamas dabartinis webhook URL adresas", "Keep in Sidebar": "Laikyti šoninėje juostoje", "Keep tools listed even while your shell is closed.": "Rodyti įrankius sąraše net tada, kai jūsų apvalkalas uždarytas.", diff --git a/src/lib/i18n/locales/lv-LV/translation.json b/src/lib/i18n/locales/lv-LV/translation.json index 03c0dedf37..038ad7164c 100644 --- a/src/lib/i18n/locales/lv-LV/translation.json +++ b/src/lib/i18n/locales/lv-LV/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "JSON specifikācija", "July": "Jūlijs", "June": "Jūnijs", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Paturēt pašreizējo webhook URL", "Keep in Sidebar": "Saglabāt sānjoslā", "Keep tools listed even while your shell is closed.": "Rādīt rīkus sarakstā arī tad, kad jūsu čaula ir aizvērta.", diff --git a/src/lib/i18n/locales/ms-MY/translation.json b/src/lib/i18n/locales/ms-MY/translation.json index fb0b6d0790..5994f8388c 100644 --- a/src/lib/i18n/locales/ms-MY/translation.json +++ b/src/lib/i18n/locales/ms-MY/translation.json @@ -1449,6 +1449,8 @@ "JSON Spec": "Spesifikasi JSON", "July": "Julai", "June": "Jun", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Kekalkan URL webhook semasa", "Keep in Sidebar": "Simpan dalam Bar Sisi", "Keep tools listed even while your shell is closed.": "Kekalkan alat dalam senarai walaupun semasa shell anda ditutup.", diff --git a/src/lib/i18n/locales/nb-NO/translation.json b/src/lib/i18n/locales/nb-NO/translation.json index 69619c32d3..d901604143 100644 --- a/src/lib/i18n/locales/nb-NO/translation.json +++ b/src/lib/i18n/locales/nb-NO/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-spesifikasjon", "July": "juli", "June": "juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Behold nåværende webhook-URL", "Keep in Sidebar": "Vis i sidepanelet", "Keep tools listed even while your shell is closed.": "Behold verktøyene i listen selv når skallet ditt er lukket.", diff --git a/src/lib/i18n/locales/nl-NL/translation.json b/src/lib/i18n/locales/nl-NL/translation.json index 1fe023b409..9bbfaf9706 100644 --- a/src/lib/i18n/locales/nl-NL/translation.json +++ b/src/lib/i18n/locales/nl-NL/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-specificatie", "July": "juli", "June": "juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Huidige webhook-URL behouden", "Keep in Sidebar": "In zijbalk houden", "Keep tools listed even while your shell is closed.": "Houd tools in de lijst, ook als je shell gesloten is.", diff --git a/src/lib/i18n/locales/pa-IN/translation.json b/src/lib/i18n/locales/pa-IN/translation.json index cee0f0be29..d52c0f5d44 100644 --- a/src/lib/i18n/locales/pa-IN/translation.json +++ b/src/lib/i18n/locales/pa-IN/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON ਨਿਰਧਾਰਨ", "July": "ਜੁਲਾਈ", "June": "ਜੂਨ", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "ਮੌਜੂਦਾ ਵੈੱਬਹੁੱਕ URL ਰੱਖੋ", "Keep in Sidebar": "ਸਾਈਡਬਾਰ ਵਿੱਚ ਰੱਖੋ", "Keep tools listed even while your shell is closed.": "ਤੁਹਾਡਾ ਸ਼ੈੱਲ ਬੰਦ ਹੋਣ 'ਤੇ ਵੀ ਟੂਲ ਸੂਚੀ ਵਿੱਚ ਰੱਖੋ।", diff --git a/src/lib/i18n/locales/pl-PL/translation.json b/src/lib/i18n/locales/pl-PL/translation.json index aeba776954..222bf28e02 100644 --- a/src/lib/i18n/locales/pl-PL/translation.json +++ b/src/lib/i18n/locales/pl-PL/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "Specyfikacja JSON", "July": "Lipiec", "June": "Czerwiec", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Zachowaj obecny adres URL webhooka", "Keep in Sidebar": "Zachowaj w pasku bocznym", "Keep tools listed even while your shell is closed.": "Pokazuj narzędzia na liście nawet wtedy, gdy powłoka jest zamknięta.", diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index 81dfc5c02a..f50abc6132 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Especificação JSON", "July": "Julho", "June": "Junho", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Manter a URL atual do webhook", "Keep in Sidebar": "Manter na barra lateral", "Keep tools listed even while your shell is closed.": "Mantenha as ferramentas listadas mesmo com o shell fechado.", diff --git a/src/lib/i18n/locales/pt-PT/translation.json b/src/lib/i18n/locales/pt-PT/translation.json index be22333af5..2cc3a28dd5 100644 --- a/src/lib/i18n/locales/pt-PT/translation.json +++ b/src/lib/i18n/locales/pt-PT/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Especificação JSON", "July": "Julho", "June": "Junho", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Manter o URL do webhook atual", "Keep in Sidebar": "Manter na Barra Lateral", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/ro-RO/translation.json b/src/lib/i18n/locales/ro-RO/translation.json index 5162469b50..1aa7fab859 100644 --- a/src/lib/i18n/locales/ro-RO/translation.json +++ b/src/lib/i18n/locales/ro-RO/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "Specificație JSON", "July": "Iulie", "June": "Iunie", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Păstrează URL-ul curent al webhook-ului", "Keep in Sidebar": "Păstrează în bara laterală", "Keep tools listed even while your shell is closed.": "Păstrează instrumentele listate chiar și când shell-ul dumneavoastră este închis.", diff --git a/src/lib/i18n/locales/ru-RU/translation.json b/src/lib/i18n/locales/ru-RU/translation.json index 4293669a9d..d2651ea9cf 100644 --- a/src/lib/i18n/locales/ru-RU/translation.json +++ b/src/lib/i18n/locales/ru-RU/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "JSON-спецификация", "July": "Июль", "June": "Июнь", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Сохранить текущий URL вебхука", "Keep in Sidebar": "Оставить на боковой панели", "Keep tools listed even while your shell is closed.": "Оставлять инструменты в списке, даже когда командная строка закрыта.", diff --git a/src/lib/i18n/locales/sk-SK/translation.json b/src/lib/i18n/locales/sk-SK/translation.json index 364c060ab4..9708dc53f3 100644 --- a/src/lib/i18n/locales/sk-SK/translation.json +++ b/src/lib/i18n/locales/sk-SK/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "Špecifikácia JSON", "July": "Júl", "June": "Jún", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Ponechať aktuálnu URL adresu webhooku", "Keep in Sidebar": "Ponechať v bočnom paneli", "Keep tools listed even while your shell is closed.": "Ponechať nástroje v zozname, aj keď je váš shell zatvorený.", diff --git a/src/lib/i18n/locales/sl-SI/translation.json b/src/lib/i18n/locales/sl-SI/translation.json index bff5394cb7..235178721c 100644 --- a/src/lib/i18n/locales/sl-SI/translation.json +++ b/src/lib/i18n/locales/sl-SI/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "JSON Spec", "July": "julij", "June": "junij", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Ohrani trenutni URL spletnega klica", "Keep in Sidebar": "Obdrži v stranski vrstici", "Keep tools listed even while your shell is closed.": "Orodja ohrani na seznamu tudi, ko je vaša lupina zaprta.", diff --git a/src/lib/i18n/locales/sr-RS/translation.json b/src/lib/i18n/locales/sr-RS/translation.json index 41b81d63ed..860a7f8795 100644 --- a/src/lib/i18n/locales/sr-RS/translation.json +++ b/src/lib/i18n/locales/sr-RS/translation.json @@ -1463,6 +1463,8 @@ "JSON Spec": "JSON спецификација", "July": "Јул", "June": "Јун", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Задржите тренутни URL вебхука", "Keep in Sidebar": "Задржи на бочној траци", "Keep tools listed even while your shell is closed.": "Задржава алате на листи чак и док је ваша љуска затворена.", diff --git a/src/lib/i18n/locales/sv-SE/translation.json b/src/lib/i18n/locales/sv-SE/translation.json index 798582824f..6eee61dcae 100644 --- a/src/lib/i18n/locales/sv-SE/translation.json +++ b/src/lib/i18n/locales/sv-SE/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON-specifikation", "July": "juli", "June": "juni", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Behåll aktuell webhook-URL", "Keep in Sidebar": "Behåll i sidofältet", "Keep tools listed even while your shell is closed.": "Behåll verktygen i listan även när ditt skal är stängt.", diff --git a/src/lib/i18n/locales/ta-IN/translation.json b/src/lib/i18n/locales/ta-IN/translation.json index 10a1297f0b..dd9c713399 100644 --- a/src/lib/i18n/locales/ta-IN/translation.json +++ b/src/lib/i18n/locales/ta-IN/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON விவரக்குறிப்பு", "July": "ஜூலை", "June": "ஜூன்", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "தற்போதைய வெப்ஹூக் URL ஐ வைத்திருக்கவும்", "Keep in Sidebar": "பக்கப்பட்டியில் வைக்கவும்", "Keep tools listed even while your shell is closed.": "உங்கள் ஷெல் மூடப்பட்டிருக்கும்போதும் கருவிகளைப் பட்டியலில் வைத்திருக்கவும்.", diff --git a/src/lib/i18n/locales/th-TH/translation.json b/src/lib/i18n/locales/th-TH/translation.json index a56a18fd0c..512360b967 100644 --- a/src/lib/i18n/locales/th-TH/translation.json +++ b/src/lib/i18n/locales/th-TH/translation.json @@ -1437,6 +1437,8 @@ "JSON Spec": "สเปก JSON", "July": "กรกฎาคม", "June": "มิถุนายน", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "คง URL ของ Webhook ปัจจุบันไว้", "Keep in Sidebar": "ปักหมุดไว้ที่แถบด้านข้าง", "Keep tools listed even while your shell is closed.": "แสดงเครื่องมือในรายการไว้แม้ขณะที่ Shell ของคุณปิดอยู่", diff --git a/src/lib/i18n/locales/tk-TM/translation.json b/src/lib/i18n/locales/tk-TM/translation.json index 850ca11774..34e0e3e212 100644 --- a/src/lib/i18n/locales/tk-TM/translation.json +++ b/src/lib/i18n/locales/tk-TM/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON Spesifikasiýasy", "July": "Iýul", "June": "Iýun", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Häzirki webhook URL salgysyny saklaň", "Keep in Sidebar": "Gapdal Panelde Sakla", "Keep tools listed even while your shell is closed.": "Shell-iňiz ýapyk wagty hem gurallary sanawda saklaň.", diff --git a/src/lib/i18n/locales/tr-TR/translation.json b/src/lib/i18n/locales/tr-TR/translation.json index 2d3989d716..c701047306 100644 --- a/src/lib/i18n/locales/tr-TR/translation.json +++ b/src/lib/i18n/locales/tr-TR/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON Belirtimi", "July": "Temmuz", "June": "Haziran", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Mevcut webhook URL'sini koru", "Keep in Sidebar": "Kenar Çubuğunda Tut", "Keep tools listed even while your shell is closed.": "Kabuğunuz kapalıyken bile araçları listede tut.", diff --git a/src/lib/i18n/locales/ug-CN/translation.json b/src/lib/i18n/locales/ug-CN/translation.json index e534ccbc2c..8c571f18a9 100644 --- a/src/lib/i18n/locales/ug-CN/translation.json +++ b/src/lib/i18n/locales/ug-CN/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON ئۆلچىمى", "July": "ئىيۇل", "June": "ئىيۇن", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "نۆۋەتتىكى Webhook URL نى ساقلاپ قېلىڭ", "Keep in Sidebar": "يانتاختىدا ساقلا", "Keep tools listed even while your shell is closed.": "shell يېپىلغان ۋاقىتتىمۇ قوراللارنى تىزىملىكتە ساقلاڭ.", diff --git a/src/lib/i18n/locales/uk-UA/translation.json b/src/lib/i18n/locales/uk-UA/translation.json index 4dba863d31..9b2df3680c 100644 --- a/src/lib/i18n/locales/uk-UA/translation.json +++ b/src/lib/i18n/locales/uk-UA/translation.json @@ -1476,6 +1476,8 @@ "JSON Spec": "Специфікація JSON", "July": "Липень", "June": "Червень", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Зберегти поточну URL-адресу вебхука", "Keep in Sidebar": "Залишити на бічній панелі", "Keep tools listed even while your shell is closed.": "Залишати інструменти в списку, навіть коли ваш командний рядок закрито.", diff --git a/src/lib/i18n/locales/ur-PK/translation.json b/src/lib/i18n/locales/ur-PK/translation.json index cc74bc3d43..faaacd2e61 100644 --- a/src/lib/i18n/locales/ur-PK/translation.json +++ b/src/lib/i18n/locales/ur-PK/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "JSON تصریح", "July": "جولائی", "June": "جون", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "موجودہ ویب ہک URL برقرار رکھیں", "Keep in Sidebar": "سائیڈ بار میں رکھیں", "Keep tools listed even while your shell is closed.": "آپ کا شیل بند ہونے پر بھی ٹولز فہرست میں رکھیں۔", diff --git a/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json b/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json index f6c508f53c..92ef1b4d0e 100644 --- a/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json +++ b/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "", "July": "июл", "June": "июн", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "", "Keep in Sidebar": "", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/uz-Latn-UZ/translation.json b/src/lib/i18n/locales/uz-Latn-UZ/translation.json index 04aa6e8f39..8baa1f392a 100644 --- a/src/lib/i18n/locales/uz-Latn-UZ/translation.json +++ b/src/lib/i18n/locales/uz-Latn-UZ/translation.json @@ -1450,6 +1450,8 @@ "JSON Spec": "", "July": "iyul", "June": "iyun", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "", "Keep in Sidebar": "", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/vi-VN/translation.json b/src/lib/i18n/locales/vi-VN/translation.json index 8e32a719b4..6758737aaf 100644 --- a/src/lib/i18n/locales/vi-VN/translation.json +++ b/src/lib/i18n/locales/vi-VN/translation.json @@ -1437,6 +1437,8 @@ "JSON Spec": "Đặc tả JSON", "July": "Tháng 7", "June": "Tháng 6", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "Giữ URL webhook hiện tại", "Keep in Sidebar": "Giữ trên thanh bên", "Keep tools listed even while your shell is closed.": "Vẫn liệt kê các tool ngay cả khi shell của bạn đã đóng.", diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index b03e17f0fa..e6078169fc 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -1447,6 +1447,8 @@ "JSON Spec": "以 JSON 格式定义的接口规范", "July": "七月", "June": "六月", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "保留当前 webhook URL", "Keep in Sidebar": "保留在侧边栏", "Keep tools listed even while your shell is closed.": "", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index 51d1a6536e..4ee0d7af6c 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -1437,6 +1437,8 @@ "JSON Spec": "以 JSON 格式定義的介面規範", "July": "7 月", "June": "6 月", + "Keep both": "", + "Keep both versions in the proposed revision, original first.": "", "Keep current webhook URL": "保留目前的 Webhook 網址", "Keep in Sidebar": "保留在側邊欄", "Keep tools listed even while your shell is closed.": "", From 44953df70ed758a6321f7a2e139c93aca29129f1 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:15:17 +0200 Subject: [PATCH 02/20] Merge pull request #32421 from Classic298/channel-message-meta refac: keep channel message metadata server-side --- backend/open_webui/routers/channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 633ceb1d70..6ef96c8062 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1462,6 +1462,8 @@ async def post_new_message( db: AsyncSession = Depends(get_async_session), ): await check_channels_access(request, user) + # Message metadata is set by the server only. + form_data.meta = None try: message, channel = await new_message_handler(request, id, form_data, user, db) @@ -1769,6 +1771,7 @@ async def update_message_by_id( if user.role != 'admin' and message.user_id != user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + form_data.meta = None try: await Messages.update_message_by_id(message_id, form_data, db=db) message = await Messages.get_message_by_id(message_id, db=db) From 8bddf9bc62c3ffcaa3e6433bc5ac0f1d04a94421 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:15:25 +0200 Subject: [PATCH 03/20] fix: memory reindex ignores the Embedding Concurrent Requests limit (#32420) Reindexing memories (the admin "Reindex Knowledge and Memory Vectors" button, a user resetting their memory vectors, or the automatic reindex after switching to an embedding model with a different vector size) ignored the "Embedding Concurrent Requests" setting and sent one embedding request per memory at once. With a rate-limited embedding provider, most of those requests came back as 429 errors. Reindexing memories now respects that setting and the embedding batch size, like knowledge file uploads already do. Fixes #32212 --- backend/open_webui/routers/memories.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index 2ed11e6094..af95faca73 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import logging from typing import Literal @@ -142,15 +141,11 @@ async def reindex_memory_vectors_for_user( if not memories: return 0 - vectors = await asyncio.gather( - *[ - request.app.state.EMBEDDING_FUNCTION( - memory_vector_text(memory.content, memory.path), - prefix=RAG_EMBEDDING_CONTENT_PREFIX, - user=user, - ) - for memory in memories - ] + # One list call so batching and the concurrency limit apply. + vectors = await request.app.state.EMBEDDING_FUNCTION( + [memory_vector_text(memory.content, memory.path) for memory in memories], + prefix=RAG_EMBEDDING_CONTENT_PREFIX, + user=user, ) await ASYNC_VECTOR_DB_CLIENT.upsert( @@ -488,10 +483,9 @@ async def reset_memory_from_vector_db( """Reset user's memory vector embeddings. CRITICAL: We intentionally do NOT use Depends(get_async_session) here. - This endpoint generates embeddings for ALL user memories in parallel using - asyncio.gather(). A user with 100 memories would trigger 100 embedding API - calls simultaneously. With a session held, this could block a connection - for MINUTES, completely exhausting the connection pool. + This endpoint generates embeddings for ALL user memories. With a session + held, this could block a connection for MINUTES, completely exhausting the + connection pool. """ await check_memories_permission(user) From 77fd8aea613c42e0d590804c7f9fdcf6f5f4af02 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:18:56 +0200 Subject: [PATCH 04/20] fix: some file browser actions ignore the chat's own terminal workspace in Per chat mode (#32418) With an orchestrator connection in Admin > Integrations > Open Terminal set to Terminal Contexts > Chat = "Per chat", several file browser actions ran in the shared terminal workspace, outside the chat's own one. Opening an HTML file showed "File not found", notebooks could not run, saving an edited file and New File wrote to the shared workspace so the change never showed up in the chat, and deleting several selected files or downloading a folder or a selection as a zip failed. All of these now use the chat's own workspace, like the rest of the file browser. Fixes #29965 --- src/lib/components/chat/FileNav.svelte | 45 ++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 12b015a3da..1e821dc173 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -389,7 +389,7 @@ let terminalChatContextPending = false; let terminalChatContextHidden = false; let isPerChatTerminal = false; - $: portPreviewBaseUrl = + $: previewBaseUrl = selectedTerminal && isPerChatTerminal && chatId ? `${selectedTerminal.url}/chats/${encodeURIComponent(chatId)}` : (selectedTerminal?.url ?? ''); @@ -1033,7 +1033,12 @@ // Directories end with '/', downloaded as a ZIP archive const isDir = path.endsWith('/'); const result = isDir - ? await archiveFromTerminal(terminal.url, terminal.key, [path.replace(/\/$/, '')]) + ? await archiveFromTerminal( + terminal.url, + terminal.key, + [path.replace(/\/$/, '')], + chatId ?? undefined + ) : await downloadFileBlob(terminal.url, terminal.key, path, chatId ?? undefined); if (!result) { toast.error($i18n.t('Download failed')); @@ -1218,7 +1223,13 @@ if (!terminal) return; const emptyFile = new File([''], name, { type: 'application/octet-stream' }); - const result = await uploadToTerminal(terminal.url, terminal.key, currentPath, emptyFile); + const result = await uploadToTerminal( + terminal.url, + terminal.key, + currentPath, + emptyFile, + chatId ?? undefined + ); toast[result ? 'success' : 'error']( result ? $i18n.t('File created') : $i18n.t('Failed to create file') ); @@ -1387,7 +1398,12 @@ const paths = [...selectedEntries]; let ok = 0; for (const p of paths) { - const result = await deleteEntry(terminal.url, terminal.key, p.replace(/\/$/, '')); + const result = await deleteEntry( + terminal.url, + terminal.key, + p.replace(/\/$/, ''), + chatId ?? undefined + ); if (result) ok++; } toast[ok > 0 ? 'success' : 'error']( @@ -1415,7 +1431,12 @@ const toastId = toast.loading($i18n.t('Preparing download...')); try { // Archive everything into a single ZIP - const result = await archiveFromTerminal(terminal.url, terminal.key, paths); + const result = await archiveFromTerminal( + terminal.url, + terminal.key, + paths, + chatId ?? undefined + ); if (!result) { toast.error($i18n.t('Download failed')); return; @@ -1907,7 +1928,7 @@ > {#if previewPort !== null} { @@ -1945,7 +1966,7 @@ const DOMPurify = (await import('dompurify')).default; fileOfficeHtml = DOMPurify.sanitize(result.html); }} - baseUrl={selectedTerminal?.url ?? ''} + baseUrl={previewBaseUrl} apiKey={selectedTerminal?.key ?? ''} overlay={overlay || isDraggingHandle} onSave={async (content) => { @@ -1954,7 +1975,13 @@ const fileName = selectedFile.split('/').pop() ?? 'file'; const dir = selectedFile.substring(0, selectedFile.lastIndexOf('/') + 1) || '/'; const file = new File([content], fileName, { type: 'text/plain' }); - const result = await uploadToTerminal(terminal.url, terminal.key, dir, file); + const result = await uploadToTerminal( + terminal.url, + terminal.key, + dir, + file, + chatId ?? undefined + ); toast[result ? 'success' : 'error']( result ? $i18n.t('File saved') : $i18n.t('Failed to save file') ); @@ -2192,7 +2219,7 @@ {#key JSON.stringify([chatId, selectedTerminal.url])} { From f35b1993271e6f0cf1ffc55375d634a65c9e0ab1 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:19:06 +0200 Subject: [PATCH 05/20] fix: voice calls and dictation go silent on Android Chrome after about ten uses (#32417) Voice calls and dictation on Android Chrome stopped working after about ten uses in one session: calls hung on "Listening" and spoken replies went silent until the page was reloaded. Each use left the microphone level display holding on to an audio output, and Chrome on Android allows only ten of those per page. The level display now releases it when a turn ends and when a call or recording stops. Fixes #29969 --- .../components/chat/MessageInput/CallOverlay.svelte | 12 +++++++++++- .../chat/MessageInput/VoiceRecording.svelte | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/MessageInput/CallOverlay.svelte b/src/lib/components/chat/MessageInput/CallOverlay.svelte index 2004964f6a..12c8834a76 100644 --- a/src/lib/components/chat/MessageInput/CallOverlay.svelte +++ b/src/lib/components/chat/MessageInput/CallOverlay.svelte @@ -46,6 +46,7 @@ let hasStartedSpeaking = false; let mediaRecorder; let audioStream = null; + let audioContext: AudioContext | undefined; let audioChunks = []; let destroyed = false; let avatarView: VoiceAvatar | undefined; @@ -358,6 +359,12 @@ } }; + // Chrome on Android caps open audio outputs at 10. + const closeAudioContext = () => { + void audioContext?.close(); + audioContext = undefined; + }; + const stopAudioStream = async () => { try { if (mediaRecorder) { @@ -367,6 +374,8 @@ console.log('Error stopping audio stream:', error); } + closeAudioContext(); + if (!audioStream) return; audioStream.getAudioTracks().forEach(function (track) { @@ -387,7 +396,8 @@ }; const analyseAudio = (stream) => { - const audioContext = new AudioContext(); + closeAudioContext(); + audioContext = new AudioContext(); const audioStreamSource = audioContext.createMediaStreamSource(stream); const analyser = audioContext.createAnalyser(); diff --git a/src/lib/components/chat/MessageInput/VoiceRecording.svelte b/src/lib/components/chat/MessageInput/VoiceRecording.svelte index 50e239705a..de82130681 100644 --- a/src/lib/components/chat/MessageInput/VoiceRecording.svelte +++ b/src/lib/components/chat/MessageInput/VoiceRecording.svelte @@ -87,6 +87,7 @@ }; let stream; + let audioContext: AudioContext | null = null; let speechRecognition; let mediaRecorder; @@ -116,8 +117,14 @@ return Math.min(1.0, Math.max(0.01, scaledRMS)); }; + // Chrome on Android caps open audio outputs at 10. + const closeAudioContext = () => { + void audioContext?.close(); + audioContext = null; + }; + const analyseAudio = (stream) => { - const audioContext = new AudioContext(); + audioContext = new AudioContext(); const audioStreamSource = audioContext.createMediaStreamSource(stream); const analyser = audioContext.createAnalyser(); @@ -399,6 +406,7 @@ } stream = null; + closeAudioContext(); }; const confirmRecording = async () => { @@ -418,6 +426,7 @@ } stream = null; + closeAudioContext(); }; let resizeObserver; @@ -463,6 +472,7 @@ window.removeEventListener('keydown', handleKeyDown); document.removeEventListener('visibilitychange', handleVisibilityChange); releaseWakeLock(); + closeAudioContext(); // remove resize observer resizeObserver.disconnect(); }); From 7b47c3b22ce983d57f4a13073748a935c4614889 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 14 Oct 2026 23:21:22 +0400 Subject: [PATCH 06/20] refac --- backend/open_webui/routers/openai.py | 4 +- backend/open_webui/tools/builtin.py | 4 +- backend/open_webui/utils/middleware.py | 61 +++++++++++++-------- backend/open_webui/utils/misc.py | 21 +++++--- backend/open_webui/utils/tool_search.py | 71 +++++++++++++++++++++++-- 5 files changed, 125 insertions(+), 36 deletions(-) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 8548a8ab0d..0f59316524 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -42,7 +42,7 @@ from open_webui.utils.anthropic import ANTHROPIC_VERSION, get_anthropic_models, from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.headers import get_headers_and_cookies, include_user_info_headers from open_webui.utils.json_codec import JSONCodec -from open_webui.utils.misc import convert_logit_bias_input_to_json +from open_webui.utils.misc import convert_logit_bias_input_to_json, get_provider_function from open_webui.utils.model_ids import strip_provider_model_prefix from open_webui.utils.payload import ( apply_model_params_to_body_openai, @@ -1190,6 +1190,8 @@ def _normalize_stored_item(item: dict) -> dict: if allowed is None: # Unknown type — pass through as-is (e.g. reasoning, extension items). return item + if item_type == 'function_call': + item = {**item, **get_provider_function(item)} return {k: v for k, v in item.items() if k in allowed} diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index b3eb6c68a1..297b3b055c 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -3837,10 +3837,12 @@ async def search_tools( """ Search the tools listed in and return their full definitions. Pass the exact tool name when you already know it. + Invoke a result with call_tool(tool_name, params), where params is a JSON-encoded object + matching the tool's parameters. Do not call deferred tools directly by name. :param query: Keywords describing the capability you need (e.g. "jira create issue"), or an exact tool name :param count: Maximum number of results to return (default: 5, max: 20) - :return: JSON with the definitions of the matching tools, which can then be called by name + :return: JSON with the definitions of the matching tools, which can then be invoked through call_tool """ from open_webui.utils.tool_search import search_deferred_tools diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 0afeffa277..5e9fc24d43 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -145,7 +145,7 @@ from open_webui.utils.task import ( tools_function_calling_generation_template, ) from open_webui.utils.tool_approval import complete_tool_call, pending_tool_calls -from open_webui.utils.tool_search import apply_tool_search +from open_webui.utils.tool_search import apply_tool_search, resolve_tool_call from open_webui.utils.tools import ( connect_mcp_server, get_attached_knowledge, @@ -3290,7 +3290,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): metadata['tools'] = tools_dict if metadata.get('params', {}).get('function_calling') != 'legacy': - deferred = await apply_tool_search(form_data, metadata, tools_dict) + deferred = await apply_tool_search(form_data, metadata, tools_dict, inlet_filter_tools) # If the function calling is native, then call the tools function calling handler form_data['tools'] = [ @@ -3438,6 +3438,9 @@ async def build_chat_response_context(request, form_data, user, model, metadata, async def execute_tool_call(form_data, metadata, event_caller, tool_call): + error = resolve_tool_call(tool_call, metadata.get('tools', {})) + if error: + return {}, error, None, None, False name = tool_call.get('function', {}).get('name', '') tool_args = tool_call.get('function', {}).get('arguments', '{}') params = {} @@ -5981,7 +5984,41 @@ async def streaming_chat_response_handler(response, ctx): ): tool_call_iterations += 1 - response_tool_calls = tool_calls.pop(0) + # Resolve dispatch before approvals and special handling such as ask_user. + response_tool_calls = [] + output_by_call_id = { + item.get('call_id'): item for item in output if item.get('type') == 'function_call' + } + for tool_call in tool_calls.pop(0): + error = resolve_tool_call(tool_call, metadata.get('tools', {})) + call_id = tool_call.get('id') or output_id('fc') + tool_call['id'] = call_id + function = tool_call.get('function', {}) + item = output_by_call_id.get(call_id) + if item is None: + item = {'type': 'function_call', 'id': call_id, 'call_id': call_id} + output.append(item) + output_by_call_id[call_id] = item + item.update( + name=function.get('name', ''), + arguments=function.get('arguments', '{}'), + status='failed' if error else item.get('status', 'in_progress'), + ) + if 'provider_function' in tool_call: + item['provider_function'] = tool_call['provider_function'] + if error: + output.append( + { + 'type': 'function_call_output', + 'id': output_id('fco'), + 'call_id': call_id, + 'output': [{'type': 'input_text', 'text': error}], + 'status': 'failed', + } + ) + else: + response_tool_calls.append(tool_call) + ask_user_staged, ask_user_error = stage_ask_user_tool_calls(response_tool_calls, output, output_id) if ask_user_error: response_tool_calls = [ @@ -6006,24 +6043,6 @@ async def streaming_chat_response_handler(response, ctx): if item.get('name') == 'ask_user' and item.get('status') == 'pending': item['status'] = 'in_progress' - # Append function_call items for each tool call - # (Responses API already has them from streaming, so skip duplicates) - existing_call_ids = {item.get('call_id') for item in output if item.get('type') == 'function_call'} - for tc in response_tool_calls: - call_id = tc.get('id', '') - if call_id not in existing_call_ids: - func = tc.get('function', {}) - output.append( - { - 'type': 'function_call', - 'id': call_id or output_id('fc'), - 'call_id': call_id, - 'name': func.get('name', ''), - 'arguments': func.get('arguments', '{}'), - 'status': 'in_progress', - } - ) - tool_approval_mode = metadata.get('params', {}).get('tool_approval_mode', 'full') if ( response_tool_calls diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 9cc979117a..b6be078bda 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -336,6 +336,18 @@ def get_reasoning_details(payload: dict): return payload.get('reasoning_details') or provider_details +def get_provider_function(item: dict) -> dict: + """Restore the model's call when OWUI displays and executes a resolved target instead.""" + function = item.get('provider_function') + if not isinstance(function, dict): + function = item + arguments = function.get('arguments', '{}') + return { + 'name': function.get('name', ''), + 'arguments': arguments if isinstance(arguments, str) else JSONCodec.dumps(arguments), + } + + def convert_output_to_messages( output: list, raw: bool = False, @@ -502,18 +514,11 @@ def convert_output_to_messages( continue # Collect tool calls to batch into assistant message - arguments = item.get('arguments', '{}') - # Ensure arguments is always a JSON string - if not isinstance(arguments, str): - arguments = JSONCodec.dumps(arguments) pending_tool_calls.append( { 'id': item.get('call_id', ''), 'type': 'function', - 'function': { - 'name': item.get('name', ''), - 'arguments': arguments, - }, + 'function': get_provider_function(item), } ) diff --git a/backend/open_webui/utils/tool_search.py b/backend/open_webui/utils/tool_search.py index 8bff23e62c..96dbdca6f8 100644 --- a/backend/open_webui/utils/tool_search.py +++ b/backend/open_webui/utils/tool_search.py @@ -1,7 +1,7 @@ """ Tool search: large tool schemas are left out of the provider `tools` array and listed by name in an `` system prompt block. `search_tools` returns the full definitions of matching tools, -which the model then calls by name. The `tools` array never changes during a chat, so prompt caches hold. +which the model invokes through `call_tool`. Discovery does not change the provider `tools` array. """ import fnmatch @@ -14,6 +14,7 @@ from open_webui.utils.json_codec import JSONCodec from open_webui.utils.misc import add_or_update_system_message SEARCH_TOOL_NAME = 'search_tools' +CALL_TOOL_NAME = 'call_tool' MANIFEST_DESCRIPTION_MAX_CHARS = 100 MAX_SEARCH_COUNT = 20 DEFAULT_SEARCH_COUNT = 5 @@ -66,15 +67,20 @@ def build_deferred_tools_manifest(tools_dict: dict[str, dict], deferred: list[st '\n' 'The following tools are available but their definitions are not loaded. ' f'To use one, call `{SEARCH_TOOL_NAME}` with a short keyword query or the exact tool name to get its ' - 'definition, then call the tool by name with the parameters it defines. ' + f'definition, then use `{CALL_TOOL_NAME}` with `tool_name` set to its exact name and `params` set ' + 'to a JSON-encoded object containing its arguments. Do not call deferred tools directly. ' + 'For example: call_tool(tool_name="get_weather", params=\'{"city":"Dubai"}\'). ' 'Never tell the user a capability is unavailable without searching first.\n' f'{entries}' ) -async def apply_tool_search(form_data: dict, metadata: dict, tools_dict: dict[str, dict]) -> set[str]: - """Add search_tools and the manifest when tools can be deferred; returns the names to leave out of `tools`.""" - config = SEARCH_TOOL_NAME not in tools_dict and await get_tool_search_config() +async def apply_tool_search( + form_data: dict, metadata: dict, tools_dict: dict[str, dict], extra_tools: list[dict] | None = None +) -> set[str]: + """Add discovery and dispatch when tools can be deferred; return names to omit from provider tools.""" + names = set(tools_dict) | {(tool.get('function') or tool).get('name') for tool in extra_tools or []} + config = not names.intersection({SEARCH_TOOL_NAME, CALL_TOOL_NAME}) and await get_tool_search_config() deferred = select_deferred_tools(tools_dict, config) if config else [] if not deferred: return set() @@ -100,9 +106,64 @@ async def apply_tool_search(form_data: dict, metadata: dict, tools_dict: dict[st 'spec': get_builtin_tool_spec(search_tools), 'type': 'builtin', } + # Middleware resolves this before approvals and execution, preserving the target tool's behavior. + tools_dict[CALL_TOOL_NAME] = { + 'tool_id': f'builtin:{CALL_TOOL_NAME}', + 'type': 'builtin', + 'spec': { + 'name': CALL_TOOL_NAME, + 'description': ( + 'Invoke a tool found by search_tools. Use its exact name and encode its arguments as a JSON ' + 'object string matching the returned definition. Use "{}" for a tool without arguments.' + ), + 'strict': True, + 'parameters': { + 'type': 'object', + 'properties': { + 'tool_name': {'type': 'string', 'description': 'Exact tool name returned by search_tools.'}, + 'params': {'type': 'string', 'description': 'JSON-encoded object of arguments for the tool.'}, + }, + 'required': ['tool_name', 'params'], + 'additionalProperties': False, + }, + }, + } return set(deferred) +def resolve_tool_call(tool_call: dict, tools: dict[str, dict]) -> str | None: + """Unwrap our dispatcher in place, retaining the provider call for history replay. Never execute here.""" + function = tool_call.get('function', {}) + if ( + function.get('name') != CALL_TOOL_NAME + or tools.get(CALL_TOOL_NAME, {}).get('tool_id') != f'builtin:{CALL_TOOL_NAME}' + ): + return None + + try: + arguments = JSONCodec.loads(function.get('arguments', '')) + if not isinstance(arguments, dict) or set(arguments) != {'tool_name', 'params'}: + raise ValueError('call_tool requires tool_name and params.') + name = arguments['tool_name'] + if not isinstance(name, str) or not name.strip(): + raise ValueError('tool_name must be a non-empty string.') + if name in {SEARCH_TOOL_NAME, CALL_TOOL_NAME}: + raise ValueError('call_tool cannot invoke search_tools or itself.') + if name not in tools: + raise ValueError(f'Tool "{name}" is not available. Search for an available tool.') + if not isinstance(arguments['params'], str): + raise ValueError('params must be a JSON-encoded object string.') + params = JSONCodec.loads(arguments['params']) + if not isinstance(params, dict): + raise ValueError('params must decode to a JSON object.') + except (TypeError, ValueError) as error: + return f'Error: {error}' + + tool_call['provider_function'] = dict(function) + tool_call['function'] = {'name': name, 'arguments': JSONCodec.dumps(params)} + return None + + def strip_deferred_tools_manifest(system_prompt: str | None) -> str | None: """Remove the manifest from a system prompt that is reused by a request which builds its own.""" return _MANIFEST_RE.sub('', system_prompt) if system_prompt else system_prompt From 6276e81cec38f5c5cce067e4be905c5710c2d7d6 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:25:28 +0200 Subject: [PATCH 07/20] fix: SCIM can delete admin accounts (#32415) An identity provider could delete an admin account through SCIM, including the first admin account, for example when its sync pruned users it no longer had assigned. SCIM already ignores deactivation requests for admins so a sync cannot demote them, and the admin panel refuses to delete the first admin, but a SCIM delete request still went through and could leave the instance without anyone who has admin access. Deleting an admin through SCIM is now refused with a 403 and the account stays; admins are removed from the admin panel instead. Fixes #27662 --- backend/open_webui/routers/scim.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index 886ec521a9..9b290c9ee4 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -870,6 +870,13 @@ async def delete_user( detail=f'User {user_id} not found', ) + # An IdP prune must not lock the instance out by deleting an admin. + if user.role == 'admin': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + success = await Auths.delete_auth_by_id(user_id, db=db) if not success: raise HTTPException( From 40bf4a1b99aa0aae316095eff100c07570c542da Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 14 Oct 2026 23:30:00 +0400 Subject: [PATCH 08/20] refac --- .../components/common/FileItemModal.svelte | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/lib/components/common/FileItemModal.svelte b/src/lib/components/common/FileItemModal.svelte index a4f4606e4c..1a11cc61ca 100644 --- a/src/lib/components/common/FileItemModal.svelte +++ b/src/lib/components/common/FileItemModal.svelte @@ -261,21 +261,16 @@
{ - if (item.type === 'file' || safeLinkUrl(item.url)) { - let fileId = item?.id ?? item?.tempId; - window.open( - item.type === 'file' - ? item?.url?.startsWith('http') - ? item.url - : `${WEBUI_API_BASE_URL}/files/${fileId}/content` - : item.url, - '_blank' - ); - } - }} > {item?.name ?? 'File'} From 81f5b8adf80e49bd2213390b59dc867074c3738e Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:54:41 +0200 Subject: [PATCH 09/20] feat: support MinerU 4.x self-hosted servers (#32422) MinerU 4.0 dropped the parsing interface its self-hosted server had in 3.x, so the local MinerU engine failed with a 404 on every document when pointed at a 4.x server. Open WebUI now detects a 4.x server and talks to it through its new interface, while 3.x servers keep working as before. On 4.x, `tier` in the MinerU parameters picks the parsing quality (flash, basic, standard or advanced) and `enable_ocr` forces OCR; 3.x-only settings such as `language` and `model_version` are not sent, since 4.x refuses them. 4.x also returns every image as base64 text inside the document, which would otherwise be indexed as text, so images are left out of the extracted content. Fixes #31516 --- .../open_webui/retrieval/loaders/mineru.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/backend/open_webui/retrieval/loaders/mineru.py b/backend/open_webui/retrieval/loaders/mineru.py index 4459515dc2..0a3017cbd1 100644 --- a/backend/open_webui/retrieval/loaders/mineru.py +++ b/backend/open_webui/retrieval/loaders/mineru.py @@ -1,5 +1,6 @@ import logging import os +import re import tempfile import time import zipfile @@ -11,6 +12,10 @@ from langchain_core.documents import Document log = logging.getLogger(__name__) +# The V1 API embeds every image as a base64 data URI, which is not text worth indexing +MARKDOWN_IMAGE = re.compile(r'!\[([^\[\]\n]*)\]\(data:[^()\s]*\)') +HTML_IMAGE = re.compile(r']*>', re.IGNORECASE) + class MinerULoader: """ @@ -44,6 +49,7 @@ class MinerULoader: self.enable_table = self.params.get('enable_table', True) self.language = self.params.get('language', 'en') self.model_version = self.params.get('model_version', 'pipeline') + self.tier = self.params.get('tier') self.page_ranges = self.params.pop('page_ranges', '') @@ -63,6 +69,8 @@ class MinerULoader: try: if self.api_mode == 'cloud': return self._load_cloud_api() + elif self._is_v1_api(): + return self._load_v1_api() else: return self._load_local_api() except Exception as e: @@ -175,6 +183,116 @@ class MinerULoader: return [Document(page_content=markdown_content, metadata=metadata)] + def _is_v1_api(self) -> bool: + """ + Detect MinerU 4.x, whose V1 API replaced the /file_parse endpoint of 3.x. + """ + try: + return requests.get(f'{self.api_url}/v1/health', timeout=10).ok + except requests.RequestException: + return False + + def _load_v1_api(self) -> List[Document]: + """ + Load document using the MinerU 4.x V1 API (asynchronous). + Uploads the file, submits a parse job, polls it and downloads the markdown. + """ + log.info('Using MinerU V1 API at %s', self.api_url) + + filename = os.path.basename(self.file_path) + + upload = self._request_v1_api( + 'POST', + '/v1/uploads', + json={ + 'filename': filename, + 'bytes': os.path.getsize(self.file_path), + 'mime_type': 'application/octet-stream', + }, + ).json() + with open(self.file_path, 'rb') as f: + self._request_v1_api('PUT', f'/v1/uploads/{upload["id"]}/content', data=f) + upload = self._request_v1_api('POST', f'/v1/uploads/{upload["id"]}/complete').json() + + file_entry = {'source': {'type': 'file_id', 'file_id': upload['file']['id']}} + if self.page_ranges and filename.lower().endswith('.pdf'): + file_entry['page_range'] = self.page_ranges + + # The job rejects unknown fields, so the 3.x params are not passed through + job = self._request_v1_api( + 'POST', + '/v1/parse/jobs', + json={ + 'files': [file_entry], + 'tier': self.tier, + 'ocr_mode': 'ocr' if self.enable_ocr else 'auto', + }, + ).json() + + log.info('Polling MinerU V1 job: %s', job['job_id']) + deadline = time.monotonic() + self.timeout if self.timeout else None + while job['status'] in ['queued', 'running']: + if deadline and time.monotonic() > deadline: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail=f'MinerU processing timed out after {self.timeout} seconds', + ) + time.sleep(2) + job = self._request_v1_api('GET', f'/v1/parse/jobs/{job["job_id"]}').json() + + file_result = job['files'][0] + if job['status'] != 'completed': + error = file_result.get('error') or {} + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'MinerU processing {job["status"]}: {error.get("message", "Unknown error")}', + ) + + markdown_id = file_result['output_files']['markdown']['file_id'] + markdown_content = self._request_v1_api('GET', f'/v1/files/{markdown_id}/content').content.decode('utf-8') + markdown_content = HTML_IMAGE.sub('', MARKDOWN_IMAGE.sub(r'\1', markdown_content)) + + if not markdown_content: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail='MinerU returned empty markdown content', + ) + + log.info('Successfully parsed document with MinerU V1 API: %s', filename) + + metadata = { + 'source': filename, + 'api_mode': 'local', + 'tier': job['tier'], + 'version': (file_result.get('parse') or {}).get('parser_version', 'unknown'), + } + + return [Document(page_content=markdown_content, metadata=metadata)] + + def _request_v1_api(self, method: str, path: str, **kwargs) -> requests.Response: + """ + Send one request to the MinerU V1 API and raise an HTTPException on failure. + """ + try: + response = requests.request(method, f'{self.api_url}{path}', timeout=self.timeout, **kwargs) + response.raise_for_status() + except requests.Timeout: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail='MinerU V1 API request timed out', + ) + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'MinerU V1 API request failed: {e} - {e.response.text}', + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error calling MinerU V1 API: {str(e)}', + ) + return response + def _load_cloud_api(self) -> List[Document]: """ Load document using Cloud API (asynchronous). From de446952c2db1168dca5c6e3da6d961e6f55a8eb Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 14 Oct 2026 23:58:58 +0400 Subject: [PATCH 10/20] refac --- backend/open_webui/main.py | 14 +++++++++----- backend/open_webui/utils/automations.py | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 863d8be4a0..5db490fbf2 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1813,7 +1813,7 @@ async def chat_completion( # Fan out: one task per model if metadata.get('session_id') and metadata.get('chat_id'): - task_ids = [] + chat_tasks = {} subagent_results = [] is_internal = getattr(request.state, 'internal', False) is True chat_id = metadata['chat_id'] @@ -1869,13 +1869,13 @@ async def chat_completion( subagent_results.append(await process) continue - task_id, _ = await create_task( + task_id, task = await create_task( request.app.state.redis, process, id=chat_id, task_id=per_model_metadata['task_id'], ) - task_ids.append(task_id) + chat_tasks[task_id] = task if is_internal: return { @@ -1886,7 +1886,7 @@ async def chat_completion( } # Emit chat:active=true - if task_ids: + if chat_tasks: event_emitter = await get_event_emitter( {**metadata, 'message_id': message_ids[0]['message_id']}, update_db=False, @@ -1895,9 +1895,13 @@ async def chat_completion( folder_id = metadata.get('folder_id') or await Chats.get_chat_folder_id(chat_id, user.id) await event_emitter({'type': 'chat:active', 'data': {'active': True, 'folder_id': folder_id}}) + # Automation chat runs must finish before their caller records success. + if metadata.get('automation_id') and is_saved_chat_id(chat_id): + await asyncio.gather(*chat_tasks.values()) + return { 'status': True, - 'task_ids': task_ids, + 'task_ids': list(chat_tasks), 'chat_id': chat_id, } else: diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index 0bda3a3d1b..ce4c9af3ec 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -357,6 +357,7 @@ async def execute_automation(app, automation: AutomationModel) -> None: session_id + chat_id + message_id → async task → pipeline handles everything (filters, model params, knowledge/RAG, tools, DB saves, webhooks). """ + chat = None try: user = await Users.get_user_by_id(automation.user_id) if not user: @@ -521,6 +522,12 @@ async def execute_automation(app, automation: AutomationModel) -> None: request = _build_request(app, token=token) await app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user) + reply = await Chats.get_message_by_id_and_message_id(chat.id, assistant_msg_id) + if reply and reply.get('error'): + raise ValueError(str(reply['error']['content'])) + if not reply or not reply.get('done'): + raise ValueError('Automation reply did not complete') + # Notify user from open_webui.socket.main import sio @@ -544,16 +551,18 @@ async def execute_automation(app, automation: AutomationModel) -> None: data={'name': automation.name, 'chat_id': chat.id}, ) - except Exception as e: + except (Exception, asyncio.CancelledError) as e: log.exception(f'Automation {automation.id} failed') - error = str(e)[:4000] - await _record_run(automation.id, 'error', error=error) + error = 'Automation reply was cancelled' if isinstance(e, asyncio.CancelledError) else str(e)[:4000] + await _record_run(automation.id, 'error', chat_id=chat.id if chat else None, error=error) await publish_event( app, EVENTS.AUTOMATION_RUN_FAILED, subject_id=automation.id, - data={'name': automation.name, 'error': error}, + data={'name': automation.name, 'error': error, **({'chat_id': chat.id} if chat else {})}, ) + if isinstance(e, asyncio.CancelledError): + raise #################### From 3bc82bb2c110fc3ed7064e6107a4731b8d9ab5ae Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:59:57 +0200 Subject: [PATCH 11/20] feat: allow a custom TLS certificate for Milvus connections (#32427) Open WebUI could not connect to a Milvus server that uses a self-signed or company-issued TLS certificate, because there was no way to tell it which certificate to trust. The new MILVUS_SERVER_PEM_PATH setting takes the path to that certificate file and uses it for the Milvus connection, in both normal and multi-tenancy mode. Leaving it unset keeps the current behaviour. Fixes #31632 --- backend/open_webui/config.py | 1 + backend/open_webui/retrieval/vector/dbs/milvus.py | 7 +++++-- .../open_webui/retrieval/vector/dbs/milvus_multitenancy.py | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index d3f77d3472..210eafcb71 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -603,6 +603,7 @@ if VECTOR_DB == 'mariadb-vector': MILVUS_URI = os.getenv('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db') MILVUS_DB = os.getenv('MILVUS_DB', 'default') MILVUS_TOKEN = os.getenv('MILVUS_TOKEN', None) +MILVUS_SERVER_PEM_PATH = os.getenv('MILVUS_SERVER_PEM_PATH', '') MILVUS_INDEX_TYPE = os.getenv('MILVUS_INDEX_TYPE', 'HNSW') MILVUS_METRIC_TYPE = os.getenv('MILVUS_METRIC_TYPE', 'COSINE') MILVUS_HNSW_M = int(os.getenv('MILVUS_HNSW_M', '16')) diff --git a/backend/open_webui/retrieval/vector/dbs/milvus.py b/backend/open_webui/retrieval/vector/dbs/milvus.py index 2eefb8f67f..b0787e145c 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus.py @@ -17,6 +17,7 @@ from open_webui.config import ( MILVUS_INDEX_TYPE, MILVUS_IVF_FLAT_NLIST, MILVUS_METRIC_TYPE, + MILVUS_SERVER_PEM_PATH, MILVUS_TOKEN, MILVUS_URI, ) @@ -244,9 +245,11 @@ class MilvusClient(VectorDBBase): def __init__(self): self.collection_prefix = 'open_webui' if MILVUS_TOKEN is None: - self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB) + self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, server_pem_path=MILVUS_SERVER_PEM_PATH) else: - self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN) + self.client = Client( + uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN, server_pem_path=MILVUS_SERVER_PEM_PATH + ) if ENABLE_DB_MIGRATIONS: collections = { collection_name_full.removesuffix(BM25_STAGING_SUFFIX) diff --git a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py index b6306b88a5..cca6a44415 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py @@ -14,6 +14,7 @@ from open_webui.config import ( MILVUS_INDEX_TYPE, MILVUS_IVF_FLAT_NLIST, MILVUS_METRIC_TYPE, + MILVUS_SERVER_PEM_PATH, MILVUS_TOKEN, MILVUS_URI, ) @@ -76,7 +77,9 @@ class MilvusClient(VectorDBBase): def __init__(self): # Milvus collection names can only contain numbers, letters, and underscores. self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace('-', '_') - self.client = Client(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB) + self.client = Client( + uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB, server_pem_path=MILVUS_SERVER_PEM_PATH + ) # Main collection types for multi-tenancy self.MEMORY_COLLECTION = f'{self.collection_prefix}_memories' From 980225e5823808de7ace86a8ee73a0b880b37023 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:00:08 +0200 Subject: [PATCH 12/20] feat: tag Valkey vector store connections as Open WebUI (#32428) On a shared Valkey or ElastiCache, Open WebUI's vector store connections identified themselves only as the generic GLIDE Python client, the same as every other app using that library, so operators could not tell its traffic apart in server-side logs. Both connections now report the library name as GlidePySync(open-webui). The tag needs valkey-glide-sync 2.5.2 or newer, so the pinned install version moves from 2.3.1 to 2.5.2, and an older manual install fails at startup until it is upgraded. Fixes #31654 --- backend/open_webui/retrieval/vector/dbs/valkey.py | 4 +++- backend/requirements.txt | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/retrieval/vector/dbs/valkey.py b/backend/open_webui/retrieval/vector/dbs/valkey.py index bf202a22c0..c315a0a5fb 100644 --- a/backend/open_webui/retrieval/vector/dbs/valkey.py +++ b/backend/open_webui/retrieval/vector/dbs/valkey.py @@ -56,7 +56,7 @@ def _import_glide(): ) except ImportError as e: raise ImportError( - 'valkey-glide-sync is required when VECTOR_DB=valkey. Install it with: pip install valkey-glide-sync==2.3.1' + 'valkey-glide-sync is required when VECTOR_DB=valkey. Install it with: pip install valkey-glide-sync==2.5.2' ) from e return { 'Batch': Batch, @@ -193,6 +193,7 @@ class ValkeyClient(VectorDBBase): database_id=db if db else None, request_timeout=5000, client_name='open_webui_vector_store_client', + client_info_tag='open-webui', ) try: self.client = GlideClient.create(config) @@ -206,6 +207,7 @@ class ValkeyClient(VectorDBBase): database_id=db if db else None, request_timeout=10000, # 10s — HNSW indexing can take 1-4s per vector client_name='open_webui_vector_store_batch_client', + client_info_tag='open-webui', ) try: self.batch_client = GlideClient.create(batch_config) diff --git a/backend/requirements.txt b/backend/requirements.txt index e26cd08c67..c5c95fa427 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -116,7 +116,7 @@ pgvector==0.4.2 PyMySQL==1.2.0 boto3==1.42.62 # mariadb==1.1.14 should be added if you want to support MariaDB -# valkey-glide-sync==2.3.1 # optional: install manually if VECTOR_DB=valkey +# valkey-glide-sync==2.5.2 # optional: install manually if VECTOR_DB=valkey pymilvus==2.6.14 qdrant-client==1.18.0 diff --git a/pyproject.toml b/pyproject.toml index 202bf0a352..78f256eff8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "pycrdt==0.14.8", "redis==8.0.1", "hiredis==3.4.2", - # "valkey-glide-sync==2.3.1", # optional: install manually if VECTOR_DB=valkey + # "valkey-glide-sync==2.5.2", # optional: install manually if VECTOR_DB=valkey "pytz==2026.2", From fb3d92a6ef56b5a8a6820f615f6e6294c44eaad7 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:00:19 +0200 Subject: [PATCH 13/20] feat: record the requested model as its own field in the audit log (#32429) The audit log only showed which model a request used inside the logged request body, which is cut off at MAX_BODY_LOG_SIZE, so the model went missing whenever a long prompt came before it. Audit entries for chat and completion requests, including the OpenAI, Ollama and Anthropic-compatible endpoints, now carry a model_id field with the model the client asked for, at every AUDIT_LOG_LEVEL and however long the request is. Other requests log it as null. Fixes #30097 --- backend/open_webui/main.py | 2 ++ backend/open_webui/routers/ollama.py | 7 +++++++ backend/open_webui/routers/openai.py | 5 +++++ backend/open_webui/utils/audit.py | 2 ++ backend/open_webui/utils/logger.py | 1 + 5 files changed, 17 insertions(+) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 5db490fbf2..c9540eeb9d 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1152,6 +1152,7 @@ async def chat_completion( await get_all_models(request, user=user) model_id = form_data.get('model', None) + request.state.model_id = model_id model_item = form_data.pop('model_item', {}) tasks = form_data.pop('background_tasks', None) @@ -2059,6 +2060,7 @@ async def generate_messages( Anthropic's x-api-key header (via middleware translation). """ requested_model = form_data.get('model', '') + request.state.model_id = requested_model input_tokens = None try: input_tokens = await openai.count_anthropic_tokens(request, form_data, user) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index fd5bf331af..2d2b0fe397 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -1033,6 +1033,7 @@ async def generate_completion( if not await Config.get('ollama.enable'): raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + request.state.model_id = form_data.model await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) await validate_ollama_backend_idx(request, form_data.model, url_idx, user) @@ -1164,6 +1165,8 @@ async def generate_chat_completion( payload.pop('metadata', None) model_id = payload['model'] + if not hasattr(request.state, 'model_id'): + request.state.model_id = model_id model_info = await Models.get_model_by_id(model_id) if model_info is not None: @@ -1266,6 +1269,7 @@ async def generate_openai_completion( payload = form_data.model_dump(exclude_none=True, exclude=['metadata']) model_id = form_data.model + request.state.model_id = model_id model_info = await Models.get_model_by_id(model_id) if model_info is not None: if model_info.base_model_id: @@ -1370,6 +1374,7 @@ async def generate_openai_chat_completion( payload = form_data.model_dump(exclude_none=True, exclude=['metadata']) model_id = form_data.model + request.state.model_id = model_id model_info = await Models.get_model_by_id(model_id) if model_info is not None: if model_info.base_model_id: @@ -1427,6 +1432,7 @@ async def generate_anthropic_messages( payload = {**form_data} model_id = payload.get('model', '') + request.state.model_id = model_id model_info = await Models.get_model_by_id(model_id) if model_info: @@ -1485,6 +1491,7 @@ async def generate_responses( payload = form_data.model_dump() model_id = form_data.model + request.state.model_id = model_id model_info = await Models.get_model_by_id(model_id) if model_info: diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 0f59316524..ee96e53c99 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -1434,6 +1434,9 @@ async def generate_chat_completion( metadata = payload.pop('metadata', None) model_id = form_data.get('model') + # keep the model the client asked for when reached from /api/chat/completions + if not hasattr(request.state, 'model_id'): + request.state.model_id = model_id model_info = await Models.get_model_by_id(model_id) # Check model info and override the payload @@ -1821,6 +1824,7 @@ async def responses( idx = 0 model_id = form_data.model + request.state.model_id = model_id # Enforce per-model access control await check_model_access(user, await Models.get_model_by_id(model_id), BYPASS_MODEL_ACCESS_CONTROL) @@ -1944,6 +1948,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): idx = 0 model_id = payload.get('model') if isinstance(payload, dict) else None + request.state.model_id = model_id if model_id: models = request.app.state.OPENAI_MODELS if not models or model_id not in models: diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py index aace99dd54..64a3345844 100644 --- a/backend/open_webui/utils/audit.py +++ b/backend/open_webui/utils/audit.py @@ -43,6 +43,7 @@ class AuditLogEntry: request_uri: str user_agent: Optional[str] = None source_ip: Optional[str] = None + model_id: Optional[str] = None # `Request` audit level properties request_object: Any = None # `Request Response` level @@ -309,6 +310,7 @@ class AuditLoggingMiddleware: response_status_code=context.metadata.get('response_status_code', None), source_ip=request.client.host if request.client else None, user_agent=request.headers.get('user-agent'), + model_id=getattr(request.state, 'model_id', None), request_object=request_body, response_object=response_body, ) diff --git a/backend/open_webui/utils/logger.py b/backend/open_webui/utils/logger.py index f54b7f9840..3ab517f396 100644 --- a/backend/open_webui/utils/logger.py +++ b/backend/open_webui/utils/logger.py @@ -156,6 +156,7 @@ def file_format(record: 'Record'): 'response_status_code': record['extra'].get('response_status_code', 0), 'source_ip': record['extra'].get('source_ip', ''), 'user_agent': record['extra'].get('user_agent', ''), + 'model_id': record['extra'].get('model_id', ''), 'request_object': record['extra'].get('request_object', b''), 'response_object': record['extra'].get('response_object', b''), 'extra': record['extra'].get('extra', {}), From bae22752d3995f573a373ee1ae4735b746d952a9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:00:41 +0200 Subject: [PATCH 14/20] feat: add {{MODEL_ID}} placeholder for custom connection headers (#32426) Custom headers on connections and tool servers could pass the chat, message, user and task to downstream services, but not which model the request was for. Chat requests can now use {{MODEL_ID}}, which holds the ID of the model handling the request; for a workspace model that is the workspace model's own ID. In a multi-model chat each model's request carries its own ID, so downstream services can route by model or log usage per model. Fixes #30006 --- backend/open_webui/utils/headers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/open_webui/utils/headers.py b/backend/open_webui/utils/headers.py index 8b73ca40d8..00262df0f1 100644 --- a/backend/open_webui/utils/headers.py +++ b/backend/open_webui/utils/headers.py @@ -144,6 +144,7 @@ def parse_custom_headers( '{{FILE_NAME}}': metadata.get('file_name', '') or '', '{{FILE_CONTENT_TYPE}}': metadata.get('file_content_type', '') or '', '{{TASK}}': metadata.get('task', '') or '', + '{{MODEL_ID}}': metadata.get('model_id', '') or '', '{{USER_ID}}': (user.id if user else '') or '', '{{USER_NAME}}': (user.name.strip() if user else '') or '', '{{USER_EMAIL}}': (user.email.strip() if user else '') or '', From 209b15b9633c63ce1b7cf3c1fdc6e032d67978c6 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:01:10 +0200 Subject: [PATCH 15/20] feat: keep socket.io long polling out of the audit log (#32425) With websocket support turned off, the browser falls back to HTTP long polling and every poll was written to the audit log as its own entry, flooding it. The same traffic over a websocket was never audited, and AUDIT_EXCLUDED_PATHS only matches API routes, so there was no way to filter it out. Polling requests are now skipped as well, so the audit log looks the same whichever transport is in use. Fixes #31871 --- backend/open_webui/utils/audit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py index 64a3345844..e5ff7564c1 100644 --- a/backend/open_webui/utils/audit.py +++ b/backend/open_webui/utils/audit.py @@ -255,6 +255,9 @@ class AuditLoggingMiddleware: if request.method not in self.audited_methods: return True + if request.url.path.startswith('/ws/socket.io'): + return True + path = request.url.path.lower() for endpoint in self.ALWAYS_LOG_ENDPOINTS: if path.startswith(endpoint): From 30f3c49fb02c22269b42fe3b310209926ee7e8f9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:11:01 +0200 Subject: [PATCH 16/20] refac: normalise collection names when matching retrieval collections (#32435) Retrieval now matches a collection name in a normalised form, folding letter case together and treating hyphens and underscores alike, so the same collection is identified consistently however its name is written. --- backend/open_webui/retrieval/utils.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 61cca0a441..e5a61d7ce5 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -1318,20 +1318,22 @@ async def filter_accessible_collections( validated = set() for name in safe_names: - if name == 'knowledge-bases': + # Some vector stores fold case and _/- together, so authorize a name as the collection it can resolve to. + canonical = name.lower().replace('_', '-') + if canonical == 'knowledge-bases': # System meta-collection — never exposed to non-admins. continue - elif name.startswith('file-'): - file_id = name[len('file-') :] + elif canonical.startswith('file-'): + file_id = canonical[len('file-') :] if await has_access_to_file(file_id=file_id, access_type=access_type, user=user): validated.add(name) - elif name.startswith('user-memory-'): - if name == f'user-memory-{user.id}': + elif canonical.startswith('user-memory-'): + if canonical == f'user-memory-{user.id}': validated.add(name) - elif name.startswith('web-search-'): + elif canonical.startswith('web-search-'): # Ephemeral per-query collections, owner-bound: process_web_search mints # them as web-search-{user.id}-, so only the creator may read/write. - if name.startswith(f'web-search-{user.id}-'): + if canonical.startswith(f'web-search-{user.id}-'): validated.add(name) else: # May be a knowledge-base ID or a legacy/ephemeral collection. @@ -1340,9 +1342,9 @@ async def filter_accessible_collections( # ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS: # False (default) — deny (closes the unscoped namespace) # True — allow (preserves legacy behaviour) - if await Knowledges.check_access_by_user_id(name, user.id, permission=access_type): + if await Knowledges.check_access_by_user_id(canonical, user.id, permission=access_type): validated.add(name) - elif ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS and not await Knowledges.get_knowledge_by_id(name): + elif ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS and not await Knowledges.get_knowledge_by_id(canonical): # Not a KB at all — legacy/ephemeral collection, allow validated.add(name) return validated From 5fde0485a5adda046d283416a5c4c7a543d7a5fe Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:28:23 +0200 Subject: [PATCH 17/20] fix: an approved knowledge or file tool loses its sources (#32439) With tool approval set to ask, a knowledge or file tool the user approved ran and answered, but no sources were saved on the reply or shown below it. The same call without approval listed them as usual. The sources of an approved call are now saved and shown the same way. Fixes #32436 --- backend/open_webui/utils/middleware.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 5e9fc24d43..ea4b4e7681 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -441,6 +441,9 @@ def _split_tool_calls( return expanded +CITATION_TOOL_NAMES = ('fetch_url', 'view_file', 'view_knowledge_file', 'query_knowledge_files', 'query_chat_files') + + def get_citation_source_from_tool_result( tool_name: str, tool_params: dict, tool_result: str, tool_id: str = '' ) -> list[dict]: @@ -3622,6 +3625,14 @@ async def resume_tool_calls(request, form_data, user, model, metadata, message) request, name, result, tool_type, direct_tool, metadata, user ) await terminal_event_handler(name, params, result, event_emitter) + citations_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get( + 'citations', True + ) + if event_emitter and citations_enabled and name in CITATION_TOOL_NAMES and result: + for source in get_citation_source_from_tool_result( + tool_name=name, tool_params=params, tool_result=result, tool_id=tool.get('tool_id', '') + ): + await event_emitter({'type': 'source', 'data': source}) content = tool_result_content(result) item['arguments'] = tool_call.get('function', {}).get('arguments', '{}') output_parts = [{'type': 'input_text', 'text': content}] @@ -6138,18 +6149,7 @@ async def streaming_chat_response_handler(response, ctx): ) # Extract citation sources from tool results - if ( - citations_enabled - and tool_function_name - in [ - 'fetch_url', - 'view_file', - 'view_knowledge_file', - 'query_knowledge_files', - 'query_chat_files', - ] - and tool_result - ): + if citations_enabled and tool_function_name in CITATION_TOOL_NAMES and tool_result: try: citation_sources = get_citation_source_from_tool_result( tool_name=tool_function_name, From 5e2748a03edd2459d07fcfea168daf740092bf94 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:34:41 -0400 Subject: [PATCH 18/20] fix: hovering a sidebar folder or chat always shows that item's preview (#32438) --- .../components/layout/Sidebar/ChatItem.svelte | 2 +- .../layout/Sidebar/RecursiveFolder.svelte | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 4b6cf6b2fd..675082cf4c 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -673,7 +673,7 @@ }} > + let closeActiveHoverPreview = null; + +