From ee25b7d42dcde7c5116ddb74d8485ae62649e64e Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:57:58 +0200 Subject: [PATCH] fix: cancelled responses no longer keep rendering as still running (#29495) Pressing stop saved the assistant message as finished while its output items were still marked as running, so the affected block kept showing "Thinking...", "Analyzing..." or "Executing...". That state was written to the database, so it came back after every reload. A tool call cancelled mid-execution was affected as well: it is marked completed as soon as its arguments finish streaming, well before the tool returns, and the client reads that as still executing until the result item arrives. Cancelling now closes those items before the message is saved, marking them incomplete, which is the status the client already treats as finished without flagging an error. Tool calls waiting for approval are left untouched so their approval prompt survives a reload. The corrected output is sent on the chat:tasks:cancel event that was already being emitted, so an open tab settles immediately instead of only after a reload. Emitting a chat:completion event instead would have fired response auto-copy, text to speech playback and the chat finished listeners on a response the user had just cancelled. Verified on a running instance against a mock upstream: cancelling mid-response leaves no item marked as running in the saved message, and the identical run on unpatched code leaves one. Fixes #29281 --- backend/open_webui/utils/middleware.py | 20 ++++++++++++++++++-- src/lib/components/chat/Chat.svelte | 3 +++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index be5ad7ae1c..8c77b09610 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -6374,14 +6374,30 @@ async def streaming_chat_response_handler(response, ctx): pass async def save_cancelled_state(): - await event_emitter({'type': 'chat:tasks:cancel'}) + cancelled_output = full_output() + result_call_ids = { + item.get('call_id') + for item in cancelled_output + if item.get('type') == 'function_call_output' and item.get('call_id') + } + for item in cancelled_output: + # A tool call is stamped completed when its arguments finish, before the tool runs + is_running_tool_call = ( + item.get('type') == 'function_call' + and item.get('status') == 'completed' + and (item.get('call_id') or item.get('id')) not in result_call_ids + ) + if item.get('status') == 'in_progress' or is_running_tool_call: + item['status'] = 'incomplete' + + await event_emitter({'type': 'chat:tasks:cancel', 'data': {'output': cancelled_output}}) if save_to_chat: await Chats.upsert_message_to_chat_by_id_and_message_id( metadata['chat_id'], metadata['message_id'], { 'done': True, - 'output': full_output(), + 'output': cancelled_output, }, ) await clear_response_stream(request.app.state.redis, response_stream_task_id) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index edaa51c8fa..14cf24f4c1 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1237,6 +1237,9 @@ chatCompletionEventHandler(data, message, event.chat_id); } else if (type === 'chat:tasks:cancel') { dismissContextCompactionToast(); + if (data?.output) { + message.output = data.output; + } if (event.message_id === history.currentId) { taskIds = null; // Set all response messages to done