From 8e4cc946cea72fb2d260f9e44cbe439cee528f8b Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:31:14 +0200 Subject: [PATCH] fix: emit the resolved file path in terminal file events (#30282) When a model calls display_file, write_file or replace_file_content with a relative path, Open Terminal resolves it against the session working directory and returns the absolute path, but the event sent to the browser carried the raw argument instead. The file panel matches that string against the file browser root, a relative path never matches, so the preview never opens, the panel jumps to the root and the session working directory is rewritten to the root. With the root turned off (OPEN_TERMINAL_FILE_BROWSER_ROOT=filesystem) there is nothing to clamp to and the relative string is sent to the terminal as the new working directory, moving it silently. The tool call itself succeeds either way, so the failure only shows up as a panel that will not open the file the model just wrote. Both events now carry the path from the tool result and fall back to the argument when the result cannot be read, which is what build_terminal_file_tool_result already does for the chat file attachment. The same one-line rule is applied to the direct tool server path in the frontend, where the browser runs the tool itself and the write_file branch beside it was already correct. Checked against a live Open Terminal: relative arguments now emit the absolute path, absolute ones are unchanged, non-existent files and inline displays still emit nothing, unreadable or error results still fall back to the argument, and run_command is untouched. Related to #30051 --- backend/open_webui/utils/middleware.py | 23 ++++++++++++++--------- src/routes/+layout.svelte | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index cf4f001d71..4c2711668d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1266,6 +1266,14 @@ async def process_tool_result( return tool_result, tool_result_files, tool_result_embeds +def parse_terminal_tool_result(tool_result: Any) -> dict: + try: + result = JSONCodec.loads(tool_result) + except (JSONCodec.JSONDecodeError, TypeError): + return {} + return result if isinstance(result, dict) else {} + + async def terminal_event_handler( tool_function_name: str, tool_function_params: dict, @@ -1284,17 +1292,13 @@ async def terminal_event_handler( if tool_function_name == 'display_file': if tool_function_params.get('inline') is True: return - path = tool_function_params.get('path', '') + result = parse_terminal_tool_result(tool_result) + # Open Terminal resolves the argument against the session cwd, so prefer its resolved path + path = result.get('path') or tool_function_params.get('path', '') if not path: return # Only emit if the file actually exists - parsed = tool_result - if isinstance(parsed, str): - try: - parsed = JSONCodec.loads(parsed) - except (JSONCodec.JSONDecodeError, TypeError): - pass - if isinstance(parsed, dict) and parsed.get('exists') is False: + if result.get('exists') is False: return page = tool_function_params.get('page') @@ -1308,7 +1312,8 @@ async def terminal_event_handler( } ) elif tool_function_name in ('write_file', 'replace_file_content'): - path = tool_function_params.get('path', '') + result = parse_terminal_tool_result(tool_result) + path = result.get('path') or tool_function_params.get('path', '') if not path: return await event_emitter( diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d23eb89038..69bc84986d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -537,7 +537,7 @@ if (data?.name === 'display_file' && params?.path && !inlineDisplayFile) { if (result?.exists !== false) { displayFileHandler( - params.path, + result?.path ?? params.path, { showControls, showFileNavPath }, { page: params?.page } );