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} - {#if hasTerminal} + {#if showControlsTab} + + {/if} + {#if showFilesTab} diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 155a9a5ea..cc9e05f3a 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -28,8 +28,11 @@ showCallOverlay, tools, toolServers, + terminalServers, user as _user, showControls, + showSettings, + selectedTerminalId, TTSWorker, temporaryChatEnabled } from '$lib/stores'; @@ -81,8 +84,13 @@ import Voice from '../icons/Voice.svelte'; import Cloud from '../icons/Cloud.svelte'; import IntegrationsMenu from './MessageInput/IntegrationsMenu.svelte'; + import TerminalMenu from './MessageInput/TerminalMenu.svelte'; import Component from '../icons/Component.svelte'; import PlusAlt from '../icons/PlusAlt.svelte'; + import Dropdown from '../common/Dropdown.svelte'; + + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte'; import Knobs from '../icons/Knobs.svelte'; @@ -124,6 +132,8 @@ export let webSearchEnabled = false; export let codeInterpreterEnabled = false; + let showTerminalMenu = false; + export let messageQueue: { id: string; prompt: string; files: any[] }[] = []; export let onQueueSendNow: (id: string) => void = () => {}; export let onQueueEdit: (id: string) => void = () => {}; @@ -1019,7 +1029,6 @@ }); - - {@const activeTerminal = ($settings?.terminalServers ?? []).find( - (s) => s.enabled - )} - {#if activeTerminal} -
-
- - {activeTerminal.name || - activeTerminal.url.replace(/^https?:\/\//, '')} -
-
- {/if} + + diff --git a/src/lib/components/chat/MessageInput/TerminalMenu.svelte b/src/lib/components/chat/MessageInput/TerminalMenu.svelte new file mode 100644 index 000000000..8703a1e1c --- /dev/null +++ b/src/lib/components/chat/MessageInput/TerminalMenu.svelte @@ -0,0 +1,195 @@ + + + + + + + +
+ + + {#if $user?.role === 'admin' || ($user?.permissions?.features?.direct_tool_servers ?? true)} +
+ + {$i18n.t('Direct')} + + + + +
+ + {#each directTerminals as terminal} + + {/each} + +
+ {/if} + + +
+ + {$i18n.t('System')} + + {#if $user?.role === 'admin'} + + + + {/if} +
+ + {#each systemTerminals as terminal} + + {/each} +
+
+