diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index aaa98ec7d4..8706f439c8 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -583,6 +583,8 @@ from open_webui.utils.redis import get_redis_connection from open_webui.tasks import ( redis_task_command_listener, list_task_ids_by_item_id, + has_active_tasks, + cleanup_task, create_task, stop_task, stop_item_tasks, @@ -2063,22 +2065,21 @@ async def chat_completion( except BaseException as e: log.debug(f'Error cleaning up MCP clients: {e}') + # Deregister this task, then emit chat:active=false if no others remain try: - if metadata.get('chat_id'): - - async def emit_inactive_event(): - try: - event_emitter = await get_event_emitter(metadata, update_db=False) - if event_emitter: - await event_emitter({'type': 'chat:active', 'data': {'active': False}}) - except Exception: - pass - - try: - # Shield the event emission so it finishes even if the main task is cancelled - await asyncio.shield(emit_inactive_event()) - except asyncio.CancelledError: - pass + chat_id = metadata.get('chat_id') + task_id = metadata.get('task_id') + if chat_id and task_id: + await cleanup_task(request.app.state.redis, task_id, chat_id) + if not await has_active_tasks(request.app.state.redis, chat_id): + event_emitter = await get_event_emitter(metadata, update_db=False) + if event_emitter: + try: + await asyncio.shield( + event_emitter({'type': 'chat:active', 'data': {'active': False}}) + ) + except asyncio.CancelledError: + pass except Exception: pass @@ -2128,6 +2129,7 @@ async def chat_completion( ), id=chat_id, ) + per_model_metadata['task_id'] = task_id task_ids.append(task_id) # Emit chat:active=true diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index be5feab935..d444ca450d 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1398,6 +1398,7 @@ autoScroll = true; await tick(); + // Mark all non-current assistant messages as done if (history.currentId) { for (const message of Object.values(history.messages)) { if ( @@ -1411,23 +1412,23 @@ } } - const taskRes = await getTaskIdsByChatId(localStorage.token, $chatId).catch((error) => { - return null; - }); - - if (taskRes) { - taskIds = taskRes.task_ids; - } - - // If no active tasks and current message is incomplete, generation was interrupted + // Reconcile active tasks with message state: + // If the response is already done, remaining tasks are just background + // work (follow-ups, title gen) that shouldn't block the input. + const pendingTaskIds = await getTaskIdsByChatId(localStorage.token, $chatId) + .then((res) => res?.task_ids ?? []) + .catch(() => []); const currentMessage = history.currentId ? history.messages[history.currentId] : null; - if ( - currentMessage && - currentMessage.role === 'assistant' && - !currentMessage.done && - (!taskIds || taskIds.length === 0) - ) { - currentMessage.done = true; + const responseComplete = currentMessage?.role === 'assistant' && currentMessage?.done; + + if (pendingTaskIds.length > 0 && !responseComplete) { + taskIds = pendingTaskIds; + } else { + taskIds = null; + // No active tasks and message incomplete → generation was interrupted + if (currentMessage?.role === 'assistant' && !currentMessage.done) { + currentMessage.done = true; + } } await tick();