From afef27dd6ca8e14161e0d3ed86b4b96f82b01895 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Wed, 1 Jul 2026 18:41:39 +0800 Subject: [PATCH] fix(webui): show pending local triggers --- nanobot/channels/manager.py | 3 ++ nanobot/cli/commands.py | 5 ++++ nanobot/webui/gateway_services.py | 4 +++ nanobot/webui/session_automations.py | 4 ++- nanobot/webui/ws_http.py | 30 +++++++++++++++++--- tests/channels/test_websocket_http_routes.py | 12 ++++++++ 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 7f1b0fe0..d904a704 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -71,6 +71,7 @@ class ChannelManager: local_trigger_store: Any | None = None, webui_runtime_model_name: Callable[[], str | None] | None = None, webui_cron_pending_job_ids: Callable[[str], set[str]] | None = None, + webui_local_trigger_pending_ids: Callable[[str], set[str]] | None = None, webui_static_dist: bool = True, webui_runtime_surface: str = "browser", webui_runtime_capabilities: dict[str, Any] | None = None, @@ -82,6 +83,7 @@ class ChannelManager: self._local_trigger_store = local_trigger_store self._webui_runtime_model_name = webui_runtime_model_name self._webui_cron_pending_job_ids = webui_cron_pending_job_ids + self._webui_local_trigger_pending_ids = webui_local_trigger_pending_ids self._webui_static_dist = webui_static_dist self._webui_runtime_surface = webui_runtime_surface self._webui_runtime_capabilities = dict(webui_runtime_capabilities or {}) @@ -143,6 +145,7 @@ class ChannelManager: cron_service=self._cron_service, local_trigger_store=self._local_trigger_store, cron_pending_job_ids=self._webui_cron_pending_job_ids, + local_trigger_pending_ids=self._webui_local_trigger_pending_ids, logger=logger, ) kwargs["gateway"] = gateway diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index f5dabda5..2ae21300 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -1154,6 +1154,11 @@ def _run_gateway( local_trigger_store=trigger_store, webui_runtime_model_name=_webui_runtime_model_name, webui_cron_pending_job_ids=getattr(agent, "pending_cron_job_ids_for_session", None), + webui_local_trigger_pending_ids=getattr( + agent, + "pending_local_trigger_ids_for_session", + None, + ), webui_static_dist=webui_static_dist, webui_runtime_surface=webui_runtime_surface, webui_runtime_capabilities=webui_runtime_capabilities, diff --git a/nanobot/webui/gateway_services.py b/nanobot/webui/gateway_services.py index 4fdcc8e0..c565b3a1 100644 --- a/nanobot/webui/gateway_services.py +++ b/nanobot/webui/gateway_services.py @@ -28,6 +28,7 @@ class GatewayServices: cron_service: Any | None local_trigger_store: Any | None cron_pending_job_ids: Callable[[str], set[str]] | None + local_trigger_pending_ids: Callable[[str], set[str]] | None def build_gateway_services( @@ -45,6 +46,7 @@ def build_gateway_services( cron_service: Any | None = None, local_trigger_store: Any | None = None, cron_pending_job_ids: Callable[[str], set[str]] | None = None, + local_trigger_pending_ids: Callable[[str], set[str]] | None = None, logger: Any = default_logger, ) -> GatewayServices: tokens = GatewayTokenStore() @@ -74,6 +76,7 @@ def build_gateway_services( cron_service=cron_service, local_trigger_store=local_trigger_store, cron_pending_job_ids=cron_pending_job_ids, + local_trigger_pending_ids=local_trigger_pending_ids, log=logger, ) return GatewayServices( @@ -86,4 +89,5 @@ def build_gateway_services( cron_service=cron_service, local_trigger_store=local_trigger_store, cron_pending_job_ids=cron_pending_job_ids, + local_trigger_pending_ids=local_trigger_pending_ids, ) diff --git a/nanobot/webui/session_automations.py b/nanobot/webui/session_automations.py index 3ca5d718..ef99689b 100644 --- a/nanobot/webui/session_automations.py +++ b/nanobot/webui/session_automations.py @@ -135,6 +135,7 @@ def _serialize_job( if isinstance(job, LocalTrigger): return _serialize_trigger( job, + pending=pending, include_details=include_details, session_manager=session_manager, ) @@ -189,6 +190,7 @@ def _serialize_job( def _serialize_trigger( trigger: LocalTrigger, *, + pending: bool = False, include_details: bool = False, session_manager: _SessionManagerLike | None = None, ) -> dict[str, Any]: @@ -213,7 +215,7 @@ def _serialize_trigger( "state": { "next_run_at_ms": None, "last_status": trigger.last_status, - "pending": False, + "pending": pending, }, } if not include_details: diff --git a/nanobot/webui/ws_http.py b/nanobot/webui/ws_http.py index ee1a3133..5964db25 100644 --- a/nanobot/webui/ws_http.py +++ b/nanobot/webui/ws_http.py @@ -157,6 +157,7 @@ class GatewayHTTPHandler: cron_service: CronService | None = None, local_trigger_store: LocalTriggerStore | None = None, cron_pending_job_ids: Callable[[str], set[str]] | None = None, + local_trigger_pending_ids: Callable[[str], set[str]] | None = None, log: Any = logger, ) -> None: self.config = config @@ -172,6 +173,7 @@ class GatewayHTTPHandler: self.cron_service = cron_service self.local_trigger_store = local_trigger_store self.cron_pending_job_ids = cron_pending_job_ids + self.local_trigger_pending_ids = local_trigger_pending_ids self._log = log self._runtime_surface = runtime_surface @@ -487,9 +489,7 @@ class GatewayHTTPHandler: return _http_error(400, "invalid session key") if not _is_websocket_channel_session_key(decoded_key): return _http_error(404, "session not found") - pending_job_ids: set[str] = set() - if self.cron_pending_job_ids is not None: - pending_job_ids = self.cron_pending_job_ids(decoded_key) + pending_job_ids = self._pending_automation_ids_for_session(decoded_key) return _http_json_response( session_automations_payload( self.cron_service, @@ -561,15 +561,37 @@ class GatewayHTTPHandler: pending.update(self.cron_pending_job_ids(session_key)) return pending + def _pending_local_trigger_ids_for_all(self) -> set[str]: + if self.local_trigger_store is None or self.local_trigger_pending_ids is None: + return set() + pending: set[str] = set() + for trigger in self.local_trigger_store.list_triggers(include_disabled=True): + session_key = trigger.session_key + if not session_key and trigger.channel and trigger.chat_id: + session_key = f"{trigger.channel}:{trigger.chat_id}" + if session_key: + pending.update(self.local_trigger_pending_ids(session_key)) + return pending + + def _pending_automation_ids_for_session(self, session_key: str) -> set[str]: + pending: set[str] = set() + if self.cron_pending_job_ids is not None: + pending.update(self.cron_pending_job_ids(session_key)) + if self.local_trigger_pending_ids is not None: + pending.update(self.local_trigger_pending_ids(session_key)) + return pending + def _handle_webui_automations(self, request: WsRequest) -> Response: if not self.check_api_token(request): return _http_error(401, "Unauthorized") + pending_job_ids = self._pending_cron_job_ids_for_all() + pending_job_ids.update(self._pending_local_trigger_ids_for_all()) return _http_json_response( all_automations_payload( self.cron_service, local_trigger_store=self.local_trigger_store, session_manager=self.session_manager, - pending_job_ids=self._pending_cron_job_ids_for_all(), + pending_job_ids=pending_job_ids, ) ) diff --git a/tests/channels/test_websocket_http_routes.py b/tests/channels/test_websocket_http_routes.py index 144669b3..2df8d577 100644 --- a/tests/channels/test_websocket_http_routes.py +++ b/tests/channels/test_websocket_http_routes.py @@ -49,6 +49,7 @@ def _make_handler( cron_service: CronService | None = None, local_trigger_store: LocalTriggerStore | None = None, cron_pending_job_ids: Any | None = None, + local_trigger_pending_ids: Any | None = None, ) -> GatewayServices: config = WebSocketConfig.model_validate(cfg) if isinstance(cfg, dict) else cfg workspace = workspace_path or Path.cwd() @@ -65,6 +66,7 @@ def _make_handler( cron_service=cron_service, local_trigger_store=local_trigger_store, cron_pending_job_ids=cron_pending_job_ids, + local_trigger_pending_ids=local_trigger_pending_ids, ) @@ -79,6 +81,7 @@ def _ch( cron_service: CronService | None = None, local_trigger_store: LocalTriggerStore | None = None, cron_pending_job_ids: Any | None = None, + local_trigger_pending_ids: Any | None = None, **extra: Any, ) -> WebSocketChannel: cfg: dict[str, Any] = { @@ -99,6 +102,7 @@ def _ch( cron_service=cron_service, local_trigger_store=local_trigger_store, cron_pending_job_ids=cron_pending_job_ids, + local_trigger_pending_ids=local_trigger_pending_ids, ) return WebSocketChannel(cfg, bus, gateway=gateway) @@ -341,6 +345,9 @@ async def test_session_automations_route_lists_local_triggers( bus, session_manager=_seed_session(tmp_path, key="websocket:abc"), local_trigger_store=trigger_store, + local_trigger_pending_ids=lambda key: ( + {trigger.id} if key == "websocket:abc" else set() + ), port=port, ) server_task = asyncio.create_task(channel.start()) @@ -363,6 +370,7 @@ async def test_session_automations_route_lists_local_triggers( assert job["schedule"]["kind"] == "local" assert job["payload"]["kind"] == "local_trigger" assert job["payload"]["command"] == f'nanobot trigger {trigger.id} "message"' + assert job["state"]["pending"] is True finally: await channel.stop() await server_task @@ -1146,6 +1154,9 @@ async def test_webui_automations_route_manages_local_triggers( bus, session_manager=_seed_session(tmp_path, key="websocket:abc"), local_trigger_store=trigger_store, + local_trigger_pending_ids=lambda key: ( + {trigger.id} if key == "websocket:abc" else set() + ), port=port, ) server_task = asyncio.create_task(channel.start()) @@ -1159,6 +1170,7 @@ async def test_webui_automations_route_manages_local_triggers( assert listed.status_code == 200 by_id = {job["id"]: job for job in listed.json()["jobs"]} assert by_id[trigger.id]["kind"] == "local_trigger" + assert by_id[trigger.id]["state"]["pending"] is True assert by_id[trigger.id]["trigger"]["command"] == f'nanobot trigger {trigger.id} "message"' disabled = await _http_get(