From 723dcc8a8539b1c002f0e7c44d2d43fa8dcd30c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 19:44:26 +0000 Subject: [PATCH] fix: honor Terminal File Display setting for backend-managed terminals The 'Terminal File Display' interface setting (terminalFileDisplay) was only applied for direct, browser-executed terminal servers in src/routes/+layout.svelte. Backend-managed terminal tools ('terminal:') execute server-side in middleware.py, where the setting was never consulted, so display_file always opened the sidebar file viewer even with the setting on 'Inline'. Resolve the user's effective setting (stored user settings fall back to the admin 'ui.default_interface_settings', mirroring the merge in routers/users.py) and default display_file to inline=true for terminal tools when the setting is 'inline' and the model did not pass inline itself. The existing build_terminal_file_tool_result / terminal_event_handler logic then renders the file inline and skips the sidebar event, matching the direct tool server behavior. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FmssLUPVLjj6jF4A2LRcjW --- backend/open_webui/utils/middleware.py | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index cc5f57e0c3..0c87b00dd9 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -298,6 +298,65 @@ def build_terminal_file_tool_result( } +def is_terminal_tool(tool: dict | None) -> bool: + tool_id = (tool or {}).get('tool_id', '') + return (tool or {}).get('type') == 'terminal' or (isinstance(tool_id, str) and tool_id.startswith('terminal:')) + + +async def get_terminal_file_display_setting(user) -> str: + """Resolve the user's effective 'terminalFileDisplay' interface setting. + + Stored user settings are stripped of values equal to the admin defaults, + so fall back to 'ui.default_interface_settings' when the user has no + explicit value. + """ + settings = getattr(user, 'settings', None) + if hasattr(settings, 'model_dump'): + settings = settings.model_dump() + if not isinstance(settings, dict): + settings = {} + ui_settings = settings.get('ui') + value = ui_settings.get('terminalFileDisplay') if isinstance(ui_settings, dict) else None + + if value not in ('inline', 'sidebar'): + default_interface_settings = await Config.get('ui.default_interface_settings') + if isinstance(default_interface_settings, dict): + value = default_interface_settings.get('terminalFileDisplay') + + return value if value in ('inline', 'sidebar') else 'sidebar' + + +async def apply_terminal_display_file_defaults( + tool_function_name: str, + tool_function_params: dict, + tool: dict | None, + user, +) -> dict: + """Default display_file to inline for backend-managed terminal tools when + the user's 'Terminal File Display' interface setting is 'inline'. + + Direct (browser-executed) terminal servers apply the same default in + src/routes/+layout.svelte; without this, the setting has no effect on + 'terminal:' tools and display_file always opens the sidebar viewer. + """ + if ( + tool_function_name != 'display_file' + or not isinstance(tool_function_params, dict) + or not tool_function_params.get('path') + or 'inline' in tool_function_params + or not is_terminal_tool(tool) + ): + return tool_function_params + + try: + if await get_terminal_file_display_setting(user) == 'inline': + return {**tool_function_params, 'inline': True} + except Exception as e: + log.debug(e) + + return tool_function_params + + def tool_result_content(tool_result: Any) -> str: if not tool_result: return '' @@ -3169,6 +3228,7 @@ async def execute_tool_call_for_output(request, form_data, user, metadata, event direct_tool = tool.get('direct', False) allowed_params = spec.get('parameters', {}).get('properties', {}).keys() params = {key: value for key, value in params.items() if key in allowed_params} + params = await apply_terminal_display_file_defaults(name, params, tool, user) try: if direct_tool: @@ -5629,6 +5689,7 @@ async def streaming_chat_response_handler(response, ctx): direct_tool = tool.get('direct', False) allowed_params = spec.get('parameters', {}).get('properties', {}).keys() params = {key: value for key, value in params.items() if key in allowed_params} + params = await apply_terminal_display_file_defaults(name, params, tool, user) try: if direct_tool: result = await event_caller(