From 828656b35f04bf486609183799cf8aa2e9850a76 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 5 Mar 2026 14:41:18 -0600 Subject: [PATCH] feat: auto-refresh FileNav on write_file, replace_file_content, and run_command Backend emits terminal events for write_file, replace_file_content, and run_command. Frontend showFileNavDir subscriber uses startsWith path matching to smartly refresh only when the event is relevant: - write_file/replace_file_content: refresh if path is in current view - run_command: always refresh (uses root '/' which matches everything) - Also adds copy-to-clipboard button and code preview full-height fix --- backend/open_webui/utils/middleware.py | 22 ++++++++++++++++------ src/lib/components/chat/Chat.svelte | 7 +++++-- src/lib/components/chat/FileNav.svelte | 18 +++++++++++------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 9c11c96447..53d1a54859 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1053,16 +1053,16 @@ async def terminal_event_handler( """Emit terminal:* events for Open Terminal tools. - display_file → emits 'terminal:display_file' to open the file preview. - - write_file → emits 'terminal:write_file' to silently refresh the file browser. + - write_file / replace_file_content → emits 'terminal:write_file' to refresh. + - run_command → emits 'terminal:run_command' with cwd to refresh if relevant. """ if not event_emitter: return - path = tool_function_params.get("path", "") - if not path: - return - if tool_function_name == "display_file": + path = tool_function_params.get("path", "") + if not path: + return # Only emit if the file actually exists parsed = tool_result if isinstance(parsed, str): @@ -1079,13 +1079,23 @@ async def terminal_event_handler( "data": {"path": path}, } ) - elif tool_function_name == "write_file": + elif tool_function_name in ("write_file", "replace_file_content"): + path = tool_function_params.get("path", "") + if not path: + return await event_emitter( { "type": f"terminal:{tool_function_name}", "data": {"path": path}, } ) + elif tool_function_name == "run_command": + await event_emitter( + { + "type": "terminal:run_command", + "data": {}, + } + ) async def chat_completion_tools_handler( diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 50b9a0294d..5f68320452 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -406,11 +406,14 @@ }; const terminalEventHandler = (type: string, data: any) => { - if (!data?.path) return; if (type === 'terminal:display_file') { + if (!data?.path) return; displayFileHandler(data.path, { showControls, showFileNavPath }); - } else if (type === 'terminal:write_file') { + } else if (type === 'terminal:write_file' || type === 'terminal:replace_file_content') { + if (!data?.path) return; showFileNavDir.set(data.path); + } else if (type === 'terminal:run_command') { + showFileNavDir.set('/'); } }; diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 141f4c68bc..7e9e977e67 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -511,13 +511,15 @@ const lastSlash = filePath.lastIndexOf('/'); const dir = lastSlash > 0 ? filePath.substring(0, lastSlash + 1) : '/'; - if (dir === currentPath) { - await loadDir(currentPath); - } - if (filePath === selectedFile) { - const fileName = filePath.substring(lastSlash + 1); - const entry = entries.find((e) => e.name === fileName); - if (entry) await openEntry(entry); + if (selectedFile) { + if (selectedFile === filePath || currentPath.startsWith(dir)) { + const fileName = selectedFile.split('/').pop() ?? ''; + await openEntry({ name: fileName, type: 'file', size: 0 }); + } + } else { + if (currentPath.startsWith(dir) || dir.startsWith(currentPath)) { + await loadDir(currentPath); + } } }); @@ -549,6 +551,8 @@ document.addEventListener('visibilitychange', onVisibilityChange); return () => { + unsubFileNav(); + unsubFileNavDir(); window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); window.removeEventListener('blur', onBlur);