Merge pull request #32431 from open-webui/dev
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true USE_CUDA_VER=cu126 free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true USE_CUDA_VER=cu126 free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
Release / publish (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Release to PyPI / release (push) Waiting to run

0.12.2
This commit is contained in:
Tim Baek 2026-10-14 16:07:16 -05:00 • committed by GitHub
commit 6a18585ff4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 647 additions and 122 deletions

View file

@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.12.2] - 2026-10-14
### Added
- 📑 **MinerU 4.x servers.** The MinerU document engine now works with self-hosted MinerU 4.x servers, where every document failed with a 404 because 4.x removed the old parsing interface; 3.x servers work as before, 4.x servers take a tier (flash, basic, standard or advanced) and enable_ocr in the MinerU parameters while language and model_version stay 3.x only, and images that 4.x returns inside the document text are left out of the indexed content. [#32422](https://github.com/open-webui/open-webui/pull/32422), [Commit](https://github.com/open-webui/open-webui/commit/81f5b8adf80e49bd2213390b59dc867074c3738e), [#31516](https://github.com/open-webui/open-webui/issues/31516)
- 🤝 **Keep both changes when comparing.** The Compare revision view now has a Keep both button that puts the original and the proposed text of the selected change into the proposed revision, original first, so you no longer have to pick one side. [Commit](https://github.com/open-webui/open-webui/commit/97b7ed73c84767611d3a26d594c36f63619d92ea)
- 📚 **Open attached knowledge from its preview.** Clicking the name in the preview of an attached knowledge collection now opens that collection in the workspace, and file and link names are real links that open in a new tab. [Commit](https://github.com/open-webui/open-webui/commit/40bf4a1b99aa0aae316095eff100c07570c542da)
- 🏷️ **Model ID in custom headers.** Custom headers on connections and tool servers can now use {{MODEL_ID}}, which holds the ID of the model handling the chat request, the workspace model's own ID for a workspace model and each model's own ID in a multi-model chat, so downstream services can route or log usage per model. [#32426](https://github.com/open-webui/open-webui/pull/32426), [Commit](https://github.com/open-webui/open-webui/commit/bae22752d3995f573a373ee1ae4735b746d952a9), [#30006](https://github.com/open-webui/open-webui/issues/30006)
- 🧾 **Requested model in the audit log.** Audit log 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, where the model only appeared in the request body and went missing when MAX_BODY_LOG_SIZE cut a long prompt; other requests log it as null. [#32429](https://github.com/open-webui/open-webui/pull/32429), [Commit](https://github.com/open-webui/open-webui/commit/fb3d92a6ef56b5a8a6820f615f6e6294c44eaad7), [#30097](https://github.com/open-webui/open-webui/issues/30097)
- 🔐 **Custom TLS certificate for Milvus.** Open WebUI can now connect to a Milvus server with a self-signed or company-issued TLS certificate by pointing the new "MILVUS_SERVER_PEM_PATH" environment variable at the certificate file, in normal and multi-tenancy mode. [#32427](https://github.com/open-webui/open-webui/pull/32427), [Commit](https://github.com/open-webui/open-webui/commit/3bc82bb2c110fc3ed7064e6107a4731b8d9ab5ae), [#31632](https://github.com/open-webui/open-webui/issues/31632)
- 🧭 **call_tool for Tool Search.** With Tool Search on, the model now gets a new call_tool tool and runs a tool it found with search_tools through it, passing the tool's name and its arguments, instead of calling the deferred tool directly by a name that is not in the request's tool list, which strict providers rejected; approvals and other tool behavior stay the same. [Commit](https://github.com/open-webui/open-webui/commit/7b47c3b22ce983d57f4a13073748a935c4614889), [#32243](https://github.com/open-webui/open-webui/issues/32243)
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
### Fixed
- 🔏 **Channel message metadata kept server side.** Channel messages posted or edited through the API no longer take their metadata from the request, so a user can no longer make a message appear to come from a channel webhook or a model. [#32421](https://github.com/open-webui/open-webui/pull/32421), [Commit](https://github.com/open-webui/open-webui/commit/44953df70ed758a6321f7a2e139c93aca29129f1)
- 👮 **SCIM cannot delete admins.** An identity provider can no longer delete administrator accounts, including the first admin, through a SCIM delete request, which now answers with 403 and leaves the account in place, so a sync that prunes unassigned users can no longer leave the instance without an administrator; admins are removed in the admin panel. [#32415](https://github.com/open-webui/open-webui/pull/32415), [Commit](https://github.com/open-webui/open-webui/commit/6276e81cec38f5c5cce067e4be905c5710c2d7d6), [#27662](https://github.com/open-webui/open-webui/issues/27662)
- 🗝️ **Collection names checked as stored.** Retrieval access checks now compare collection names the way vector stores resolve them, ignoring letter case and treating hyphens and underscores alike, so a name written differently can no longer pass the check as something other than the collection that is then read. [#32435](https://github.com/open-webui/open-webui/pull/32435), [Commit](https://github.com/open-webui/open-webui/commit/30f3c49fb02c22269b42fe3b310209926ee7e8f9)
- 📱 **Voice on Android Chrome.** Voice calls and dictation on Android Chrome no longer go silent or hang on Listening after about ten uses until the page is reloaded, since the microphone level display now releases its audio output after each turn. [#32417](https://github.com/open-webui/open-webui/pull/32417), [Commit](https://github.com/open-webui/open-webui/commit/f35b1993271e6f0cf1ffc55375d634a65c9e0ab1), [#29969](https://github.com/open-webui/open-webui/issues/29969)
- 🖥️ **Per chat terminal file actions.** With the Open Terminal chat context set to Per chat, opening HTML files, running notebooks, saving edited files, creating new files, deleting several selected files and downloading a folder or selection as a zip now use the chat's own workspace, where HTML files showed "File not found" and saved files never appeared in the chat. [#32418](https://github.com/open-webui/open-webui/pull/32418), [Commit](https://github.com/open-webui/open-webui/commit/77fd8aea613c42e0d590804c7f9fdcf6f5f4af02), [#29965](https://github.com/open-webui/open-webui/issues/29965)
- 🧮 **Memory reindex rate limits.** Reindexing memories, from the admin Reindex Knowledge and Memory Vectors button, a user resetting their memory vectors or the automatic reindex after an embedding model change, now follows the Embedding Concurrent Requests limit and batch size, where it sent one request per memory at once and rate-limited providers failed most of them with 429 errors. [#32420](https://github.com/open-webui/open-webui/pull/32420), [Commit](https://github.com/open-webui/open-webui/commit/8bddf9bc62c3ffcaa3e6433bc5ac0f1d04a94421), [#32212](https://github.com/open-webui/open-webui/issues/32212)
- ⏰ **Automation run results.** An automation run is now recorded as successful only after its reply finishes, and a reply that fails or is cancelled is recorded as an error that links to the chat, where runs counted as successful as soon as the reply started. [Commit](https://github.com/open-webui/open-webui/commit/de446952c2db1168dca5c6e3da6d961e6f55a8eb)
- 📜 **Long polling kept out of the audit log.** With websocket support turned off, every socket.io long polling request is no longer written to the audit log as its own entry, which flooded it and could not be filtered with AUDIT_EXCLUDED_PATHS, so the audit log looks the same with either transport. [#32425](https://github.com/open-webui/open-webui/pull/32425), [Commit](https://github.com/open-webui/open-webui/commit/209b15b9633c63ce1b7cf3c1fdc6e032d67978c6), [#31871](https://github.com/open-webui/open-webui/issues/31871)
- 📎 **Sources of approved tools.** With tool approval set to ask, a knowledge or file tool the user approved now saves and shows its sources below the reply, where they were lost after approval. [#32439](https://github.com/open-webui/open-webui/pull/32439), [Commit](https://github.com/open-webui/open-webui/commit/5fde0485a5adda046d283416a5c4c7a543d7a5fe), [#32436](https://github.com/open-webui/open-webui/issues/32436)
- 🗂️ **Sidebar hover previews.** Moving the pointer from one sidebar folder to the next no longer leaves the first folder's preview open or keeps the next one from opening, and folder and chat previews now also open when the pointer rests on the padding around a name. [#32438](https://github.com/open-webui/open-webui/pull/32438), [Commit](https://github.com/open-webui/open-webui/commit/5e2748a03edd2459d07fcfea168daf740092bf94), [#32437](https://github.com/open-webui/open-webui/issues/32437)
### Changed
- 🔖 **Valkey connections tagged as Open WebUI.** Open WebUI's Valkey vector store connections now report their library name as GlidePySync(open-webui), so operators of a shared Valkey or ElastiCache can tell its traffic apart; this needs valkey-glide-sync 2.5.2 or newer, so installs that use VECTOR_DB=valkey with an older manually installed version must upgrade it, or startup fails. [#32428](https://github.com/open-webui/open-webui/pull/32428), [Commit](https://github.com/open-webui/open-webui/commit/980225e5823808de7ace86a8ee73a0b880b37023), [#31654](https://github.com/open-webui/open-webui/issues/31654)
## [0.12.1] - 2026-10-14
### Added

View file

@ -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'))

View file

@ -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)
@ -1813,7 +1814,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 +1870,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 +1887,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 +1896,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:
@ -2055,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)

View file

@ -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'<img\b[^<>]*>', 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).

View file

@ -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}-<hash>, 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

View file

@ -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)

View file

@ -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'

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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}
@ -1432,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
@ -1819,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)
@ -1942,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:

View file

@ -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(

View file

@ -3837,10 +3837,12 @@ async def search_tools(
"""
Search the tools listed in <available_tools> 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

View file

@ -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
@ -254,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):
@ -309,6 +313,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,
)

View file

@ -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
####################

View file

@ -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 '',

View file

@ -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', {}),

View file

@ -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,
@ -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]:
@ -3290,7 +3293,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 +3441,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 = {}
@ -3619,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}]
@ -5981,7 +5995,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 +6054,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
@ -6119,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,

View file

@ -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),
}
)

View file

@ -1,7 +1,7 @@
"""
Tool search: large tool schemas are left out of the provider `tools` array and listed by name in an
`<available_tools>` 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
'<available_tools>\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}</available_tools>'
)
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

View file

@ -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

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.12.1",
"version": "0.12.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.12.1",
"version": "0.12.2",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",

View file

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.12.1",
"version": "0.12.2",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",

View file

@ -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",

View file

@ -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}
<PortPreview
baseUrl={portPreviewBaseUrl}
baseUrl={previewBaseUrl}
port={previewPort}
overlay={overlay || isDraggingHandle}
onClose={() => {
@ -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])}
<PortList
baseUrl={selectedTerminal.url}
previewBaseUrl={portPreviewBaseUrl}
{previewBaseUrl}
apiKey={selectedTerminal.key}
{chatId}
on:previewPort={(e) => {

View file

@ -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();

View file

@ -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();
});

View file

@ -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<typeof chunkOverview> = [];
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 @@
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="diff-editor flex h-full min-h-0 flex-col text-xs" on:keydown|capture={preventSave}>
<header class="flex shrink-0 flex-wrap items-center gap-3 px-5 pt-4 pb-2">
<header class="flex shrink-0 flex-wrap items-center gap-2 px-5 pt-4 pb-2">
<div class="min-w-0 flex-1">
<Tooltip content={session.path ?? ''} allowHTML={false} className="min-w-0">
<h2 class="truncate font-medium">{session.title || $i18n.t('Compare revision')}</h2>
@ -415,7 +439,7 @@
>
{/if}
{#if comparing}
<div class="comparison-controls flex flex-wrap items-center gap-3">
<div class="comparison-controls flex flex-wrap items-center gap-1.5">
<div class="mr-1 flex items-center gap-1.5">
<span class="whitespace-nowrap text-gray-500" role="status" aria-live="polite"
>{loading
@ -469,6 +493,13 @@
title={$i18n.t('Copy change to proposal')}
on:click={() => copy('to-proposal')}>{$i18n.t('Proposed revision')} →</button
>
<button
type="button"
class="secondary"
disabled={loading || !canKeepBoth}
title={$i18n.t('Keep both versions in the proposed revision, original first.')}
on:click={keepBoth}>{$i18n.t('Keep both')}</button
>
{:else}
<button type="button" class="secondary" disabled={loading} on:click={swap}
>{$i18n.t('Swap sides')}</button
@ -710,13 +741,14 @@
.secondary,
.primary {
display: inline-flex;
height: 1.75rem;
height: 1.5rem;
min-width: 1.5rem;
flex-shrink: 0;
align-items: center;
justify-content: center;
gap: 0.375rem;
border-radius: 0.5rem;
padding: 0 0.625rem;
border-radius: 0.375rem;
padding: 0 0.5rem;
font-size: 0.75rem;
font-weight: 400;
transition:

View file

@ -261,21 +261,16 @@
<div>
<div class=" font-normal text-lg dark:text-gray-100">
<a
href="#"
href={item?.type === 'file'
? item?.url?.startsWith('http')
? item.url
: `${WEBUI_API_BASE_URL}/files/${item?.id ?? item?.tempId}/content`
: item?.type === 'collection' && item?.id
? `/workspace/knowledge/${item.id}`
: safeLinkUrl(item?.url)}
target="_blank"
rel="noreferrer"
class="hover:underline line-clamp-1"
on:click|preventDefault={() => {
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'}
</a>

View file

@ -67,6 +67,22 @@ export function copyChunk(a: Text, b: Text, chunk: Chunk, direction: MergeDirect
return { from, to: Math.min(to, destination.length), insert };
}
export function keepBothChunk(a: Text, b: Text, chunk: Chunk): ChangeSpec | null {
// A line-aligned chunk can include text on both sides even for a pure EOF insertion.
if (
!chunk.changes.some((change) => 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;

View file

@ -673,7 +673,7 @@
}}
>
<LinkPreview.Trigger
class="flex min-w-0 flex-1 items-center"
class="flex min-w-0 flex-1 -my-1.5 py-1.5 items-center"
href="/c/{id}"
aria-current={id === $chatId ? 'page' : undefined}
onclick={selectChatHandler}

View file

@ -1,3 +1,7 @@
<script context="module">
let closeActiveHoverPreview = null;
</script>
<script>
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
@ -92,6 +96,17 @@
openPreview = false;
}
const closeHoverPreview = () => {
if (openPreview) {
openPreview = false;
}
};
$: if (openPreview && closeActiveHoverPreview !== closeHoverPreview) {
closeActiveHoverPreview?.();
closeActiveHoverPreview = closeHoverPreview;
}
let name = '';
const formatUnreadCount = (count) =>
@ -445,6 +460,10 @@
onDestroy(() => {
delete folderRegistry[folderId];
if (closeActiveHoverPreview === closeHoverPreview) {
closeActiveHoverPreview = null;
}
if (folderElement) {
folderElement.removeEventListener('dragover', onDragOver);
folderElement.removeEventListener('drop', onDrop);
@ -800,7 +819,9 @@
{/if}
</button>
<div class="translate-y-[0.5px] flex min-w-0 flex-1 items-center gap-1.5 pr-6 text-start">
<div
class="translate-y-[0.5px] flex min-w-0 flex-1 self-stretch items-center gap-1.5 pr-6 text-start"
>
{#if edit}
<input
id="folder-{folderId}-input"
@ -836,7 +857,7 @@
<LinkPreview.Trigger
href={`/folders/${folderId}`}
draggable="false"
class="flex min-w-0 flex-1 items-center gap-1.5"
class="flex min-w-0 flex-1 self-stretch -my-1 items-center gap-1.5"
ondblclick={(e) => {
if (!isWritable || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
if (clickTimer) {

View file

@ -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.": "إبقاء الأدوات مدرجة حتى أثناء إغلاق الطرفية.",

View file

@ -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.": "إبقاء الأدوات مدرجة حتى أثناء إغلاق الطرفية.",

View file

@ -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.",

View file

@ -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.": "Инструментите остават в списъка, дори когато командната обвивка е затворена.",

View file

@ -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.": "শেল বন্ধ থাকলেও টুলগুলো তালিকায় রাখুন।",

View file

@ -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.": "ཁྱེད་ཀྱི་བཀའ་བརྡའི་འགྲེལ་བཤད་ཆས་ཁ་བརྒྱབ་ཡོད་སྐབས་ཀྱང་ལག་ཆ་ཐོ་ནང་འཇོག་པ།",

View file

@ -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.",

View file

@ -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.": "",

View file

@ -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.",

View file

@ -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ý.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.": "Τα εργαλεία παραμένουν στη λίστα ακόμη και όταν το κέλυφός σας είναι κλειστό.",

View file

@ -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.": "",

View file

@ -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.": "",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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مانند.",

View file

@ -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.": "",

View file

@ -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.",

View file

@ -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ð.",

View file

@ -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.": "",

View file

@ -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.": "",

View file

@ -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.",

View file

@ -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.": "השארת הכלים ברשימה גם כשהמעטפת שלך סגורה.",

View file

@ -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.": "आपका शेल बंद होने पर भी टूल सूची में रखें.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.": "シェルが閉じている間もツールを一覧に残します。",

View file

@ -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.": "ხელსაწყოებს სიაში ტოვებს მაშინაც, როცა თქვენი გარსი დახურულია.",

View file

@ -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.",

View file

@ -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.": "셸이 닫혀 있는 동안에도 도구를 목록에 유지합니다.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.": "ਤੁਹਾਡਾ ਸ਼ੈੱਲ ਬੰਦ ਹੋਣ 'ਤੇ ਵੀ ਟੂਲ ਸੂਚੀ ਵਿੱਚ ਰੱਖੋ।",

View file

@ -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.",

View file

@ -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.",

View file

@ -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.": "",

View file

@ -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.",

View file

@ -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.": "Оставлять инструменты в списке, даже когда командная строка закрыта.",

View file

@ -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ý.",

View file

@ -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.",

View file

@ -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.": "Задржава алате на листи чак и док је ваша љуска затворена.",

View file

@ -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.",

View file

@ -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.": "உங்கள் ஷெல் மூடப்பட்டிருக்கும்போதும் கருவிகளைப் பட்டியலில் வைத்திருக்கவும்.",

View file

@ -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 ของคุณปิดอยู่",

View file

@ -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ň.",

View file

@ -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.",

View file

@ -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 يېپىلغان ۋاقىتتىمۇ قوراللارنى تىزىملىكتە ساقلاڭ.",

View file

@ -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.": "Залишати інструменти в списку, навіть коли ваш командний рядок закрито.",

View file

@ -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.": "آپ کا شیل بند ہونے پر بھی ٹولز فہرست میں رکھیں۔",

View file

@ -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.": "",

View file

@ -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.": "",

View file

@ -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.",

View file

@ -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.": "",

View file

@ -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.": "",