From 2dadc5435af77b0638af69200e3af1b9654417b6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 9 Aug 2026 13:22:46 -0600 Subject: [PATCH] refac --- backend/open_webui/routers/terminals.py | 14 +++-- src/lib/apis/terminal/index.ts | 10 +++- src/lib/components/chat/FileNav.svelte | 57 +++++++++++++------ .../chat/FileNav/BulkActionBar.svelte | 4 +- .../chat/FileNav/FileEntryRow.svelte | 23 +++++++- .../chat/FileNav/FileNavToolbar.svelte | 19 +++++-- .../chat/FileNav/FilePreview.svelte | 12 ++-- 7 files changed, 101 insertions(+), 38 deletions(-) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 371605b533..d9c586fa45 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -23,6 +23,7 @@ from open_webui.utils.json_codec import JSONCodec from open_webui.utils.terminals import ( TERMINAL_CONTEXT_HEADER, get_terminal_server_url, + is_terminal_orchestrator, terminal_context_available, terminal_context_config, terminal_context_id, @@ -233,7 +234,7 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str): The client must send ``{"type": "auth", "token": ""}`` as its first message after connecting. - Returns ``(user, connection, chat_id)`` on success, or ``None`` after + Returns ``(user, connection, chat_id, token)`` on success, or ``None`` after closing *ws* with an appropriate error code. """ import asyncio @@ -247,7 +248,8 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str): if payload.get('type') != 'auth': await ws.close(code=4001, reason='Expected auth message') return None - user = await get_verified_user_by_token(payload.get('token', ''), getattr(ws.app.state, 'redis', None)) + token = payload.get('token', '') + user = await get_verified_user_by_token(token, getattr(ws.app.state, 'redis', None)) if user is None: await ws.close(code=4001, reason='Invalid token') return None @@ -279,7 +281,7 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str): if not terminal_context_available(connection, 'chat'): await ws.close(code=4003, reason='Terminal server is not available in chats') return None - return user, connection, chat_id if isinstance(chat_id, str) else '' + return user, connection, chat_id if isinstance(chat_id, str) else '', token @router.websocket('/{server_id}/api/terminals/{session_id}') @@ -292,14 +294,14 @@ async def ws_terminal( Uses first-message auth: the client sends ``{"type": "auth", "token": ""}`` as its first message. The proxy validates the JWT, then connects to the - upstream terminal server and authenticates with the server's API key. + upstream terminal server using the configured terminal auth mode. """ await ws.accept() result = await _resolve_authenticated_connection(ws, server_id) if result is None: return - user, connection, chat_id = result + user, connection, chat_id, token = result base_url = get_terminal_server_url(connection) if not base_url: @@ -347,6 +349,8 @@ async def ws_terminal( if auth_type == 'bearer': key = normalize_bearer_token(connection.get('key', '')) await upstream.send_str(_json.dumps({'type': 'auth', 'token': key})) + elif auth_type == 'session' and is_terminal_orchestrator(connection): + await upstream.send_str(_json.dumps({'type': 'auth', 'token': token})) await publish_event( app, diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index 4e4a5a5ab6..b4ec6a4ac2 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -3,6 +3,12 @@ export type FileEntry = { type: 'file' | 'directory'; size?: number; modified?: number; + writable?: boolean; +}; + +export type TerminalFileList = { + entries: FileEntry[]; + writable?: boolean; }; export type ListeningPort = { @@ -85,7 +91,7 @@ export const listFiles = async ( apiKey: string, path: string = '/', sessionId?: string -): Promise => { +): Promise => { // The endpoint uses `directory` as the query param name const url = `${baseUrl.replace(/\/$/, '')}/files/list?directory=${encodeURIComponent(path)}`; const headers: Record = bearerHeaders(apiKey); @@ -99,7 +105,7 @@ export const listFiles = async ( console.error('open-terminal listFiles error:', err); return null; }); - return res?.entries ?? null; + return res?.entries ? { entries: res.entries, writable: res.writable } : null; }; export const readFile = async ( diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 5dda6f42cd..f3cd3828b2 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -93,6 +93,7 @@ let currentPath = savedPath; let fileRoot: TerminalFileRoot | null = null; let entries: FileEntry[] = []; + let currentWritable = true; let loading = false; let error: string | null = null; @@ -175,6 +176,7 @@ // ── File preview state ─────────────────────────────────────────────── let selectedFile: string | null = null; + let selectedFileWritable = true; let previewPort: number | null = null; let fileContent: string | null = null; let fileImageUrl: string | null = null; @@ -410,6 +412,7 @@ loading = true; error = null; selectedFile = null; + selectedFileWritable = true; previewPort = null; clearFilePreview(); clearSelection(); @@ -428,7 +431,8 @@ 'Failed to load directory. Check your Terminal connection in Settings → Integrations.'; entries = []; } else { - entries = sortEntries(result); + currentWritable = result.writable !== false; + entries = sortEntries(result.entries); } }; @@ -439,6 +443,7 @@ } const filePath = `${currentPath}${entry.name}`; + selectedFileWritable = entry.writable !== false; pushNavHistory(currentPath, filePath); const terminal = selectedTerminal; @@ -566,6 +571,7 @@ // ── Drag-and-drop upload ───────────────────────────────────────────── const handleDragOver = (e: DragEvent) => { if (selectedFile) return; + if (!currentWritable) return; if (!e.dataTransfer?.types.includes('Files')) return; e.preventDefault(); e.stopPropagation(); @@ -578,7 +584,7 @@ isDragOver = false; const terminal = selectedTerminal; - if (selectedFile || !terminal) return; + if (selectedFile || !terminal || !currentWritable) return; const droppedFiles = Array.from(e.dataTransfer?.files ?? []); if (!droppedFiles.length) return; @@ -593,7 +599,7 @@ const handleUploadFiles = async (files: File[]) => { const terminal = selectedTerminal; - if (!files.length || !terminal) return; + if (!files.length || !terminal || !currentWritable) return; uploading = true; for (const file of files) { @@ -605,6 +611,7 @@ // ── Folder creation ────────────────────────────────────────────────── const startNewFolder = async () => { + if (!currentWritable) return; creatingFolder = true; newFolderName = ''; await tick(); @@ -634,6 +641,7 @@ // ── File creation ──────────────────────────────────────────────────── const startNewFile = async () => { + if (!currentWritable) return; creatingFile = true; newFileName = ''; await tick(); @@ -658,7 +666,7 @@ // ── Delete ─────────────────────────────────────────────────────────── const handleDelete = async (path: string, name: string) => { const terminal = selectedTerminal; - if (!terminal) return; + if (!terminal || !currentWritable) return; const result = await deleteEntry(terminal.url, terminal.key, path, chatId ?? undefined); toast[result ? 'success' : 'error']( @@ -668,6 +676,7 @@ }; const requestDelete = (path: string, name: string) => { + if (!currentWritable) return; deleteTarget = { path, name }; showDeleteConfirm = true; }; @@ -675,7 +684,7 @@ // ── Move (drag-and-drop) ──────────────────────────────────────────── const handleMove = async (source: string, destFolder: string) => { const terminal = selectedTerminal; - if (!terminal) return; + if (!terminal || !currentWritable) return; const fileName = source.split('/').pop() ?? ''; const destination = `${destFolder}${fileName}`; @@ -704,7 +713,7 @@ // ── Rename ────────────────────────────────────────────────────────── const handleRename = async (oldPath: string, newName: string) => { const terminal = selectedTerminal; - if (!terminal || !newName) return; + if (!terminal || !newName || !currentWritable) return; const dir = oldPath.substring(0, oldPath.lastIndexOf('/') + 1) || currentPath; const destination = `${dir}${newName}`; @@ -733,6 +742,13 @@ $: selectedCount = selectedEntries.size; $: hasSelectedFiles = [...selectedEntries].some((p) => !p.endsWith('/')); + $: selectedEntriesWritable = + currentWritable && + [...selectedEntries].every((path) => { + const name = path.replace(/\/$/, '').split('/').pop(); + const entry = entries.find((item) => item.name === name); + return entry?.writable !== false; + }); const clearSelection = () => { selectedEntries = new Set(); @@ -792,7 +808,7 @@ const bulkDelete = async () => { const terminal = selectedTerminal; - if (!terminal) return; + if (!terminal || !selectedEntriesWritable) return; const paths = [...selectedEntries]; let ok = 0; @@ -1046,6 +1062,7 @@ breadcrumbs={buildBreadcrumbs(currentPath)} {selectedFile} {loading} + writable={currentWritable} {canGoBack} {canGoForward} {sortBy} @@ -1143,9 +1160,9 @@ {#if isHtml && showRaw}