diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py
index 986620afe..38398168b 100644
--- a/backend/open_webui/routers/configs.py
+++ b/backend/open_webui/routers/configs.py
@@ -216,12 +216,14 @@ async def set_tool_servers_config(
class TerminalServerConnection(BaseModel):
- id: str
+ id: Optional[str] = ""
url: str
+ path: Optional[str] = "/openapi.json"
+ auth_type: Optional[str] = "bearer"
key: Optional[str] = ""
name: Optional[str] = ""
- auth_type: Optional[str] = "bearer"
- config: Optional[dict] = None # holds access_grants, etc.
+ enabled: Optional[bool] = True
+ config: Optional[dict] = None
model_config = ConfigDict(extra="allow")
diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py
index f8a03a9e3..f7fe90984 100644
--- a/backend/open_webui/routers/terminals.py
+++ b/backend/open_webui/routers/terminals.py
@@ -35,7 +35,7 @@ async def list_terminal_servers(request: Request, user=Depends(get_verified_user
return [
{"id": connection.get("id", ""), "url": connection.get("url", ""), "name": connection.get("name", "")}
for connection in connections
- if has_connection_access(user, connection, user_group_ids)
+ if connection.get("enabled", True) and has_connection_access(user, connection, user_group_ids)
]
diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py
index 72baf3015..d7f36937e 100644
--- a/backend/open_webui/utils/misc.py
+++ b/backend/open_webui/utils/misc.py
@@ -210,7 +210,8 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
content = ""
for part in output_parts:
if part.get("type") == "input_text":
- content += part.get("text", "")
+ text_val = part.get("text", "")
+ content += str(text_val) if not isinstance(text_val, str) else text_val
messages.append(
{
diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py
index b89a14526..967f821d9 100644
--- a/backend/open_webui/utils/tools.py
+++ b/backend/open_webui/utils/tools.py
@@ -900,26 +900,27 @@ async def set_terminal_servers(request: Request):
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
- # Build server configs with info containing the connection ID
+ # Build server configs compatible with get_tool_servers_data
+ # Terminal connections store id/name at top level; translate to info dict
server_configs = []
for connection in connections:
- conn_id = connection.get("id", "")
if not connection.get("url"):
continue
- auth_type = connection.get("auth_type", "bearer")
- token = None
- if auth_type == "bearer":
- token = connection.get("key", "")
+ enabled = connection.get("enabled", True)
server_configs.append({
"url": connection.get("url", ""),
- "key": token or "",
- "auth_type": auth_type,
- "path": "openapi.json",
+ "key": connection.get("key", ""),
+ "auth_type": connection.get("auth_type", "bearer"),
+ "path": connection.get("path", "/openapi.json"),
"spec_type": "url",
- "config": {"enable": True},
- "info": {"id": conn_id, "name": connection.get("name", "")},
+ # get_tool_servers_data reads config.enable to filter active servers
+ "config": {"enable": enabled},
+ "info": {
+ "id": connection.get("id", ""),
+ "name": connection.get("name", ""),
+ },
})
request.app.state.TERMINAL_SERVERS = await get_tool_servers_data(server_configs)
diff --git a/src/lib/components/admin/Settings/Integrations.svelte b/src/lib/components/admin/Settings/Integrations.svelte
index ced8de7d9..9e7529f4c 100644
--- a/src/lib/components/admin/Settings/Integrations.svelte
+++ b/src/lib/components/admin/Settings/Integrations.svelte
@@ -35,7 +35,7 @@
let showConnectionModal = false;
// Terminal server admin connections
- let terminalConnections: { id: string; url: string; key: string; name: string }[] = [];
+ let terminalConnections = [];
let showAddTerminalModal = false;
let editTerminalIdx: number | null = null;
let showDeleteTerminalConfirm = false;
@@ -72,20 +72,17 @@
}
};
- const addTerminalConnection = (server: { url: string; key: string; name?: string }) => {
+ const addTerminalConnection = (server) => {
terminalConnections = [
...terminalConnections,
- { id: uuidv4(), url: server.url, key: server.key, name: server.name ?? '' }
+ { ...server, id: server.id ?? uuidv4() }
];
saveTerminalServers();
};
- const updateTerminalConnection = (
- idx: number,
- updated: { url: string; key: string; name?: string }
- ) => {
+ const updateTerminalConnection = (idx: number, updated) => {
terminalConnections = terminalConnections.map((c, i) =>
- i === idx ? { ...c, url: updated.url, key: updated.key, name: updated.name ?? '' } : c
+ i === idx ? { ...c, ...updated, id: updated.id ?? c.id } : c
);
saveTerminalServers();
};
@@ -191,15 +188,15 @@
{/each}
- {#if servers.length === 0}
-
- {$i18n.t('No tool server connections configured.')}
-
- {/if}
+ {#if servers.length === 0}
+
+ {$i18n.t('No tool server connections configured.')}
+
+ {/if}
-
-
- {$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
+
+
+ {$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
@@ -235,7 +232,7 @@
-
+
@@ -260,6 +257,18 @@
+
+
+ {
+ terminalConnections = terminalConnections.map((c, i) =>
+ i === idx ? { ...c, enabled: !(c?.enabled !== false) } : c
+ );
+ saveTerminalServers();
+ }}
+ />
+
{/each}
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index 933846afa..74d0ce4de 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -42,12 +42,12 @@
functions,
selectedFolder,
pinnedChats,
- showEmbeds
+ showEmbeds,
+ selectedTerminalId
} from '$lib/stores';
import { WEBUI_API_BASE_URL } from '$lib/constants';
-
import {
convertMessagesToHistory,
copyToClipboard,
@@ -140,13 +140,18 @@
let selectedToolIds = [];
let selectedFilterIds = [];
+
let imageGenerationEnabled = false;
let webSearchEnabled = false;
let codeInterpreterEnabled = false;
- // Auto-inject terminal servers into selected tool IDs so they act like toggled-on tools
+ // 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 terminalIds = $terminalServers.map((_, i) => `direct_server:terminal_${i}`);
+ 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];
@@ -155,7 +160,10 @@
// Remove disabled terminal servers from selectedToolIds automatically
$: if (selectedToolIds.length > 0) {
- const terminalIds = ($terminalServers ?? []).map((_, i) => `direct_server:terminal_${i}`);
+ 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)
);
@@ -354,9 +362,12 @@
selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:'));
}
- // Auto-inject terminal servers
+ // Auto-inject direct terminal servers (system ones are handled via terminal_id)
if ($terminalServers && $terminalServers.length > 0) {
- const terminalIds = $terminalServers.map((_, i) => `direct_server:terminal_${i}`);
+ const directTerminalServers = $terminalServers.filter((t) => !t.id);
+ const terminalIds = directTerminalServers.map(
+ (_, i) => `direct_server:terminal_${$terminalServers.indexOf(directTerminalServers[i])}`
+ );
selectedToolIds = [...new Set([...selectedToolIds, ...terminalIds])];
}
@@ -650,6 +661,14 @@
audioQueue.set(new AudioQueue(document.getElementById('audioElement')));
+ // Reset direct terminal enabled states — selectedTerminalId starts null on every page load
+ if ($settings?.terminalServers?.some((s) => s.enabled)) {
+ settings.set({
+ ...$settings,
+ terminalServers: ($settings.terminalServers ?? []).map((s) => ({ ...s, enabled: false }))
+ });
+ }
+
pageSubscribe = page.subscribe(async (p) => {
if (p.url.pathname === '/') {
await tick();
@@ -2130,9 +2149,8 @@
});
}
- // Determine the active terminal (first accessible backend terminal, or user-configured)
- const activeTerminal = $terminalServers?.[0] ?? null;
- const activeTerminalId = activeTerminal?.id ?? null;
+ // Use the user-selected terminal from the dropdown
+ const activeTerminalId = $selectedTerminalId ?? null;
const res = await generateOpenAIChatCompletion(
localStorage.token,
diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte
index b2eb3354f..e5398f7f9 100644
--- a/src/lib/components/chat/ChatControls.svelte
+++ b/src/lib/components/chat/ChatControls.svelte
@@ -17,7 +17,9 @@
showArtifacts,
showEmbeds,
settings,
- showFileNavPath
+ showFileNavPath,
+ selectedTerminalId,
+ user
} from '$lib/stores';
import { uploadFile } from '$lib/apis/files';
@@ -59,10 +61,24 @@
// Tab state for Controls+Files panel
let activeTab: 'controls' | 'files' | 'overview' = savedTab;
$: savedTab = activeTab;
- $: hasTerminal = !!($settings?.terminalServers ?? []).find((s) => s.enabled)?.url
- || $terminalServers.length > 0;
$: hasMessages = history?.messages && Object.keys(history.messages).length > 0;
- $: if (!hasMessages && activeTab === 'overview') activeTab = 'controls';
+
+ $: showControlsTab = $user?.role === 'admin' || ($user?.permissions?.chat?.controls ?? true);
+ $: showFilesTab = !!$selectedTerminalId;
+ $: showOverviewTab = hasMessages;
+
+ // Tab fallback: if active tab becomes hidden, switch to next available
+ $: if (!showOverviewTab && activeTab === 'overview') activeTab = 'controls';
+ $: if (!showFilesTab && activeTab === 'files') activeTab = 'controls';
+ $: if (!showControlsTab && activeTab === 'controls') {
+ if (showFilesTab) activeTab = 'files';
+ else if (showOverviewTab) activeTab = 'overview';
+ }
+
+ // Auto-close if there are no visible tabs
+ $: if (!showControlsTab && !showFilesTab && !showOverviewTab) {
+ showControls.set(false);
+ }
// Auto-switch to Files tab when display_file is triggered
$: if ($showFileNavPath) {
@@ -70,6 +86,12 @@
showControls.set(true);
}
+ // Auto-open Files tab when a terminal is selected
+ $: if ($selectedTerminalId) {
+ activeTab = 'files';
+ showControls.set(true);
+ }
+
// Attach a terminal file to the chat input
const handleTerminalAttach = async (blob: Blob, name: string, contentType: string) => {
const tempItemId = uuidv4();
@@ -182,7 +204,9 @@
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mouseup', onMouseUp);
- setTimeout(() => { paneReady = true; }, 0);
+ setTimeout(() => {
+ paneReady = true;
+ }, 0);
// If controls were persisted as open, set the pane to the saved size
if ($showControls && pane) {
@@ -247,15 +271,17 @@
-
- {#if hasTerminal}
+ {#if showControlsTab}
+
+ {/if}
+ {#if showFilesTab}