diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 83cb58c756..205144a202 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2238,6 +2238,7 @@ async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De if event_emitter: await event_emitter({'type': 'chat:completion', 'data': {'done': True, 'output': output}}) await event_emitter({'type': 'chat:tasks:cancel'}) + await event_emitter({'type': 'chat:active', 'data': {'active': False}}) return result diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index 58b1fed141..f135372b06 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -10,7 +10,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from open_webui.internal.db import Base, get_async_db_context from open_webui.utils.response import normalize_usage -from open_webui.utils.session_records import normalize_tool_output, recorded_timestamp +from open_webui.utils.session_records import has_pending_tool_approval, normalize_tool_output, recorded_timestamp from pydantic import BaseModel, ConfigDict from sqlalchemy import ( JSON, @@ -356,6 +356,20 @@ class ChatMessageTable: message = await db.get(ChatMessage, id) return ChatMessageModel.model_validate(message) if message else None + async def get_chat_ids_with_pending_approval( + self, chat_ids: list[str], db: Optional[AsyncSession] = None + ) -> set[str]: + if not chat_ids: + return set() + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatMessage.chat_id, ChatMessage.output) + .where(ChatMessage.chat_id.in_(chat_ids)) + .where(ChatMessage.role == 'assistant') + .where(ChatMessage.done.is_(False)) + ) + return {chat_id for chat_id, output in result.all() if has_pending_tool_approval(output)} + async def has_unfinished_assistant_by_chat_id( self, chat_id: str, diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 51ae9933bd..57f44331e3 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -356,6 +356,7 @@ class ChatTitleIdResponse(BaseModel): last_read_at: int | None = None snippet: str | None = None active: bool = False + pending_approval: bool = False archived: bool = False diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 55283e9dbe..b283f4f3d9 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -179,7 +179,9 @@ def set_message_authors(chat: dict, user: UserModel) -> None: async def add_active_state_to_chat_list( request: Request, chat_list: list[ChatTitleIdResponse] ) -> list[ChatTitleIdResponse]: + pending_approval_ids = await ChatMessages.get_chat_ids_with_pending_approval([chat.id for chat in chat_list]) for chat in chat_list: + chat.pending_approval = chat.id in pending_approval_ids chat.active = False if not await has_active_tasks(request.app.state.redis, chat.id): continue diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index 5ddd359230..b852d86c95 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -619,6 +619,9 @@ async def get_shared_folder_chats( user_ids.update(uid for ids in participant_ids.values() for uid in ids) users = await Users.get_users_by_user_ids(list(user_ids), db=db) if user_ids else [] names = {u.id: u.name for u in users} + pending_approval_ids = await ChatMessages.get_chat_ids_with_pending_approval( + [chat['id'] for chat in chats], db=db + ) for chat in chats: uid = chat['user_id'] chat['owner_name'] = names.get(uid, 'Unknown') @@ -632,6 +635,7 @@ async def get_shared_folder_chats( ), ) chat['active'] = False + chat['pending_approval'] = chat['id'] in pending_approval_ids if chat['user_id'] != user.id: chat['last_read_at'] = chat['updated_at'] if await has_active_tasks(request.app.state.redis, chat['id']): diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 65695f8e9a..ea9ea1de9b 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -37,6 +37,7 @@ from open_webui.env import ( ) from open_webui.models.access_grants import AccessGrants from open_webui.models.channels import Channels +from open_webui.models.chat_messages import ChatMessages from open_webui.models.chats import Chats from open_webui.models.config import Config from open_webui.models.folders import Folders @@ -1624,6 +1625,13 @@ async def get_event_emitter(request_info, update_db=True): if internal and event_data.get('type') == 'notification': return + if event_data.get('type') == 'chat:active' and is_saved_chat_id(chat_id): + pending_approval_ids = await ChatMessages.get_chat_ids_with_pending_approval([chat_id]) + event_data = { + **event_data, + 'data': {**(event_data.get('data') or {}), 'pending_approval': chat_id in pending_approval_ids}, + } + room = f'user:{user_id}' if is_temporary_chat_id(chat_id): owner = await get_temporary_chat(redis, chat_id) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 6458fdd8e6..d2b6cbd115 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -3327,6 +3327,9 @@ async def pause_for_tool_approval(chat_id: str, message_id: str, output: list[di }, touch=False, ) + event_emitter = await get_event_emitter(metadata, update_db=False) + if event_emitter: + await event_emitter({'type': 'chat:active', 'data': {'active': True}}) def get_response_data(response): diff --git a/backend/open_webui/utils/session_records.py b/backend/open_webui/utils/session_records.py index 80af860208..b35574d6ed 100644 --- a/backend/open_webui/utils/session_records.py +++ b/backend/open_webui/utils/session_records.py @@ -6,6 +6,21 @@ import re import time +def has_pending_tool_approval(output): + if not isinstance(output, list): + return False + items = [item for item in output if isinstance(item, dict)] + result_ids = {item.get('call_id') for item in items if item.get('type') == 'function_call_output'} + return any( + item.get('type') == 'function_call' + and (item.get('call_id') or item.get('id')) + and (item.get('call_id') or item.get('id')) not in result_ids + and item.get('status') in {'pending', 'queued', 'requires_approval'} + and item.get('approved') is not True + for item in items + ) + + def recorded_timestamp(value): try: value = float(value) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 76bd41ea32..4611cbcc44 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -258,7 +258,7 @@ { setFolderItems?: () => unknown; upsertChat?: (chat: Record) => unknown; - setChatActive?: (chatId: string, active: boolean) => boolean; + setChatActive?: (chatId: string, active: boolean, pendingApproval: boolean) => boolean; setChatReadAt?: (chatId: string, lastReadAt: number) => boolean; setAllChatsRead?: () => unknown; } @@ -912,6 +912,7 @@ type: string; data: { active?: boolean; + pending_approval?: boolean; folder_id?: string | null; last_read_at?: number; folder_unread_counts?: Record; @@ -921,10 +922,12 @@ if (event.data?.type === 'chat:active') { const eventData = event.data.data ?? {}; const active = eventData.active ?? false; - const found = setChatActive(event.chat_id, active); + const pendingApproval = eventData.pending_approval ?? false; + const found = setChatActive(event.chat_id, active, pendingApproval); let foundInFolder = false; for (const folder of Object.values(folderRegistry)) { - foundInFolder = folder?.setChatActive?.(event.chat_id, active) || foundInFolder; + foundInFolder = + folder?.setChatActive?.(event.chat_id, active, pendingApproval) || foundInFolder; } if (!foundInFolder && active && eventData.folder_id) { await folderRegistry[eventData.folder_id]?.setFolderItems?.(); @@ -1778,6 +1781,7 @@ updatedAt={chat.updated_at} lastReadAt={chat.last_read_at} active={chat.active ?? false} + pendingApproval={chat.pending_approval ?? false} {shiftKey} selected={selectedChatId === chat.id} on:select={() => { @@ -1846,6 +1850,7 @@ updatedAt={chat.updated_at} lastReadAt={chat.last_read_at} active={chat.active ?? false} + pendingApproval={chat.pending_approval ?? false} {shiftKey} selected={selectedChatId === chat.id} on:select={() => { diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 4cb12625aa..8acb8c3948 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -93,6 +93,7 @@ export let updatedAt: number | null = null; export let lastReadAt: number | null = null; export let active = false; + export let pendingApproval = false; export let selected = false; export let shiftKey = false; @@ -563,19 +564,26 @@ {#snippet chatItemContent()} - - {#if active} + {#if pendingApproval || active || unread}
- +
+ {#if pendingApproval} +
+ {:else if active} + + {:else} +
+ {/if} +
{/if}
- {#if unread} -
-
-
- {/if}