Merge pull request #22168 from open-webui/dev

0.8.8
This commit is contained in:
Tim Baek
2026-03-03 03:32:58 +04:00
committed by GitHub
79 changed files with 1238 additions and 891 deletions
+22
View File
@@ -5,6 +5,28 @@ 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.8.8] - 2026-03-02
### Added
- 📁 **Open Terminal file moving.** Users can now move files and folders between directories in the Open Terminal file browser by dragging and dropping them. [Commit](https://github.com/open-webui/open-webui/commit/0c42cd2c012f9f49816adac897e2b46573b3cb6c), [Commit](https://github.com/open-webui/open-webui/commit/72951324dfeef64e09f4776898d675bc1c44f040), [Commit](https://github.com/open-webui/open-webui/commit/395098c6f1b7499d37ad55145a5931431d3e72e9), [Commit](https://github.com/open-webui/open-webui/commit/11487d66fc1a2dfafbdaa2b7ef939a86caaf3872)
- 📄 **Open Terminal HTML file preview.** Users can now preview HTML files directly in the Open Terminal file browser, with a rendered iframe view and source toggle, enabling iterative AI editing of HTML files. [Commit](https://github.com/open-webui/open-webui/commit/3909b62ffcf49839fa57346ed8487ae759811503), [Commit](https://github.com/open-webui/open-webui/commit/933a3bbbd3f4fc3eeb0ec52c7965e9ac1c4cea39)
- 🌐 **Open Terminal WebSocket proxy.** Added a new WebSocket proxy endpoint for interactive terminal sessions, enabling real-time bidirectional terminal communication with the terminal server. [Commit](https://github.com/open-webui/open-webui/commit/4f6cb771f1afded09aad6199cdb244dd8a6c77a6)
- ⚙️ **Open Terminal feature toggle.** Administrators can now enable or disable the Interactive Terminal feature for Open Terminal via configuration on the terminal server, controlling access to terminal routes. [Commit](https://github.com/open-webui/open-webui/commit/b5c3395f79bcc7ff5bc1d82bb86a60583bb3b5bd)
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
- 🌐 Translations for Simplified Chinese, Traditional Chinese, Irish, and Catalan were enhanced and expanded.
### Fixed
- 🔧 **Middleware variable shadowing.** Fixed a variable shadowing issue in the middleware that could cause incorrect tool output processing during chat. [#22145](https://github.com/open-webui/open-webui/pull/22145)
- ⚡ **ChatControls reactivity fix.** Fixed a Svelte reactivity issue where the active tab state in the ChatControls panel was not properly saved when switching between chats. [#22127](https://github.com/open-webui/open-webui/pull/22127)
- 🔧 **ChatControls TypeScript fix.** Fixed a TypeScript syntax error in ChatControls.svelte where the module script block was missing lang="ts", causing esbuild to fail during vite dev. [#22131](https://github.com/open-webui/open-webui/pull/22131)
- 🔌 **Open Terminal tools for direct connections.** Fixed an issue where Open Terminal tools were not available to the model when the terminal was configured via direct connection settings, ensuring users can now interact with terminal files and operations through the AI. [#22137](https://github.com/open-webui/open-webui/issues/22137)
- 📜 **Chat history pagination.** Fixed an issue where older messages in long chats were not loaded when scrolling to the top. [Commit](https://github.com/open-webui/open-webui/commit/d7147d6cddfd314f0f1be77b15cec406a609ef36), [Commit](https://github.com/open-webui/open-webui/commit/c701ebe07bd152eecb42b0bf6de26071358a5c76)
- 🔧 **Terminal tool null parameter handling.** Fixed a bug where null parameters in terminal tool calls were sent as the string "None" instead of being omitted, causing 422 validation errors from the open-terminal server. [#22124](https://github.com/open-webui/open-webui/issues/22124), [#22144](https://github.com/open-webui/open-webui/pull/22144)
### Changed
## [0.8.7] - 2026-03-01
### Fixed
+154 -1
View File
@@ -8,13 +8,14 @@ Routes:
import logging
import aiohttp
from fastapi import APIRouter, Depends, Request, Response
from fastapi import APIRouter, Depends, Request, Response, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.background import BackgroundTask
from open_webui.utils.auth import get_verified_user
from open_webui.utils.access_control import has_connection_access
from open_webui.models.groups import Groups
from open_webui.models.users import Users
log = logging.getLogger(__name__)
@@ -149,3 +150,155 @@ async def proxy_terminal(
return JSONResponse(
{"error": f"Terminal proxy error: {error}"}, status_code=502
)
# ---------------------------------------------------------------------------
# WebSocket proxy for interactive terminal sessions
# ---------------------------------------------------------------------------
async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
"""Authenticate a WebSocket via first-message auth and resolve the terminal server.
The client must send ``{"type": "auth", "token": "<jwt>"}`` as its first
message after connecting.
Returns ``(user, connection)`` on success, or ``None`` after closing *ws*
with an appropriate error code.
"""
import asyncio
import json
from open_webui.utils.auth import decode_token
# First-message authentication
try:
raw = await asyncio.wait_for(ws.receive_text(), timeout=10.0)
payload = json.loads(raw)
if payload.get("type") != "auth":
await ws.close(code=4001, reason="Expected auth message")
return None
token = payload.get("token", "")
data = decode_token(token)
if data is None or "id" not in data:
await ws.close(code=4001, reason="Invalid token")
return None
user = Users.get_user_by_id(data["id"])
if user is None:
await ws.close(code=4001, reason="User not found")
return None
except (asyncio.TimeoutError, json.JSONDecodeError):
await ws.close(code=4001, reason="Auth timeout or invalid payload")
return None
except Exception:
await ws.close(code=4001, reason="Invalid token")
return None
# Resolve terminal server
connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connection = next((c for c in connections if c.get("id") == server_id), None)
if connection is None:
await ws.close(code=4004, reason="Terminal server not found")
return None
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
if not has_connection_access(user, connection, user_group_ids):
await ws.close(code=4003, reason="Access denied")
return None
return user, connection
@router.websocket("/{server_id}/api/terminals/{session_id}")
async def ws_terminal(
ws: WebSocket,
server_id: str,
session_id: str,
):
"""Proxy an interactive WebSocket terminal session to a terminal server.
Uses first-message auth: the client sends ``{"type": "auth", "token": "<jwt>"}``
as its first message. The proxy validates the JWT, then connects to the
upstream terminal server and authenticates with the server's API key.
"""
await ws.accept()
result = await _resolve_authenticated_connection(ws, server_id)
if result is None:
return
user, connection = result
base_url = (connection.get("url") or "").rstrip("/")
if not base_url:
await ws.close(code=4003, reason="Terminal server URL not configured")
return
# Build upstream WebSocket URL (no token in URL)
ws_base = base_url.replace("https://", "wss://").replace("http://", "ws://")
auth_type = connection.get("auth_type", "bearer")
upstream_params = {}
# For orchestrator-backed servers, pass user_id
upstream_params["user_id"] = user.id
import urllib.parse
upstream_url = f"{ws_base}/api/terminals/{session_id}"
if upstream_params:
upstream_url += f"?{urllib.parse.urlencode(upstream_params)}"
session = aiohttp.ClientSession()
try:
async with session.ws_connect(upstream_url) as upstream:
import asyncio
import json as _json
# First-message auth to upstream terminal server
auth_type = connection.get("auth_type", "bearer")
if auth_type == "bearer":
key = connection.get("key", "")
await upstream.send_str(_json.dumps({"type": "auth", "token": key}))
async def _client_to_upstream():
"""Forward client → upstream."""
try:
while True:
msg = await ws.receive()
if msg["type"] == "websocket.disconnect":
break
elif "bytes" in msg and msg["bytes"]:
await upstream.send_bytes(msg["bytes"])
elif "text" in msg and msg["text"]:
await upstream.send_str(msg["text"])
except Exception:
pass
async def _upstream_to_client():
"""Forward upstream → client."""
try:
async for msg in upstream:
if msg.type == aiohttp.WSMsgType.BINARY:
await ws.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.TEXT:
await ws.send_text(msg.data)
elif msg.type in (
aiohttp.WSMsgType.CLOSE,
aiohttp.WSMsgType.ERROR,
):
break
except Exception:
pass
await asyncio.gather(
_client_to_upstream(),
_upstream_to_client(),
return_exceptions=True,
)
except Exception as e:
log.exception("Terminal WebSocket proxy error: %s", e)
finally:
await session.close()
try:
await ws.close()
except Exception:
pass
+1 -1
View File
@@ -1831,7 +1831,7 @@ async def query_knowledge_files(
elif item_type == "file":
# Individual file - use file-{id} as collection name
file = Files.get_file_by_id(item_id)
if file and (user_role == "admin" or file.user_id == user_id):
if file:
collection_names.append(f"file-{item_id}")
elif item_type == "note":
@@ -7,6 +7,7 @@ from open_webui.models.knowledge import Knowledges
from open_webui.models.channels import Channels
from open_webui.models.chats import Chats
from open_webui.models.groups import Groups
from open_webui.models.models import Models
from open_webui.models.access_grants import AccessGrants
log = logging.getLogger(__name__)
@@ -21,6 +22,7 @@ def has_access_to_file(
"""
Check if a user has the specified access to a file through any of:
- Knowledge bases (ownership or access grants)
- Shared workspace models that attach the file directly
- Channels the user is a member of
- Shared chats
@@ -72,4 +74,15 @@ def has_access_to_file(
if chats:
return True
# Check if the file is directly attached to a shared workspace model
for model in Models.get_models_by_user_id(user.id, permission=access_type, db=db):
knowledge_items = getattr(model.meta, "knowledge", None) or []
for item in knowledge_items:
if (
isinstance(item, dict)
and item.get("type") == "file"
and item.get("id") == file.id
):
return True
return False
+50 -24
View File
@@ -91,6 +91,7 @@ from open_webui.utils.misc import (
get_last_user_message_item,
get_last_assistant_message,
get_system_message,
replace_system_message_content,
prepend_to_first_user_message_content,
convert_logit_bias_input_to_json,
get_content_from_message,
@@ -375,9 +376,9 @@ def serialize_output(output: list) -> str:
result_item = tool_outputs.get(call_id)
if result_item:
result_text = ""
for output in result_item.get("output", []):
if "text" in output:
output_text = output.get("text", "")
for result_output in result_item.get("output", []):
if "text" in result_output:
output_text = result_output.get("text", "")
result_text += (
str(output_text)
if not isinstance(output_text, str)
@@ -4090,6 +4091,22 @@ async def streaming_chat_response_handler(response, ctx):
all_tool_call_sources = [] # Accumulated sources across all iterations
user_message = get_last_user_message(form_data["messages"])
# Check if citations are enabled for this model
citations_enabled = (
model.get("info", {}).get("meta", {}).get("capabilities") or {}
).get("citations", True)
# Save original system message so we can restore it before
# re-applying source context (prevents duplication when
# RAG_SYSTEM_CONTEXT is enabled and the template is appended
# to the system message on each iteration).
original_system_message = get_system_message(form_data["messages"])
original_system_content = (
get_content_from_message(original_system_message)
if original_system_message
else None
)
while (
len(tool_calls) > 0
and tool_call_retries < CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES
@@ -4244,7 +4261,8 @@ async def streaming_chat_response_handler(response, ctx):
# Extract citation sources from tool results
if (
tool_function_name
citations_enabled
and tool_function_name
in [
"search_web",
"fetch_url",
@@ -4334,27 +4352,35 @@ async def streaming_chat_response_handler(response, ctx):
}
)
# Emit citation sources for UI display
for source in tool_call_sources:
await event_emitter({"type": "source", "data": source})
# Emit citation sources and apply source context to messages
if citations_enabled:
for source in tool_call_sources:
await event_emitter({"type": "source", "data": source})
# Apply source context to messages for model
# Use metadata_only=True to avoid duplicating content
# that is already in the tool result message.
all_tool_call_sources.extend(tool_call_sources)
if all_tool_call_sources and user_message:
# Restore original user message before re-applying to avoid recursive nesting
set_last_user_message_content(
user_message, form_data["messages"]
)
form_data["messages"] = apply_source_context_to_messages(
request,
form_data["messages"],
all_tool_call_sources,
user_message,
include_content=False,
)
tool_call_sources.clear()
# Apply source context to messages for model.
# Use include_content=False to avoid duplicating content
# that is already in the tool result message.
all_tool_call_sources.extend(tool_call_sources)
if all_tool_call_sources and user_message:
# Restore original messages before re-applying to
# avoid recursive nesting (user message) and
# duplication (system message with RAG_SYSTEM_CONTEXT).
set_last_user_message_content(
user_message, form_data["messages"]
)
if original_system_content is not None:
replace_system_message_content(
original_system_content,
form_data["messages"],
)
form_data["messages"] = apply_source_context_to_messages(
request,
form_data["messages"],
all_tool_call_sources,
user_message,
include_content=False,
)
tool_call_sources.clear()
await event_emitter(
{
+2 -1
View File
@@ -1247,7 +1247,8 @@ async def execute_tool_server(
if param_in == "path":
path_params[param_name] = params[param_name]
elif param_in == "query":
query_params[param_name] = params[param_name]
if params[param_name] is not None:
query_params[param_name] = params[param_name]
final_url = f"{url}{route_path}"
for key, value in path_params.items():
+34 -467
View File
@@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.8.7",
"version": "0.8.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.8.7",
"version": "0.8.8",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
@@ -38,6 +38,8 @@
"@tiptap/pm": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.4.2",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/svelte": "^0.1.19",
"alpinejs": "^3.15.0",
@@ -181,22 +183,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@azure/msal-browser": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.5.0.tgz",
@@ -656,131 +642,6 @@
"node": ">=0.1.90"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@cypress/request": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
@@ -3405,9 +3266,9 @@
}
},
"node_modules/@tiptap/extension-collaboration": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.4.5.tgz",
"integrity": "sha512-JyPXTYkYi2XzUWsmObv2cogMrs7huAvfq6l7d5hAwsU2FnA1vMycaa48N4uekogySP6VBkiQNDf9B4T09AwwqA==",
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.20.0.tgz",
"integrity": "sha512-JItmI4U0i4kqorO114u24hM9k945IdaQ6Uc2DEtPBFFuS8cepJf2zw+ulAT1kAx6ZRiNvNpT9M7w+J0mWRn+Sg==",
"license": "MIT",
"peer": true,
"funding": {
@@ -3415,9 +3276,9 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.4.5",
"@tiptap/pm": "^3.4.5",
"@tiptap/y-tiptap": "^3.0.0-beta.3",
"@tiptap/core": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/y-tiptap": "^3.0.2",
"yjs": "^13"
}
},
@@ -3664,9 +3525,9 @@
}
},
"node_modules/@tiptap/extension-node-range": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.4.5.tgz",
"integrity": "sha512-mHCjdJZX8DZCpnw9wBqioanANy6tRoy20/OcJxMW1T7naeRCuCU4sFjwO37yb/tmYk1BQA2/L1/H2r0fVoZwtA==",
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.20.0.tgz",
"integrity": "sha512-XeKKTV88VuJ4Mh0Rxvc/PPzG76cb44sE+rB4u0J/ms63R/WFTm6yJQlCgUVGnGeHleSlrWuZY8gGSuoljmQzqg==",
"license": "MIT",
"peer": true,
"funding": {
@@ -3674,8 +3535,8 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.4.5",
"@tiptap/pm": "^3.4.5"
"@tiptap/core": "^3.20.0",
"@tiptap/pm": "^3.20.0"
}
},
"node_modules/@tiptap/extension-ordered-list": {
@@ -3745,9 +3606,9 @@
}
},
"node_modules/@tiptap/extension-text-style": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.0.7.tgz",
"integrity": "sha512-naJ1XxlbFJ1qlpA+i54lQYKuhWP1dnkUslM86OT0TZt0zJBeu7LIrqSOVGmMB++lF/btnQLMnYkYSSnkLgIw3A==",
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.0.tgz",
"integrity": "sha512-zyWW1a6W+kaXAn3wv2svJ1XuVMapujftvH7Xn2Q3QmKKiDkO+NiFkrGe8BhMopu8Im51nO3NylIgVA0X1mS1rQ==",
"license": "MIT",
"peer": true,
"funding": {
@@ -3755,7 +3616,7 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.0.7"
"@tiptap/core": "^3.20.0"
}
},
"node_modules/@tiptap/extension-typography": {
@@ -3892,9 +3753,9 @@
}
},
"node_modules/@tiptap/y-tiptap": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.0.tgz",
"integrity": "sha512-HIeJZCj+KYJde2x6fONzo4o6kd7gW7eonwhQsv2p2VQnUgwNXMVhN+D6Z3AH/2i541Sq33y1PO4U/1ThCPjqbA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.2.tgz",
"integrity": "sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -4620,6 +4481,18 @@
"node": ">=10.0.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
"license": "MIT"
},
"node_modules/@xterm/addon-web-links": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
"license": "MIT"
},
"node_modules/@xterm/xterm": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
@@ -4695,18 +4568,6 @@
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14"
}
},
"node_modules/aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -6056,31 +5917,6 @@
"node": ">=4"
}
},
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/cssstyle/node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/cypress": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
@@ -6736,22 +6572,6 @@
"node": ">=0.10"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -6774,15 +6594,6 @@
}
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/deep-eql": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -8312,21 +8123,6 @@
"node": ">=12.0.0"
}
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/html-entities": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.3.tgz",
@@ -8395,22 +8191,6 @@
"entities": "^4.5.0"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-signature": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
@@ -8425,22 +8205,6 @@
"node": ">=0.10"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/human-signals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
@@ -8801,15 +8565,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -8912,73 +8667,6 @@
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"dev": true
},
"node_modules/jsdom": {
"version": "24.1.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz",
"integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.0.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.4.3",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.5",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.12",
"parse5": "^7.1.2",
"rrweb-cssom": "^0.7.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.4",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^2.11.2"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsdom/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -10244,15 +9932,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nwsapi": {
"version": "2.2.21",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
"integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -11620,15 +11299,6 @@
"points-on-path": "^0.2.1"
}
},
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -12077,21 +11747,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@@ -12746,15 +12401,6 @@
"node": ">=12.0.0"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/symlink-or-copy": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
@@ -13016,21 +12662,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -14510,21 +14141,6 @@
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/walk-sync": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
@@ -14563,18 +14179,6 @@
"node": "*"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
@@ -14598,22 +14202,6 @@
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/wheel": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
@@ -14800,18 +14388,6 @@
"node": ">=0.8"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
@@ -14821,15 +14397,6 @@
"node": ">=4.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.8.7",
"version": "0.8.8",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@@ -82,6 +82,8 @@
"@tiptap/pm": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.4.2",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/svelte": "^0.1.19",
"alpinejs": "^3.15.0",
+42
View File
@@ -5,6 +5,10 @@ export type FileEntry = {
modified?: number;
};
export type TerminalFeatures = {
terminal?: boolean;
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export type TerminalServer = {
@@ -23,6 +27,18 @@ export const getTerminalServers = async (token: string): Promise<TerminalServer[
return res.json().catch(() => []);
};
export const getTerminalConfig = async (
baseUrl: string,
apiKey: string
): Promise<{ features: TerminalFeatures } | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/api/config`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
}).catch(() => null);
if (!res || !res.ok) return null;
return res.json().catch(() => null);
};
export const getCwd = async (baseUrl: string, apiKey: string): Promise<string | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`;
const res = await fetch(url, {
@@ -193,3 +209,29 @@ export const setCwd = async (
});
return res;
};
export const moveEntry = async (
baseUrl: string,
apiKey: string,
source: string,
destination: string
): Promise<{ source: string; destination: string } | { error: string }> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/move`;
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ source, destination })
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.error('open-terminal moveEntry error:', err);
return { error: err?.detail ?? 'Move failed' };
});
return res;
};
@@ -25,7 +25,7 @@
let id = '';
let auth_type = 'bearer';
let path = '/openapi.json';
let enabled = true;
let enabled = false;
let showAdvanced = false;
let showAccessControlModal = false;
let accessGrants: any[] = [];
@@ -47,7 +47,7 @@
name = '';
auth_type = 'bearer';
path = '/openapi.json';
enabled = true;
enabled = false;
accessGrants = [];
}
};
+4 -43
View File
@@ -146,34 +146,6 @@
let webSearchEnabled = false;
let codeInterpreterEnabled = false;
// Auto-inject direct terminal servers into selected tool IDs so they act like toggled-on tools
// System terminals (with id field) are handled server-side via terminal_id, not as direct tool servers
$: if ($terminalServers && $terminalServers.length > 0) {
const directTerminalServers = $terminalServers.filter((t) => !t.id);
const terminalIds = directTerminalServers.map(
(_, i) => `direct_server:terminal_${$terminalServers.indexOf(directTerminalServers[i])}`
);
const missingIds = terminalIds.filter((id) => !selectedToolIds.includes(id));
if (missingIds.length > 0) {
selectedToolIds = [...selectedToolIds, ...missingIds];
}
}
// Remove disabled terminal servers from selectedToolIds automatically
$: if (selectedToolIds.length > 0) {
const directTerminalServers = ($terminalServers ?? []).filter((t) => !t.id);
const terminalIds = directTerminalServers.map(
(_, i) =>
`direct_server:terminal_${($terminalServers ?? []).indexOf(directTerminalServers[i])}`
);
const invalidTerminalIds = selectedToolIds.filter(
(id) => id.startsWith('direct_server:terminal_') && !terminalIds.includes(id)
);
if (invalidTerminalIds.length > 0) {
selectedToolIds = selectedToolIds.filter((id) => !invalidTerminalIds.includes(id));
}
}
let showCommands = false;
let generating = false;
@@ -354,25 +326,12 @@
[...(model?.info?.meta?.toolIds ?? [])].filter((id) => $tools.find((t) => t.id === id))
)
];
} else if (
$settings?.tools &&
$settings.tools.some((id) => !id.startsWith('direct_server:terminal_'))
) {
} else if ($settings?.tools) {
selectedToolIds = $settings.tools;
} else {
// Don't wipe existing terminal servers if no default tool IDs
selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:'));
}
// Auto-inject direct terminal servers (system ones are handled via terminal_id)
if ($terminalServers && $terminalServers.length > 0) {
const directTerminalServers = $terminalServers.filter((t) => !t.id);
const terminalIds = directTerminalServers.map(
(_, i) => `direct_server:terminal_${$terminalServers.indexOf(directTerminalServers[i])}`
);
selectedToolIds = [...new Set([...selectedToolIds, ...terminalIds])];
}
// Set Default Filters (Toggleable only)
if (model?.info?.meta?.defaultFilterIds) {
selectedFilterIds = model.info.meta.defaultFilterIds.filter((id) =>
@@ -2210,7 +2169,9 @@
tool_servers: [
...($toolServers ?? []).filter(
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
)
),
// Direct terminal servers — always included when enabled (not routed through selectedToolIds)
...($terminalServers ?? []).filter((t) => !t.id)
],
features: getFeatures(),
variables: {
+22 -12
View File
@@ -1,4 +1,4 @@
<script context="module">
<script context="module" lang="ts">
let savedTab: 'controls' | 'files' | 'overview' = 'controls';
</script>
@@ -58,8 +58,12 @@
let paneReady = false;
// Tab state for Controls+Files panel
let activeTab: 'controls' | 'files' | 'overview' = savedTab;
$: savedTab = activeTab;
let activeTab = savedTab;
// svelte-ignore reactive_declaration_module_script_dependency
$: {
savedTab = activeTab;
}
$: hasMessages = history?.messages && Object.keys(history.messages).length > 0;
$: showControlsTab = $user?.role === 'admin' || ($user?.permissions?.chat?.controls ?? true);
@@ -280,10 +284,11 @@
<div class="flex flex-col h-full min-h-0">
<!-- Tab bar -->
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
<div class="flex gap-1">
<div class="flex gap-1 min-w-0 overflow-x-auto scrollbar-hidden">
{#if showControlsTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'controls'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'controls')}
@@ -293,7 +298,8 @@
{/if}
{#if showFilesTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'files'
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'files'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'files')}
@@ -303,7 +309,8 @@
{/if}
{#if showOverviewTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'overview'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'overview')}
@@ -418,10 +425,11 @@
<div class="flex flex-col h-full min-h-0">
<!-- Tab bar -->
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
<div class="flex gap-1">
<div class="flex gap-1 min-w-0 overflow-x-auto scrollbar-hidden">
{#if showControlsTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'controls'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'controls')}
@@ -431,7 +439,8 @@
{/if}
{#if showFilesTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'files'
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'files'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'files')}
@@ -441,7 +450,8 @@
{/if}
{#if showOverviewTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'overview'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'overview')}
@@ -490,7 +500,7 @@
onClose={() => showControls.set(false)}
/>
{:else if activeTab === 'files' && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} />
<FileNav onAttach={handleTerminalAttach} overlay={dragged} />
{:else}
<Controls embed={true} {models} bind:chatFiles bind:params />
{/if}
+142 -1
View File
@@ -15,12 +15,14 @@
} from '$lib/stores';
import {
getCwd,
getTerminalConfig,
listFiles,
readFile,
downloadFileBlob,
uploadToTerminal,
createDirectory,
deleteEntry,
moveEntry,
setCwd,
type FileEntry
} from '$lib/apis/terminal';
@@ -35,10 +37,47 @@
import FileNavToolbar from './FileNav/FileNavToolbar.svelte';
import FilePreview from './FileNav/FilePreview.svelte';
import FileEntryRow from './FileNav/FileEntryRow.svelte';
import XTerminal from './XTerminal.svelte';
const i18n = getContext('i18n');
export let onAttach: ((blob: Blob, name: string, contentType: string) => void) | null = null;
export let overlay = false;
// ── Terminal panel state ────────────────────────────────────────────
let terminalExpanded = false;
let terminalHeight = 200; // px, default when expanded
let isDraggingHandle = false;
let containerEl: HTMLElement;
let terminalConnected = false;
let terminalConnecting = false;
let terminalEnabled = true;
const toggleTerminal = () => {
terminalExpanded = !terminalExpanded;
};
const onHandleMouseDown = (e: MouseEvent) => {
e.preventDefault();
isDraggingHandle = true;
const startY = e.clientY;
const startHeight = terminalHeight;
const onMouseMove = (ev: MouseEvent) => {
const delta = startY - ev.clientY;
const maxH = containerEl ? containerEl.clientHeight - 100 : 500;
terminalHeight = Math.max(80, Math.min(maxH, startHeight + delta));
};
const onMouseUp = () => {
isDraggingHandle = false;
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
};
// ── Directory state ──────────────────────────────────────────────────
let currentPath = savedPath;
@@ -61,10 +100,12 @@
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
const CSV_EXTS = new Set(['csv', 'tsv']);
const HTML_EXTS = new Set(['html', 'htm']);
const getFileExt = (path: string | null) => path?.split('.').pop()?.toLowerCase() ?? '';
$: isMarkdown = MD_EXTS.has(getFileExt(selectedFile));
$: isCsv = CSV_EXTS.has(getFileExt(selectedFile));
$: isHtml = HTML_EXTS.has(getFileExt(selectedFile));
$: isTextFile = fileContent !== null && fileImageUrl === null && filePdfData === null;
// ── Upload / folder creation ─────────────────────────────────────────
@@ -112,6 +153,10 @@
if (terminal && terminal.url !== prevTerminalUrl) {
prevTerminalUrl = terminal.url;
(async () => {
// Discover server features (terminal enabled/disabled)
const config = await getTerminalConfig(terminal.url, terminal.key);
terminalEnabled = config?.features?.terminal !== false;
const cwd = await getCwd(terminal.url, terminal.key);
const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/';
savedPath = dir;
@@ -323,6 +368,29 @@
showDeleteConfirm = true;
};
// ── Move (drag-and-drop) ────────────────────────────────────────────
const handleMove = async (source: string, destFolder: string) => {
const terminal = selectedTerminal;
if (!terminal) return;
const fileName = source.split('/').pop() ?? '';
const destination = `${destFolder}${fileName}`;
if (source === destination) return;
// Prevent moving a folder into itself or its own subtree
const sourceDir = source.endsWith('/') ? source : source + '/';
if (destFolder.startsWith(sourceDir)) return;
const result = await moveEntry(terminal.url, terminal.key, source, destination);
if ('error' in result) {
toast.error(result.error);
} else {
toast.success($i18n.t('Moved {{name}}', { name: fileName }));
}
await loadDir(currentPath);
};
// ── Lifecycle ────────────────────────────────────────────────────────
onMount(async () => {
const terminal = getTerminal();
@@ -427,6 +495,7 @@
</div>
{:else}
<div
bind:this={containerEl}
class="flex flex-col h-full min-h-0 relative"
on:dragover={handleDragOver}
on:dragleave={() => (isDragOver = false)}
@@ -465,6 +534,7 @@
onNewFolder={startNewFolder}
onNewFile={startNewFile}
onUploadFiles={handleUploadFiles}
onMove={handleMove}
>
{#if fileImageUrl !== null}
<Tooltip content={$i18n.t('Reset view')}>
@@ -488,7 +558,7 @@
</button>
</Tooltip>
{/if}
{#if (isMarkdown || isCsv) && fileContent !== null && !editing}
{#if (isMarkdown || isCsv || isHtml) && fileContent !== null && !editing}
<Tooltip content={showRaw ? $i18n.t('Preview') : $i18n.t('Source')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
@@ -631,6 +701,7 @@
{fileImageUrl}
{filePdfData}
{fileContent}
{overlay}
onSave={async (content) => {
const terminal = selectedTerminal;
if (!terminal || !selectedFile) return;
@@ -717,6 +788,7 @@
onOpen={openEntry}
onDownload={downloadFile}
onDelete={requestDelete}
onMove={handleMove}
/>
{/each}
</ul>
@@ -724,5 +796,74 @@
{/if}
{/if}
</div>
<!-- Terminal bottom panel -->
{#if terminalEnabled}
<div class="shrink-0 border-t border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-850">
{#if terminalExpanded}
<!-- Drag handle (at top of panel) -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="h-1 cursor-row-resize hover:bg-blue-400/30 transition group relative"
on:mousedown={onHandleMouseDown}
>
<div class="absolute inset-x-0 -top-1 -bottom-1" />
</div>
{/if}
<!-- Toggle header (full-width button) -->
<button
class="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition"
on:click={toggleTerminal}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5"
>
<path
fill-rule="evenodd"
d="M3.25 3A2.25 2.25 0 0 0 1 5.25v9.5A2.25 2.25 0 0 0 3.25 17h13.5A2.25 2.25 0 0 0 19 14.75v-9.5A2.25 2.25 0 0 0 16.75 3H3.25Zm.943 8.752a.75.75 0 0 1 .055-1.06L6.128 9l-1.88-1.693a.75.75 0 1 1 1.004-1.114l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.06-.055ZM9.75 10.25a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z"
clip-rule="evenodd"
/>
</svg>
<span class="font-medium">{$i18n.t('Terminal')}</span>
{#if terminalExpanded}
<div
class="w-1.5 h-1.5 rounded-full transition-colors {terminalConnected
? 'bg-emerald-500'
: terminalConnecting
? 'bg-yellow-500 animate-pulse'
: 'bg-gray-400'}"
/>
{/if}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3 ml-auto transition-transform {terminalExpanded ? 'rotate-180' : ''}"
>
<path
fill-rule="evenodd"
d="M9.47 6.47a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L10 8.06l-3.72 3.72a.75.75 0 0 1-1.06-1.06l4.25-4.25Z"
clip-rule="evenodd"
/>
</svg>
</button>
{#if terminalExpanded}
<div style="height: {terminalHeight}px" class="min-h-0">
<XTerminal
{overlay}
bind:connected={terminalConnected}
bind:connecting={terminalConnecting}
/>
</div>
{/if}
</div>
{/if}
</div>
{/if}
@@ -18,24 +18,70 @@
export let onOpen: (entry: FileEntry) => void = () => {};
export let onDownload: (path: string) => void = () => {};
export let onDelete: (path: string, name: string) => void = () => {};
export let onMove: (source: string, destFolder: string) => void = () => {};
let dragOverFolder = false;
</script>
<li class="group">
<div class="w-full flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition">
<div
class="w-full flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition
{dragOverFolder
? 'bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-400 dark:ring-blue-500 ring-inset'
: ''}"
role={entry.type === 'directory' ? 'button' : undefined}
on:dragover={(e) => {
if (entry.type !== 'directory') return;
if (!e.dataTransfer?.types.includes('application/x-terminal-file-move')) return;
e.preventDefault();
e.stopPropagation();
dragOverFolder = true;
}}
on:dragleave={(e) => {
if (entry.type !== 'directory') return;
e.stopPropagation();
dragOverFolder = false;
}}
on:drop={(e) => {
if (entry.type !== 'directory') return;
const raw = e.dataTransfer?.getData('application/x-terminal-file-move');
if (!raw) return;
e.preventDefault();
e.stopPropagation();
dragOverFolder = false;
try {
const data = JSON.parse(raw);
if (data.path) {
const destFolder = `${currentPath}${entry.name}/`;
// Don't allow dropping a folder onto itself
if (data.path + '/' === destFolder || data.path === destFolder) return;
onMove(data.path, destFolder);
}
} catch {}
}}
>
<button
class="flex-1 flex items-center gap-2 px-3 py-1.5 text-left min-w-0"
draggable={entry.type === 'file'}
draggable={true}
on:dragstart={(e) => {
if (entry.type !== 'file') return;
const filePath = `${currentPath}${entry.name}`;
// Internal move data
e.dataTransfer?.setData(
'application/x-terminal-file',
JSON.stringify({
path: `${currentPath}${entry.name}`,
name: entry.name,
url: terminalUrl,
key: terminalKey
})
'application/x-terminal-file-move',
JSON.stringify({ path: filePath, name: entry.name })
);
// Keep existing chat-attachment drag for files
if (entry.type === 'file') {
e.dataTransfer?.setData(
'application/x-terminal-file',
JSON.stringify({
path: filePath,
name: entry.name,
url: terminalUrl,
key: terminalKey
})
);
}
}}
on:click={() => onOpen(entry)}
>
@@ -18,6 +18,9 @@
export let onNewFolder: () => void = () => {};
export let onNewFile: () => void = () => {};
export let onUploadFiles: (files: File[]) => void = () => {};
export let onMove: (source: string, destFolder: string) => void = () => {};
let dragOverCrumb: number | null = null;
let uploadInput: HTMLInputElement;
let breadcrumbEl: HTMLDivElement;
@@ -41,8 +44,31 @@
class="text-xs shrink-0 px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition
{!selectedFile && i === breadcrumbs.length - 1
? 'text-gray-700 dark:text-gray-300'
: 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400'}"
: 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400'}
{dragOverCrumb === i
? 'bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-400 dark:ring-blue-500'
: ''}"
on:click={() => onNavigate(crumb.path)}
on:dragover={(e) => {
if (!e.dataTransfer?.types.includes('application/x-terminal-file-move')) return;
e.preventDefault();
e.stopPropagation();
dragOverCrumb = i;
}}
on:dragleave={() => {
if (dragOverCrumb === i) dragOverCrumb = null;
}}
on:drop={(e) => {
const raw = e.dataTransfer?.getData('application/x-terminal-file-move');
if (!raw) return;
e.preventDefault();
e.stopPropagation();
dragOverCrumb = null;
try {
const data = JSON.parse(raw);
if (data.path) onMove(data.path, crumb.path);
} catch {}
}}
>
{crumb.label}
</button>
@@ -3,6 +3,7 @@
import panzoom, { type PanZoom } from 'panzoom';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { settings } from '$lib/stores';
import Spinner from '../../common/Spinner.svelte';
import PDFViewer from '../../common/PDFViewer.svelte';
@@ -16,6 +17,8 @@
export let filePdfData: ArrayBuffer | null = null;
export let fileContent: string | null = null;
export let overlay = false;
export let onSave: ((content: string) => Promise<void>) | null = null;
export let editing = false;
@@ -57,10 +60,12 @@
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
const CSV_EXTS = new Set(['csv', 'tsv']);
const HTML_EXTS = new Set(['html', 'htm']);
const getExt = (path: string | null) => path?.split('.').pop()?.toLowerCase() ?? '';
$: isMarkdown = MD_EXTS.has(getExt(selectedFile));
$: isCsv = CSV_EXTS.has(getExt(selectedFile));
$: isHtml = HTML_EXTS.has(getExt(selectedFile));
$: csvDelimiter = getExt(selectedFile) === 'tsv' ? '\t' : ',';
$: renderedHtml =
isMarkdown && fileContent
@@ -166,7 +171,19 @@
{:else if filePdfData !== null}
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} className="w-full h-full" />
{:else if fileContent !== null}
{#if isMarkdown && !showRaw}
{#if isHtml && !showRaw}
{#if overlay}
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
{/if}
<iframe
srcdoc={fileContent}
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
class="w-full h-full border-none bg-white"
title="HTML Preview"
/>
{:else if isMarkdown && !showRaw}
<div class="prose dark:prose-invert max-w-full text-sm p-3">
{@html renderedHtml}
</div>
+3 -7
View File
@@ -1636,12 +1636,10 @@
{/if}
<div class="ml-1 flex gap-1.5">
{#if (selectedToolIds ?? []).filter((id) => !id.startsWith('direct_server:terminal_')).length > 0}
{#if (selectedToolIds ?? []).length > 0}
<Tooltip
content={$i18n.t('{{COUNT}} Available Tools', {
COUNT: (selectedToolIds ?? []).filter(
(id) => !id.startsWith('direct_server:terminal_')
).length
COUNT: (selectedToolIds ?? []).length
})}
>
<button
@@ -1655,9 +1653,7 @@
<Wrench className="size-4" strokeWidth="1.75" />
<span class="text-sm">
{(selectedToolIds ?? []).filter(
(id) => !id.startsWith('direct_server:terminal_')
).length}
{(selectedToolIds ?? []).length}
</span>
</button>
</Tooltip>
@@ -96,9 +96,7 @@
}
}
selectedToolIds = selectedToolIds.filter(
(id) => Object.keys(tools).includes(id) || id.startsWith('direct_server:terminal_')
);
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id));
};
</script>
+13 -7
View File
@@ -67,6 +67,7 @@
messagesLoading = true;
messagesCount += 20;
buildMessages();
await tick();
@@ -98,16 +99,21 @@
// Throttle message list rebuilds to once per animation frame during streaming.
// Structural changes (currentId change) always rebuild immediately.
$: if (history.currentId) {
const currentIdChanged = history.currentId !== lastCurrentId;
lastCurrentId = history.currentId;
const handleHistoryChange = (currentId, _messages) => {
if (!currentId) {
messages = [];
return;
}
const currentIdChanged = currentId !== lastCurrentId;
lastCurrentId = currentId;
if (currentIdChanged) {
// Structural change: new chat, navigation, new message — rebuild immediately
cancelAnimationFrame(pendingRebuild);
pendingRebuild = null;
buildMessages();
} else if (history.messages) {
} else if (_messages) {
// Content update (streaming) — throttle to once per frame
if (!pendingRebuild) {
pendingRebuild = requestAnimationFrame(() => {
@@ -116,9 +122,9 @@
});
}
}
} else {
messages = [];
}
};
$: handleHistoryChange(history.currentId, history.messages);
$: if (autoScroll && bottomPadding) {
(async () => {
+261
View File
@@ -0,0 +1,261 @@
<script lang="ts">
import { onMount, onDestroy, getContext } from 'svelte';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { terminalServers, settings, selectedTerminalId, user } from '$lib/stores';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let overlay = false;
let terminalEl: HTMLDivElement;
let term: Terminal | null = null;
let fitAddon: FitAddon | null = null;
let ws: WebSocket | null = null;
export let connected = false;
export let connecting = false;
let resizeObserver: ResizeObserver | null = null;
// Resolve the active terminal server's info for the WebSocket URL
const getTerminalInfo = (): { serverId: string; baseUrl: string } | null => {
// System terminal (admin-configured, has an `id`)
const systemTerminals = ($terminalServers ?? []).filter((t: any) => t.id);
const systemMatch = systemTerminals.find((t: any) => t.id === $selectedTerminalId);
if (systemMatch) {
// For system terminals, WS goes through the Open WebUI backend proxy
return { serverId: systemMatch.id, baseUrl: WEBUI_API_BASE_URL };
}
// Direct terminal (user-configured, matched by URL)
const directTerminals = ($settings?.terminalServers ?? []).filter((s: any) => s.url);
const directMatch = directTerminals.find((s: any) => s.url === $selectedTerminalId);
if (directMatch) {
// For direct terminals, construct WS URL from the server URL directly
return { serverId: '__direct__', baseUrl: directMatch.url };
}
return null;
};
const connect = async () => {
if (ws) disconnect();
const info = getTerminalInfo();
if (!info) return;
connecting = true;
const token = localStorage.getItem('token') ?? '';
try {
let sessionId: string;
let wsUrl: string;
let authToken: string;
if (info.serverId === '__direct__') {
// Direct connection to open-terminal
const base = info.baseUrl.replace(/\/$/, '');
const directTerminals = ($settings?.terminalServers ?? []).filter((s: any) => s.url);
const directMatch = directTerminals.find((s: any) => s.url === $selectedTerminalId);
const apiKey = directMatch?.key ?? '';
authToken = apiKey;
// Create session
const res = await fetch(`${base}/api/terminals`, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!res.ok) throw new Error(`Failed to create session: ${res.status}`);
const session = await res.json();
sessionId = session.id;
const wsBase = base.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
wsUrl = `${wsBase}/api/terminals/${sessionId}`;
} else {
// System terminal — proxy through Open WebUI backend
const base = info.baseUrl.replace(/\/$/, '');
authToken = token;
// Create session via proxy
const res = await fetch(`${base}/terminals/${info.serverId}/api/terminals`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Failed to create session: ${res.status}`);
const session = await res.json();
sessionId = session.id;
const wsBase = base.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
wsUrl = `${wsBase}/terminals/${info.serverId}/api/terminals/${sessionId}`;
}
ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
// First-message auth (no token in URL)
if (ws) {
ws.send(JSON.stringify({ type: 'auth', token: authToken }));
}
connected = true;
connecting = false;
// Send initial resize
if (term && ws) {
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
}
};
ws.onmessage = (event) => {
if (term) {
if (event.data instanceof ArrayBuffer) {
term.write(new Uint8Array(event.data));
} else {
term.write(event.data);
}
}
};
ws.onclose = () => {
connected = false;
connecting = false;
if (term) {
term.write('\r\n\x1b[90m[Connection closed]\x1b[0m\r\n');
}
};
ws.onerror = () => {
connected = false;
connecting = false;
};
} catch (err) {
connecting = false;
if (term) {
term.write(`\r\n\x1b[31m[Error: ${err}]\x1b[0m\r\n`);
}
}
};
const disconnect = () => {
if (ws) {
ws.close();
ws = null;
}
connected = false;
connecting = false;
};
const initTerminal = () => {
if (!terminalEl || term) return;
term = new Terminal({
cursorBlink: true,
fontSize: 13,
fontFamily:
"'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, Monaco, 'Courier New', monospace",
theme: {
background: '#1a1b26',
foreground: '#c0caf5',
cursor: '#c0caf5',
cursorAccent: '#1a1b26',
selectionBackground: '#33467c',
selectionForeground: '#c0caf5',
black: '#15161e',
red: '#f7768e',
green: '#9ece6a',
yellow: '#e0af68',
blue: '#7aa2f7',
magenta: '#bb9af7',
cyan: '#7dcfff',
white: '#a9b1d6',
brightBlack: '#414868',
brightRed: '#f7768e',
brightGreen: '#9ece6a',
brightYellow: '#e0af68',
brightBlue: '#7aa2f7',
brightMagenta: '#bb9af7',
brightCyan: '#7dcfff',
brightWhite: '#c0caf5'
},
allowProposedApi: true,
scrollback: 5000
});
fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon());
term.open(terminalEl);
// Fit after a frame so the container has dimensions
requestAnimationFrame(() => {
fitAddon?.fit();
});
// Forward keystrokes to WebSocket
term.onData((data) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data));
}
});
// Forward binary data (e.g. paste with special chars)
term.onBinary((data) => {
if (ws && ws.readyState === WebSocket.OPEN) {
const buffer = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
buffer[i] = data.charCodeAt(i) & 0xff;
}
ws.send(buffer);
}
});
// Handle resize
term.onResize(({ cols, rows }) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
}
});
// Watch container size changes
resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
fitAddon?.fit();
});
});
resizeObserver.observe(terminalEl);
// Auto-connect
connect();
};
// Reconnect when the selected terminal changes
$: if ($selectedTerminalId !== undefined && term) {
// Clear the terminal screen and reconnect to the new server
disconnect();
term.clear();
if ($selectedTerminalId) {
connect();
}
}
onMount(() => {
initTerminal();
});
onDestroy(() => {
disconnect();
resizeObserver?.disconnect();
term?.dispose();
term = null;
fitAddon = null;
});
</script>
<div class="h-full min-h-0 relative">
<div bind:this={terminalEl} class="absolute inset-0 p-1" class:pointer-events-none={overlay} />
</div>
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "الأسم",
"Name and ID are required, please fill them out": "",
+1
View File
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "الأسم",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Име",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "নাম",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "མིང་།",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ime",
"Name and ID are required, please fill them out": "",
+63 -62
View File
@@ -19,11 +19,11 @@
"{{COUNT}} Rows": "{{COUNT}} files",
"{{COUNT}} Sources": "{{COUNT}} fonts",
"{{COUNT}} words": "{{COUNT}} paraules",
"{{COUNT}}d_time_ago": "",
"{{COUNT}}h_time_ago": "",
"{{COUNT}}m_time_ago": "",
"{{COUNT}}w_time_ago": "",
"{{COUNT}}y_time_ago": "",
"{{COUNT}}d_time_ago": "{{COUNT}}d_time_ago",
"{{COUNT}}h_time_ago": "{{COUNT}}h_time_ago",
"{{COUNT}}m_time_ago": "{{COUNT}}m_time_ago",
"{{COUNT}}w_time_ago": "{{COUNT}}w_time_ago",
"{{COUNT}}y_time_ago": "{{COUNT}}y_time_ago",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} a les {{LOCALIZED_TIME}}",
"{{model}} download has been canceled": "La descàrrega del model {{model}} s'ha cancel·lat",
"{{modelName}} profile image": "Imatge del perfil {{modelName}}",
@@ -32,7 +32,7 @@
"{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari",
"*Prompt node ID(s) are required for image generation": "*Els identificadors de nodes d'indicacions són necessaris per a la generació d'imatges",
"1 Source": "1 font",
"1m_time_ago": "",
"1m_time_ago": "1m_time_ago",
"A collaboration channel where people join as members": "Un canal de col·laboració on la gent s'uneix com a membres",
"A discussion channel where access is controlled by groups and permissions": "Un canal de discussió on l'accés està controlat per grups i permisos",
"A new version (v{{LATEST_VERSION}}) is now available.": "Hi ha una nova versió disponible (v{{LATEST_VERSION}}).",
@@ -43,7 +43,7 @@
"Accept Autocomplete Generation\nJump to Prompt Variable": "Accepta la generació d'autocompleció\nVés a la variable de la indicació",
"Access": "Accés",
"Access Control": "Control d'accés",
"Access Grants": "",
"Access Grants": "Assignacions d'accés",
"Access List": "Llista d'accés",
"Accessible to all users": "Accessible a tots els usuaris",
"Account": "Compte",
@@ -80,14 +80,14 @@
"Add Reaction": "Afegir reacció",
"Add tag": "Afegir etiqueta",
"Add Tag": "Afegir etiqueta",
"Add Terminal": "",
"Add Terminal Connection": "",
"Add Terminal": "Afegir terminal",
"Add Terminal Connection": "Afegir connexió a terminal",
"Add text content": "Afegir contingut de text",
"Add to favorites": "Afegir als favorits",
"Add User": "Afegir un usuari",
"Add User Group": "Afegir grup d'usuaris",
"Add webpage": "Afegir pàgina web",
"Add your Open Terminal URL and API key in Settings → Integrations.": "",
"Add your Open Terminal URL and API key in Settings → Integrations.": "Afegeix l'URL i la clau API de l'Open Terminal a Configuració → Integracions.",
"Additional Config": "Configuració addicional",
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "Opcions de configuració addicionals per al marcador. Hauria de ser una cadena JSON amb parelles clau-valor. Per exemple, '{\"key\": \"value\"}'. Les claus compatibles inclouen: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level",
"Additional feedback comments": "Comentaris addicionals de retorn",
@@ -100,7 +100,7 @@
"Admin Panel": "Panell d'administració",
"Admin Settings": "Preferències d'administració",
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Els administradors tenen accés a totes les eines en tot moment; els usuaris necessiten eines assignades per model a l'espai de treball.",
"Advanced": "",
"Advanced": "Avançat",
"Advanced Parameters": "Paràmetres avançats",
"Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)": "Paràmetres avançats per a l'anàlisi sintàctica de MinerU (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)",
"Advanced Params": "Paràmetres avançats",
@@ -108,10 +108,10 @@
"AI": "IA",
"All": "Tots",
"All chats have been unarchived.": "Tots els xats han estat desarxivats.",
"All models are now hidden": "",
"All models are now visible": "",
"All models are now hidden": "Tots els models estan amagats, ara",
"All models are now visible": "Tots els models són visibles, ara",
"All models deleted successfully": "Tots els models s'han eliminat correctament",
"All time": "",
"All time": "Sempre",
"All Users": "Tots els usuaris",
"Allow Call": "Permetre la trucada",
"Allow Chat Controls": "Permetre els controls de xat",
@@ -129,7 +129,7 @@
"Allow non-local voices": "Permetre veus no locals",
"Allow Rate Response": "Permetre valorar les respostes",
"Allow Regenerate Response": "Permetre regenerar respostes",
"Allow Sharing With Users": "",
"Allow Sharing With Users": "Permetre compartir amb usuaris",
"Allow Speech to Text": "Permetre Parla a Text",
"Allow Temporary Chat": "Permetre el xat temporal",
"Allow Text to Speech": "Permetre Text a Parla",
@@ -380,8 +380,8 @@
"Confirm your new password": "Confirma la teva nova contrasenya",
"Confirm Your Password": "Confirma la teva contrasenya",
"Connect to an AI provider to start chatting": "Connectar a un proveidor d'IA per començar a xerrar",
"Connect to Open Terminal instances to browse files and use them as always-on tools. Only one can be active at a time.": "",
"Connect to Open Terminal instances. All users will have access to file browsing and terminal tools through these servers.": "",
"Connect to Open Terminal instances to browse files and use them as always-on tools. Only one can be active at a time.": "Connecta't a instàncies d'Open Terminal per navegar per fitxers i utilitzar-los com a eines sempre actives. Només una pot estar activa alhora.",
"Connect to Open Terminal instances. All users will have access to file browsing and terminal tools through these servers.": "Connecta't a instàncies d'Open Terminal. Tots els usuaris tindran accés a la navegació de fitxers i a les eines del terminal a través d'aquests servidors.",
"Connect to your own OpenAI compatible API endpoints.": "Connecta als teus propis punts de connexió de l'API compatible amb OpenAI",
"Connect to your own OpenAPI compatible external tool servers.": "Connecta als teus propis servidors d'eines externs compatibles amb OpenAPI",
"Connection failed": "La connexió ha fallat",
@@ -423,7 +423,7 @@
"Copy URL": "Copiar la URL",
"Copying to clipboard was successful!": "La còpia al porta-retalls s'ha realitzat correctament",
"CORS must be properly configured by the provider to allow requests from Open WebUI.": "CORS ha de ser configurat correctament pel proveïdor per permetre les sol·licituds d'Open WebUI",
"Could not read file.": "",
"Could not read file.": "No s'ha pogut llegir l'arxiu",
"Create": "Crear",
"Create a knowledge base": "Crear una base de coneixement",
"Create a model": "Crear un model",
@@ -454,7 +454,7 @@
"Custom Gender": "Gènere personalitzat",
"Custom Parameter Name": "Nom del paràmetre personalitzat",
"Custom Parameter Value": "Valor del paràmetre personalitzat",
"Daily Messages": "",
"Daily Messages": "Missatges diaris",
"Danger Zone": "Zona de perill",
"Dark": "Fosc",
"Data Controls": "Controls de dades",
@@ -577,7 +577,7 @@
"Downloading stats...": "Descarregant estadístiques...",
"Draw": "Dibuixar",
"Drop any files here to upload": "Arrossega aquí qualsevol fitxer per pujar-lo",
"Drop files here to upload": "",
"Drop files here to upload": "Deixa anar els arxius aquí per pujar-los",
"DuckDuckGo": "DuckDuckGo",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
"e.g. 'low', 'medium', 'high'": "p. ex. 'baix', 'mitjà', 'alt'",
@@ -612,7 +612,7 @@
"Edit Last Message": "Editar el darrer missatge",
"Edit Memory": "Editar la memòria",
"Edit Prompt": "Editar la indicació",
"Edit Terminal Connection": "",
"Edit Terminal Connection": "Editar la connexió al terminal",
"Edit User": "Editar l'usuari",
"Edit User Group": "Editar el grup d'usuaris",
"Edit workflow.json content": "Editar el contingut de workflow.json",
@@ -822,10 +822,10 @@
"Fade Effect for Streaming Text": "Efecte de fos a negre per al text en streaming",
"Failed to add file.": "No s'ha pogut afegir l'arxiu.",
"Failed to add members": "No s'han pogut afegir el membres",
"Failed to attach file": "",
"Failed to attach file": "No s'ha pogut adjuntar l'arxiu",
"Failed to clear status": "No s'ha pogut esborar l'estat",
"Failed to connect to {{URL}} OpenAPI tool server": "No s'ha pogut connecta al servidor d'eines OpenAPI {{URL}}",
"Failed to connect to {{URL}} terminal server": "",
"Failed to connect to {{URL}} terminal server": "No s'ha pogut connecta al servidor de terminal {{URL}}",
"Failed to copy link": "No s'ha pogut copiar l'enllaç",
"Failed to create API Key.": "No s'ha pogut crear la clau API.",
"Failed to delete note": "No s'ha pogut eliminar la nota",
@@ -848,7 +848,7 @@
"Failed to save connections": "No s'han pogut desar les connexions",
"Failed to save conversation": "No s'ha pogut desar la conversa",
"Failed to save models configuration": "No s'ha pogut desar la configuració dels models",
"Failed to save terminal servers": "",
"Failed to save terminal servers": "No s'han pogut desar els servidors de terminal",
"Failed to unshare chat.": "No s'ha pogut deixar de compartir el xat.",
"Failed to update settings": "No s'han pogut actualitzar les preferències",
"Failed to update status": "No s'ha pogut actualitzar l'estat",
@@ -865,14 +865,14 @@
"Female": "Dona",
"File": "Arxiu",
"File added successfully.": "L'arxiu s'ha afegit correctament.",
"File attached to chat": "",
"File browser": "",
"File attached to chat": "L'arxiu s'ha adjuntat al xat",
"File browser": "Navegador d'arxius",
"File content": "Contingut de l'arxiu",
"File content updated successfully.": "El contingut de l'arxiu s'ha actualitzat correctament.",
"File Context": "Contingut de l'arxiu",
"File deleted successfully.": "L'arxiu s'ha eliminat correctament",
"File Mode": "Mode d'arxiu",
"File name": "",
"File name": "Nom d'arxiu",
"File not found.": "No s'ha trobat l'arxiu.",
"File removed successfully.": "Arxiu eliminat correctament.",
"File size should not exceed {{maxSize}} MB.": "La mida del fitxer no ha de superar els {{maxSize}} MB.",
@@ -895,7 +895,7 @@
"Folder Background Image": "Imatge del fons de la carpeta",
"Folder deleted successfully": "Carpeta eliminada correctament",
"Folder Max File Count": "Nombre màxim d'arxius per carpeta",
"Folder name": "",
"Folder name": "Nom de carpeta",
"Folder Name": "Nom de la carpeta",
"Folder name cannot be empty.": "El nom de la carpeta no pot ser buit.",
"Folder name updated successfully": "Nom de la carpeta actualitzat correctament",
@@ -984,7 +984,7 @@
"Hex Color - Leave empty for default color": "Color hexadecimal - Deixar buit per a color per defecte",
"Hidden": "Amagat",
"Hide": "Amaga",
"Hide All": "",
"Hide All": "Amagar tots",
"Hide from Sidebar": "Amagar de la barra lateral",
"Hide Model": "Amagar el model",
"High": "Alt",
@@ -992,7 +992,7 @@
"History": "Historial",
"Home": "Inici",
"Host": "Servidor",
"Hourly Messages": "",
"Hourly Messages": "Missatges horaris",
"How can I help you today?": "Com et puc ajudar avui?",
"How would you rate this response?": "Com avaluaries aquesta resposta?",
"HTML": "HTML",
@@ -1103,19 +1103,19 @@
"Landing Page Mode": "Mode de la pàgina d'entrada",
"Language": "Idioma",
"Language Locales": "Localització d'idiomes",
"Last 24 hours": "",
"Last 30 days": "",
"Last 7 days": "",
"Last 90 days": "",
"Last 24 hours": "Darreres 24 hores",
"Last 30 days": "Darrers 30 dies",
"Last 7 days": "Darrers 7 dies",
"Last 90 days": "Darrers 90 dies",
"Last Active": "Activitat recent",
"Last Modified": "Modificació",
"Last reply": "Darrera resposta",
"LDAP": "LDAP",
"LDAP server updated": "Servidor LDAP actualitzat",
"Leaderboard": "Tauler de classificació",
"Learn more": "",
"Learn more": "Aprèn-ne més",
"Learn More": "Aprendre'n més",
"Learn more about Open Terminal": "",
"Learn more about Open Terminal": "Aprèn més sobre Open Terminal",
"Learn more about OpenAPI tool servers.": "Aprèn més sobre els servidors d'eines OpenAPI.",
"Learn more about Voxtral transcription.": "Aprèn més sobre la transcripció amb Voxtral.",
"Leave a public review for {{modelName}}": "Deixa un comentari públic per a {{modelName}}",
@@ -1257,7 +1257,8 @@
"More options": "Més opcions",
"More Options": "Més opcions",
"Move": "Moure",
"My Terminal": "",
"Moved {{name}}": "",
"My Terminal": "El meu terminal",
"Name": "Nom",
"Name and ID are required, please fill them out": "El nom i l'ID són necessaris, emplena'ls, si us plau",
"Name your knowledge base": "Anomena la teva base de coneixement",
@@ -1265,7 +1266,7 @@
"New": "Nou",
"New Button": "Botó nou",
"New Chat": "Nou xat",
"New File": "",
"New File": "Nou arxiu",
"New Folder": "Nova carpeta",
"New Function": "Nova funció",
"New Group": "Nou grup",
@@ -1276,7 +1277,7 @@
"New Prompt": "Nova indicació",
"New Skill": "Nova habilitat",
"New Temporary Chat": "Nou xat temporal",
"New Terminal": "",
"New Terminal": "Nou terminal",
"New Tool": "Nova eina",
"New Webhook": "Nou webhook",
"new-channel": "nou-canal",
@@ -1323,9 +1324,9 @@
"No source available": "Sense font disponible",
"No sources found": "No s'han trobat fonts",
"No suggestion prompts": "Cap prompt suggerit",
"No Terminal connection configured.": "",
"No terminal connections configured.": "",
"No tool server connections configured.": "",
"No Terminal connection configured.": "No hi ha cap configuració de terminal configurada.",
"No terminal connections configured.": "No hi ha connexions de terminal configurades.",
"No tool server connections configured.": "No hi ha connexions a servidors d'eines configurades.",
"No tools found": "No s'han trobat eines",
"No users were found.": "No s'han trobat usuaris",
"No valves": "No hi ha valves",
@@ -1384,7 +1385,7 @@
"Open Model Selector": "Obrir el selector de models",
"Open Settings": "Obrir les preferències",
"Open Sidebar": "Obre la barra lateral",
"Open Terminal": "",
"Open Terminal": "Obrir el terminal",
"Open User Profile Menu": "Obre el menú de perfil d'usuari",
"Open WebUI can use tools provided by any OpenAPI server.": "Open WebUI pot utilitzar eines de servidors OpenAPI.",
"Open WebUI uses faster-whisper internally.": "Open WebUI utilitza faster-whisper internament.",
@@ -1524,7 +1525,7 @@
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "Redueix la probabilitat de generar ximpleries. Un valor més alt (p. ex. 100) donarà respostes més diverses, mentre que un valor més baix (p. ex. 10) serà més conservador.",
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
"Reference Chats": "Xats de referència",
"Refresh": "",
"Refresh": "Refrescar",
"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
"Regenerate": "Regenerar",
"Regenerate Menu": "Regenerar el menú",
@@ -1718,7 +1719,7 @@
"Show": "Mostrar",
"Show \"What's New\" modal on login": "Veure 'Què hi ha de nou' a l'entrada",
"Show Admin Details in Account Pending Overlay": "Mostrar els detalls de l'administrador a la superposició del compte pendent",
"Show All": "",
"Show All": "Mostrar tot",
"Show all ({{COUNT}} characters)": "Mostra tot ({{COUNT}} caràcters",
"Show Files": "Mostra els arxius",
"Show Formatting Toolbar": "Mostrar la barra de format",
@@ -1830,8 +1831,8 @@
"Temperature": "Temperatura",
"Temporary Chat": "Xat temporal",
"Temporary Chat by Default": "Xat temporal per defecte",
"Terminal": "",
"Terminal servers saved": "",
"Terminal": "Terminal",
"Terminal servers saved": "Servidors de terminal desats",
"Text Splitter": "Separador de text",
"Text-to-Speech": "Text-a-veu",
"Text-to-Speech Engine": "Motor de text a veu",
@@ -1865,7 +1866,7 @@
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden desades de manera segura a la teva base de dades. Gràcies!",
"This feature is currently experimental and may not work as expected.": "Aquesta funció és actualment experimental i és possible que no funcioni com s'esperava.",
"This feature is experimental and may be modified or discontinued without notice.": "Aquesta funció és experimental i es pot modificar o deixar de ser disponible sense previ avís.",
"This folder is empty": "",
"This folder is empty": "Aquesta carpeta està buida",
"This is a default user permission and will remain enabled.": "Aquest és un permís d'usuari per defecte i romandrà habilitat.",
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Aquesta és una funció experimental, és possible que no funcioni com s'espera i està subjecta a canvis en qualsevol moment.",
"This model is not publicly available. Please select another model.": "Aquest model no està disponible públicament. Seleccioneu-ne un altre.",
@@ -1907,15 +1908,15 @@
"Toast notifications for new updates": "Notificacions Toast de noves actualitzacions",
"Today": "Avui",
"Today at {{LOCALIZED_TIME}}": "Avui a les {{LOCALIZED_TIME}}",
"Toggle {{COUNT}} sources": "",
"Toggle 1 source": "",
"Toggle Dictation": "",
"Toggle Sidebar": "Alterna la barra lateral",
"Toggle status history": "",
"Toggle {{COUNT}} sources": "Activa/Desactiva {{COUNT}} fonts",
"Toggle 1 source": "Activa/Desactiva 1 font",
"Toggle Dictation": "Activa/Desactiva el dictat",
"Toggle Sidebar": "Activa/Desactiva la barra lateral",
"Toggle status history": "Activa/Desactiva l'estat de l'històric",
"Toggle whether current connection is active.": "Alterna si la connexió actual està activa.",
"Token": "Token",
"Token counts are estimates and may not reflect actual API usage": "El nombre de tokens és estimat i pot no reflectir l'ús real de l'API.",
"tokens": "",
"tokens": "tokens",
"Tokens": "Tokens",
"Too verbose": "Massa explicit",
"Tool created successfully": "Eina creada correctament",
@@ -1981,12 +1982,12 @@
"Upload Files": "Pujar fitxers",
"Upload Model": "Pujar model",
"Upload Pipeline": "Pujar una Pipeline",
"Upload profile image": "",
"Upload profile image": "Pujar imatge de perfil",
"Upload Progress": "Progrés de càrrega",
"Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "Progrés de la pujada: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)",
"Uploaded files or images": "Arxius o imatges pujats",
"Uploading file...": "Pujant l'arxiu...",
"Uploading...": "",
"Uploading...": "Pujant...",
"URL": "URL",
"URL is required": "La URL és necessaria",
"URL Mode": "Mode URL",
@@ -1995,7 +1996,7 @@
"Use '#' in the prompt input to load and include your knowledge.": "Utilitza '#' a l'entrada de la indicació per carregar i incloure els teus coneixements.",
"Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "Fes servir l'endpoint /v1/chat/completions en comptes de /v1/audio/transcriptions per a una precisió potencialment millor.",
"Use Chat Completions API": "Utilitza l'API de completació de xat",
"Use groups to organize your users and assign permissions.": "",
"Use groups to organize your users and assign permissions.": "Utilitza grups per organitzar els usuaris i assignar permisos.",
"Use LLM": "Utilizar model de llenguatge",
"Use no proxy to fetch page contents.": "No utilitzis un proxy per obtenir contingut de la pàgina.",
"Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.": "Utilitza el proxy designat per les variables d'entorn http_proxy i https_proxy per obtenir el contingut de la pàgina.",
@@ -2029,8 +2030,8 @@
"Version deleted": "Versió eliminada",
"View Replies": "Veure les respostes",
"View Result from **{{NAME}}**": "Veure el resultat de **{{NAME}}**",
"View source: {{name}}": "",
"View source: {{title}}": "",
"View source: {{name}}": "Veure font: {{name}}",
"View source: {{title}}": "Veure font: {{title}}",
"Visibility": "Visibilitat",
"Visible": "Visible",
"Visible to all users": "Visible per a tots els usuaris",
@@ -2040,7 +2041,7 @@
"Voice mode": "Mode de veu",
"Voice Mode Custom Prompt": "Indicació personalitzada per al mode de veu",
"Voice Mode Prompt": "Indicació per al mode de veu",
"Waiting for upload...": "",
"Waiting for upload...": "Esperant per pujar...",
"Warning": "Avís",
"Warning:": "Avís:",
"Warning: Enabling this will allow users to upload arbitrary code on the server.": "Avís: Habilitar això permetrà als usuaris penjar codi arbitrari al servidor.",
@@ -2108,11 +2109,11 @@
"You do not have permission to send messages in this thread.": "No tens permís per enviar missatges en aquest fil.",
"You do not have permission to upload files to this knowledge base.": "No tens permís per carregar fitxers a aquesta base de coneixements.",
"You do not have permission to upload files.": "No tens permisos per pujar arxius.",
"You do not have permission to upload web content.": "",
"You do not have permission to upload web content.": "No tens permisos per pujar contingut web",
"You have no archived conversations.": "No tens converses arxivades.",
"You have no shared conversations.": "No has compartit cap conversa.",
"You have shared this chat": "Has compartit aquest xat",
"You.com API Key": "",
"You.com API Key": "Clau API de You.com",
"You're a helpful assistant.": "Ets un assistent útil.",
"You're now logged in.": "Ara estàs connectat.",
"Your Account": "El teu compte",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ngalan",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Další možnosti",
"Move": "Přesunout",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Jméno",
"Name and ID are required, please fill them out": "Jméno a ID jsou povinné, prosím vyplňte je",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Flere muligheder",
"Move": "Flyt",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Navn",
"Name and ID are required, please fill them out": "Navn og ID er påkrævet, venligst udfyld dem",
@@ -1257,6 +1257,7 @@
"More options": "Mehr Optionen",
"More Options": "Mehr Optionen",
"Move": "Verschieben",
"Moved {{name}}": "",
"My Terminal": "Meine Terminals",
"Name": "Name",
"Name and ID are required, please fill them out": "Name und ID sind erforderlich, bitte füllen Sie diese aus",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Name",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "Μετακίνηση",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Όνομα",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "Más Opciones",
"More Options": "Más Opciones",
"Move": "Mover",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nombre",
"Name and ID are required, please fill them out": "Nombre e ID requeridos, por favor introducelos",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Rohkem valikuid",
"Move": "Teisalda",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nimi",
"Name and ID are required, please fill them out": "Nimi ja ID on nõutavad, palun täida need",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Izena",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "گزینه\u200cهای بیشتر",
"Move": "انتقال",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "نام",
"Name and ID are required, please fill them out": "نام و شناسه مورد نیاز هستند، لطفاً آنها را پر کنید",
@@ -1257,6 +1257,7 @@
"More options": "Lisää vaihtoehtoja",
"More Options": "Lisää vaihtoehtoja",
"Move": "Siirrä",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nimi",
"Name and ID are required, please fill them out": "Nimi ja ID vaaditaan, täytä puuttuvat kentät",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nom d'utilisateur",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Plus d'options",
"Move": "Déplacer",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nom d'utilisateur",
"Name and ID are required, please fill them out": "Le nom et l'identifiant sont obligatoires, veuillez les remplir",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nombre",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "שם",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "नाम",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ime",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Név",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nama",
"Name and ID are required, please fill them out": "",
File diff suppressed because it is too large Load Diff
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nome",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "詳細オプション",
"Move": "移動",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "名前",
"Name and ID are required, please fill them out": "名前とIDは必須です。項目を入力してください。",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "მეტი პარამეტრები",
"Move": "გადატანა",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "სახელი",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Ugar n textiṛiyin",
"Move": "Senkez",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Isem",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "추가 설정",
"Move": "이동",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "이름",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Pavadinimas",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Vairāk opciju",
"Move": "Pārvietot",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nosaukums",
"Name and ID are required, please fill them out": "Nosaukums un ID ir nepieciešami, lūdzu, aizpildiet tos",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nama",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Navn",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Naam",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "ਨਾਮ",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Więcej opcji",
"Move": "Przenieś",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nazwa",
"Name and ID are required, please fill them out": "Nazwa i ID są wymagane",
@@ -1257,6 +1257,7 @@
"More options": "Mais opções",
"More Options": "Mais opções",
"Move": "Mover",
"Moved {{name}}": "",
"My Terminal": "Meu Terminal",
"Name": "Nome",
"Name and ID are required, please fill them out": "Nome e ID são obrigatórios, por favor preencha-os",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nome",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Nume",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Больше опций",
"Move": "Переместить",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Имя",
"Name and ID are required, please fill them out": "Имя и ID обязательны, пожалуйста, заполните их",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "Presunúť",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Meno",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Име",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "Fler alternativ",
"Move": "Flytta",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Namn",
"Name and ID are required, please fill them out": "Namn och ID krävs, fyll i dem",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "ตัวเลือกเพิ่มเติม",
"Move": "ย้าย",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "ชื่อ",
"Name and ID are required, please fill them out": "จำเป็นต้องกรอกชื่อและ ID โปรดกรอกข้อมูลให้ครบ",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ady",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ad",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "ئات",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ім'я",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "نام",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Исм",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Ism",
"Name and ID are required, please fill them out": "",
@@ -1257,6 +1257,7 @@
"More options": "",
"More Options": "",
"Move": "",
"Moved {{name}}": "",
"My Terminal": "",
"Name": "Tên",
"Name and ID are required, please fill them out": "",
+3 -2
View File
@@ -872,7 +872,7 @@
"File Context": "文件上下文",
"File deleted successfully.": "文件已成功删除。",
"File Mode": "文件模式",
"File name": "",
"File name": "文件名",
"File not found.": "文件未找到。",
"File removed successfully.": "文件成功删除",
"File size should not exceed {{maxSize}} MB.": "文件大小不应超过 {{maxSize}} MB",
@@ -1257,6 +1257,7 @@
"More options": "更多选项",
"More Options": "更多选项",
"Move": "移动",
"Moved {{name}}": "",
"My Terminal": "我的终端",
"Name": "名称",
"Name and ID are required, please fill them out": "名称和 ID 是必填项,请填写。",
@@ -1265,7 +1266,7 @@
"New": "最新",
"New Button": "新按钮",
"New Chat": "新对话",
"New File": "",
"New File": "新建文件",
"New Folder": "创建分组",
"New Function": "新函数",
"New Group": "新建权限组",
+3 -2
View File
@@ -872,7 +872,7 @@
"File Context": "檔案上下文",
"File deleted successfully.": "檔案已成功刪除。",
"File Mode": "檔案模式",
"File name": "",
"File name": "檔案名稱",
"File not found.": "未找到檔案。",
"File removed successfully.": "成功移除檔案。",
"File size should not exceed {{maxSize}} MB.": "檔案大小不應超過 {{maxSize}} MB。",
@@ -1257,6 +1257,7 @@
"More options": "更多選項",
"More Options": "更多選項",
"Move": "移動",
"Moved {{name}}": "",
"My Terminal": "我的終端",
"Name": "名稱",
"Name and ID are required, please fill them out": "名稱和 ID 為必填項目,請填寫",
@@ -1265,7 +1266,7 @@
"New": "最新",
"New Button": "新按鈕",
"New Chat": "新增對話",
"New File": "",
"New File": "新增檔案",
"New Folder": "新增資料夾",
"New Function": "新增函式",
"New Group": "新增權限群組",