From a06685a47b89fb19dd6124fbe391ff78b54f451d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 29 Mar 2026 21:01:10 -0500 Subject: [PATCH] refac --- backend/open_webui/routers/terminals.py | 4 ++ backend/open_webui/utils/tools.py | 7 +++ src/lib/apis/index.ts | 4 +- src/lib/apis/terminal/index.ts | 21 ++++--- src/lib/components/chat/ChatControls.svelte | 4 +- src/lib/components/chat/FileNav.svelte | 66 +++++++++++++++------ src/lib/components/chat/XTerminal.svelte | 9 ++- src/routes/+layout.svelte | 7 ++- 8 files changed, 86 insertions(+), 36 deletions(-) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 59f1f3ab48..34d5eb96d6 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -105,6 +105,10 @@ async def proxy_terminal( target_url += f'?{request.query_params}' headers = {'X-User-Id': user.id} + # Forward per-session cwd tracking header + session_id = request.headers.get('x-session-id') + if session_id: + headers['X-Session-Id'] = session_id cookies = {} auth_type = connection.get('auth_type', 'bearer') diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index e266e9d9c8..377a81d749 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -1009,6 +1009,13 @@ async def get_terminal_tools( # auth_type == "none": no Authorization header system_prompt = server_data.get('system_prompt') + + # Use chat_id as the per-session key for cwd tracking + metadata = extra_params.get('__metadata__', {}) + session_id = metadata.get('chat_id') + if session_id: + headers['X-Session-Id'] = session_id + terminal_cwd = await get_terminal_cwd(connection.get('url', ''), headers, cookies) tools_dict = {} diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index b07d524cba..c46c86b801 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -454,7 +454,8 @@ export const executeToolServer = async ( url: string, name: string, params: Record, - serverData: { openapi: any; info: any; specs: any } + serverData: { openapi: any; info: any; specs: any }, + sessionId?: string ) => { let error = null; @@ -531,6 +532,7 @@ export const executeToolServer = async ( 'Content-Type': 'application/json', ...(token && { authorization: `Bearer ${token}` }) }; + if (sessionId) headers['X-Session-Id'] = sessionId; const requestOptions: RequestInit = { method: httpMethod.toUpperCase(), diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index 23567baf8e..a01ffa5125 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -45,11 +45,11 @@ export const getTerminalConfig = async ( return res.json().catch(() => null); }; -export const getCwd = async (baseUrl: string, apiKey: string): Promise => { +export const getCwd = async (baseUrl: string, apiKey: string, sessionId?: string): Promise => { const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; - const res = await fetch(url, { - headers: { Authorization: `Bearer ${apiKey}` } - }).catch(() => null); + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { headers }).catch(() => null); if (!res || !res.ok) return null; const json = await res.json().catch(() => null); return json?.cwd ?? null; @@ -218,15 +218,18 @@ export const deleteEntry = async ( export const setCwd = async ( baseUrl: string, apiKey: string, - path: string + path: string, + sessionId?: string ): Promise<{ cwd: string } | null> => { const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; + const headers: Record = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }; + if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, + headers, body: JSON.stringify({ path }) }) .then(async (res) => { diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index f27a307fea..3cbcc7ed87 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -362,7 +362,7 @@ onClose={() => showControls.set(false)} /> {:else if activeTab === 'files' && $selectedTerminalId} - + {:else if activeTab === 'files' && codeInterpreterEnabled} {:else} @@ -513,7 +513,7 @@ onClose={() => showControls.set(false)} /> {:else if activeTab === 'files' && $selectedTerminalId} - + {:else if activeTab === 'files' && codeInterpreterEnabled} {:else} diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 99549aec6b..ef2db742aa 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -49,6 +49,7 @@ export let onAttach: ((blob: Blob, name: string, contentType: string) => void) | null = null; export let overlay = false; + export let chatId: string | null = null; // ── Terminal panel state ──────────────────────────────────────────── let terminalExpanded = false; @@ -215,30 +216,48 @@ return url ? { url, key } : null; }; - // Detect terminal changes — the explicit store references ensure + // Detect terminal or chat changes — the explicit store references ensure // Svelte re-runs this block when any of them update. + // The `mounted` flag prevents the initial run from racing with onMount. let prevTerminalUrl = ''; + let prevChatId = chatId; + let mounted = false; $: { ($selectedTerminalId, $terminalServers, $settings); const terminal = getTerminal(); selectedTerminal = terminal; - if (terminal && terminal.url !== prevTerminalUrl) { - prevTerminalUrl = terminal.url; - loading = true; - error = null; - entries = []; - (async () => { - // Discover server features (terminal enabled/disabled) - const config = await getTerminalConfig(terminal.url, terminal.key); - terminalEnabled = config?.features?.terminal !== false; + const chatChanged = chatId !== prevChatId; + const oldChatId = prevChatId; + if (chatChanged) prevChatId = chatId; - const rawCwd = await getCwd(terminal.url, terminal.key); - const cwd = rawCwd ? normalizePath(rawCwd) : null; - const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/'; - savedPath = dir; - loadDir(dir); - })(); + const terminalChanged = terminal && terminal.url !== prevTerminalUrl; + if (terminalChanged) prevTerminalUrl = terminal.url; + + if (mounted && terminal) { + if (chatChanged && chatId && !oldChatId) { + // Chat just got created (null → real ID): persist the current + // browsed path as the new session's cwd — don't re-fetch. + setCwd(terminal.url, terminal.key, savedPath, chatId); + } else if (terminalChanged || chatChanged) { + // Terminal switched, new chat started, or switched between + // existing chats — re-fetch the session cwd. + loading = true; + error = null; + entries = []; + (async () => { + if (terminalChanged) { + const config = await getTerminalConfig(terminal.url, terminal.key); + terminalEnabled = config?.features?.terminal !== false; + } + + const rawCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined); + const cwd = rawCwd ? normalizePath(rawCwd) : null; + const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/'; + savedPath = dir; + loadDir(dir); + })(); + } } } @@ -316,7 +335,7 @@ loading = false; // Set working directory on the terminal server (fire-and-forget) - setCwd(terminal.url, terminal.key, path); + setCwd(terminal.url, terminal.key, path, chatId ?? undefined); if (result === null) { error = @@ -736,14 +755,22 @@ if (!handledDisplayFile) { loading = true; - if (savedPath === '/') { - const rawCwd = await getCwd(terminal.url, terminal.key); + + // Discover server features on initial mount + const config = await getTerminalConfig(terminal.url, terminal.key); + terminalEnabled = config?.features?.terminal !== false; + + if (chatId || savedPath === '/') { + // Fetch session-specific cwd from the server (or global default for new chats) + const rawCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined); const cwd = rawCwd ? normalizePath(rawCwd) : null; if (cwd) savedPath = cwd.endsWith('/') ? cwd : cwd + '/'; } loadDir(savedPath); } + mounted = true; + const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftKey = true; }; @@ -1368,6 +1395,7 @@ overlay={overlay || isDraggingHandle} bind:connected={terminalConnected} bind:connecting={terminalConnecting} + {chatId} /> {/if} diff --git a/src/lib/components/chat/XTerminal.svelte b/src/lib/components/chat/XTerminal.svelte index eb74a7e1a5..e16beef978 100644 --- a/src/lib/components/chat/XTerminal.svelte +++ b/src/lib/components/chat/XTerminal.svelte @@ -12,6 +12,7 @@ const i18n = getContext('i18n'); export let overlay = false; + export let chatId: string | null = null; let terminalEl: HTMLDivElement; let term: Terminal | null = null; @@ -67,9 +68,11 @@ authToken = apiKey; // Create session + const createHeaders: Record = { Authorization: `Bearer ${apiKey}` }; + if (chatId) createHeaders['X-Session-Id'] = chatId; const res = await fetch(`${base}/api/terminals`, { method: 'POST', - headers: { Authorization: `Bearer ${apiKey}` } + headers: createHeaders }); if (!res.ok) throw new Error(`Failed to create session: ${res.status}`); const session = await res.json(); @@ -83,9 +86,11 @@ authToken = token; // Create session via proxy + const proxyHeaders: Record = { Authorization: `Bearer ${token}` }; + if (chatId) proxyHeaders['X-Session-Id'] = chatId; const res = await fetch(`${base}/terminals/${info.serverId}/api/terminals`, { method: 'POST', - headers: { Authorization: `Bearer ${token}` } + headers: proxyHeaders }); if (!res.ok) throw new Error(`Failed to create session: ${res.status}`); const session = await res.json(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 920e56f96b..b40c9c558b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -375,7 +375,7 @@ return { toolServer, toolServerData, token }; }; - const executeTool = async (data, cb) => { + const executeTool = async (data, cb, chatId) => { const { toolServer, toolServerData, token } = resolveToolServer(data.server?.url); console.log('executeTool', data, toolServer); @@ -386,7 +386,8 @@ toolServer.url, data?.name, data?.params, - toolServerData + toolServerData, + chatId ); console.log('executeToolServer', res); @@ -485,7 +486,7 @@ executePythonAsWorker(data.id, data.code, cb, data.files || []); } else if (type === 'execute:tool') { console.log('execute:tool', data); - executeTool(data, cb); + executeTool(data, cb, event.chat_id); } else if (type === 'request:chat:completion') { console.log(data, $socket.id); const { session_id, channel, form_data, model } = data;