From e3b052ba4a3a48612d17f13fdec2cae75c707f0b Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:28:43 +0200 Subject: [PATCH] fix: scope the terminal ports panel and port preview to the chat With an orchestrator terminal set to Per chat, the Ports section of the file panel queried the user's shared workspace instead of the chat's. Servers started in the chat never showed up, previewing or opening a port went to the shared workspace, and every 5 second poll started the shared workspace container again if it had stopped. The port preview loads in an iframe and "open in new tab" in a new window, and neither can send the chat id header the other file panel requests use. So for chat-scoped terminals the ports panel now talks to `/terminals//chats//...`, and the terminal proxy reads the chat id from that path, going through the same saved-chat, access and path checks as the header. Relative asset requests from the previewed app keep the prefix, so the whole page stays in the chat's workspace, and switching chats refreshes the list right away. A query parameter was the other option, but relative sub-requests drop it and it would be forwarded to the previewed app. Shared connections and personal terminals keep their current URLs. --- backend/open_webui/routers/terminals.py | 4 +++- src/lib/components/chat/FileNav.svelte | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 6b5cd6e1d3..ef6cbd01e2 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -107,6 +107,8 @@ PROXY_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] @router.api_route('/{server_id}/{path:path}', methods=PROXY_METHODS) +# Must stay the lower decorator so it registers before the catch-all, which would swallow chat paths. +@router.api_route('/{server_id}/chats/{chat_id}/{path:path}', methods=PROXY_METHODS) async def proxy_terminal( server_id: str, path: str, @@ -151,7 +153,7 @@ async def proxy_terminal( headers = {'X-User-Id': user.id} # Forward per-session cwd tracking header - session_id = request.headers.get('x-session-id') + session_id = request.path_params.get('chat_id') or request.headers.get('x-session-id') if session_id: headers['X-Session-Id'] = session_id if not terminal_context_available(connection, 'chat'): diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index e2d9aed0af..08085adb7c 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -355,6 +355,11 @@ let selectedTerminal: { url: string; key: string } | null = null; let terminalChatContextPending = false; let terminalChatContextHidden = false; + let scopedChatId: string | null = null; + $: portBaseUrl = + selectedTerminal && scopedChatId + ? `${selectedTerminal.url}/chats/${encodeURIComponent(scopedChatId)}` + : (selectedTerminal?.url ?? ''); const chatContext = (terminal: any) => terminal?.contexts?.chat ?? {}; @@ -364,6 +369,7 @@ : ($terminalServers?.[0] ?? null); const chatConfig = chatContext(systemTerminal); const chatScoped = !!systemTerminal && chatConfig?.context_id === 'chat_id'; + scopedChatId = chatScoped ? chatId : null; terminalChatContextHidden = !!systemTerminal && (chatConfig === false || (chatScoped && isTemporaryChatId(chatId))); terminalChatContextPending = chatScoped && !terminalChatContextHidden && !isSavedChatId(chatId); @@ -1824,7 +1830,7 @@ /> {:else if previewPort !== null} { @@ -2099,7 +2105,7 @@ {#if selectedTerminal && !selectedFile && previewPort === null && !isSearching}
{ selectedFile = null;