| """Command handlers for messaging platform commands (/stop, /stats, /clear). |
| |
| Extracted from ClaudeMessageHandler to keep handler.py focused on |
| core message processing logic. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import TYPE_CHECKING |
|
|
| from loguru import logger |
|
|
| if TYPE_CHECKING: |
| from messaging.handler import ClaudeMessageHandler |
| from messaging.models import IncomingMessage |
|
|
|
|
| async def handle_stop_command( |
| handler: ClaudeMessageHandler, incoming: IncomingMessage |
| ) -> None: |
| """Handle /stop command from messaging platform.""" |
| |
| if incoming.is_reply() and incoming.reply_to_message_id: |
| reply_id = incoming.reply_to_message_id |
| tree = handler.tree_queue.get_tree_for_node(reply_id) |
| node_id = handler.tree_queue.resolve_parent_node_id(reply_id) if tree else None |
|
|
| if not node_id: |
| msg_id = await handler.platform.queue_send_message( |
| incoming.chat_id, |
| handler.format_status( |
| "⏹", "Stopped.", "Nothing to stop for that message." |
| ), |
| fire_and_forget=False, |
| message_thread_id=incoming.message_thread_id, |
| ) |
| handler.record_outgoing_message( |
| incoming.platform, incoming.chat_id, msg_id, "command" |
| ) |
| return |
|
|
| count = await handler.stop_task(node_id) |
| noun = "request" if count == 1 else "requests" |
| msg_id = await handler.platform.queue_send_message( |
| incoming.chat_id, |
| handler.format_status("⏹", "Stopped.", f"Cancelled {count} {noun}."), |
| fire_and_forget=False, |
| message_thread_id=incoming.message_thread_id, |
| ) |
| handler.record_outgoing_message( |
| incoming.platform, incoming.chat_id, msg_id, "command" |
| ) |
| return |
|
|
| |
| count = await handler.stop_all_tasks() |
| msg_id = await handler.platform.queue_send_message( |
| incoming.chat_id, |
| handler.format_status( |
| "⏹", "Stopped.", f"Cancelled {count} pending or active requests." |
| ), |
| fire_and_forget=False, |
| message_thread_id=incoming.message_thread_id, |
| ) |
| handler.record_outgoing_message( |
| incoming.platform, incoming.chat_id, msg_id, "command" |
| ) |
|
|
|
|
| async def handle_stats_command( |
| handler: ClaudeMessageHandler, incoming: IncomingMessage |
| ) -> None: |
| """Handle /stats command.""" |
| stats = handler.cli_manager.get_stats() |
| tree_count = handler.tree_queue.get_tree_count() |
| ctx = handler.get_render_ctx() |
| msg_id = await handler.platform.queue_send_message( |
| incoming.chat_id, |
| "📊 " |
| + ctx.bold("Stats") |
| + "\n" |
| + ctx.escape_text(f"• Active CLI: {stats['active_sessions']}") |
| + "\n" |
| + ctx.escape_text(f"• Message Trees: {tree_count}"), |
| fire_and_forget=False, |
| message_thread_id=incoming.message_thread_id, |
| ) |
| handler.record_outgoing_message( |
| incoming.platform, incoming.chat_id, msg_id, "command" |
| ) |
|
|
|
|
| async def _delete_message_ids( |
| handler: ClaudeMessageHandler, chat_id: str, msg_ids: set[str] |
| ) -> None: |
| """Best-effort delete messages by ID. Sorts numeric IDs descending.""" |
| if not msg_ids: |
| return |
|
|
| def _as_int(s: str) -> int | None: |
| try: |
| return int(str(s)) |
| except Exception: |
| return None |
|
|
| numeric: list[tuple[int, str]] = [] |
| non_numeric: list[str] = [] |
| for mid in msg_ids: |
| n = _as_int(mid) |
| if n is None: |
| non_numeric.append(mid) |
| else: |
| numeric.append((n, mid)) |
| numeric.sort(reverse=True) |
| ordered = [mid for _, mid in numeric] + non_numeric |
|
|
| batch_fn = getattr(handler.platform, "queue_delete_messages", None) |
| if callable(batch_fn): |
| try: |
| CHUNK = 100 |
| for i in range(0, len(ordered), CHUNK): |
| chunk = ordered[i : i + CHUNK] |
| await batch_fn(chat_id, chunk, fire_and_forget=False) |
| except Exception as e: |
| logger.debug(f"Batch delete failed: {type(e).__name__}: {e}") |
| else: |
| for mid in ordered: |
| try: |
| await handler.platform.queue_delete_message( |
| chat_id, mid, fire_and_forget=False |
| ) |
| except Exception as e: |
| logger.debug(f"Delete failed for msg {mid}: {type(e).__name__}: {e}") |
|
|
|
|
| async def _handle_clear_branch( |
| handler: ClaudeMessageHandler, |
| incoming: IncomingMessage, |
| branch_root_id: str, |
| ) -> None: |
| """ |
| Clear a branch (replied-to node + all descendants). |
| |
| Order: cancel tasks, delete messages, remove branch, update session store. |
| """ |
| tree = handler.tree_queue.get_tree_for_node(branch_root_id) |
| if not tree: |
| return |
|
|
| |
| cancelled = await handler.tree_queue.cancel_branch(branch_root_id) |
| handler.update_cancelled_nodes_ui(cancelled) |
|
|
| |
| msg_ids: set[str] = set() |
| branch_ids = tree.get_descendants(branch_root_id) |
| for nid in branch_ids: |
| node = tree.get_node(nid) |
| if node: |
| if node.incoming.message_id: |
| msg_ids.add(str(node.incoming.message_id)) |
| if node.status_message_id: |
| msg_ids.add(str(node.status_message_id)) |
| if incoming.message_id: |
| msg_ids.add(str(incoming.message_id)) |
|
|
| |
| await _delete_message_ids(handler, incoming.chat_id, msg_ids) |
|
|
| |
| removed, root_id, removed_entire_tree = await handler.tree_queue.remove_branch( |
| branch_root_id |
| ) |
|
|
| |
| try: |
| handler.session_store.remove_node_mappings([n.node_id for n in removed]) |
| if removed_entire_tree: |
| handler.session_store.remove_tree(root_id) |
| else: |
| updated_tree = handler.tree_queue.get_tree(root_id) |
| if updated_tree: |
| handler.session_store.save_tree(root_id, updated_tree.to_dict()) |
| except Exception as e: |
| logger.warning(f"Failed to update session store after branch clear: {e}") |
|
|
|
|
| async def handle_clear_command( |
| handler: ClaudeMessageHandler, incoming: IncomingMessage |
| ) -> None: |
| """ |
| Handle /clear command. |
| |
| Reply-scoped: reply to a message to clear that branch (node + descendants). |
| Standalone: global clear (stop all, delete all chat messages, reset store). |
| """ |
| from messaging.trees import TreeQueueManager |
|
|
| if incoming.is_reply() and incoming.reply_to_message_id: |
| reply_id = incoming.reply_to_message_id |
| tree = handler.tree_queue.get_tree_for_node(reply_id) |
| branch_root_id = ( |
| handler.tree_queue.resolve_parent_node_id(reply_id) if tree else None |
| ) |
| if not branch_root_id: |
| cancel_fn = getattr(handler.platform, "cancel_pending_voice", None) |
| if cancel_fn is not None: |
| cancelled = await cancel_fn(incoming.chat_id, reply_id) |
| if cancelled is not None: |
| voice_msg_id, status_msg_id = cancelled |
| msg_ids_to_del: set[str] = {voice_msg_id, status_msg_id} |
| if incoming.message_id is not None: |
| msg_ids_to_del.add(str(incoming.message_id)) |
| await _delete_message_ids(handler, incoming.chat_id, msg_ids_to_del) |
| msg_id = await handler.platform.queue_send_message( |
| incoming.chat_id, |
| handler.format_status("🗑", "Cleared.", "Voice note cancelled."), |
| fire_and_forget=False, |
| message_thread_id=incoming.message_thread_id, |
| ) |
| handler.record_outgoing_message( |
| incoming.platform, incoming.chat_id, msg_id, "command" |
| ) |
| return |
| msg_id = await handler.platform.queue_send_message( |
| incoming.chat_id, |
| handler.format_status( |
| "🗑", "Cleared.", "Nothing to clear for that message." |
| ), |
| fire_and_forget=False, |
| message_thread_id=incoming.message_thread_id, |
| ) |
| handler.record_outgoing_message( |
| incoming.platform, incoming.chat_id, msg_id, "command" |
| ) |
| return |
| await _handle_clear_branch(handler, incoming, branch_root_id) |
| return |
|
|
| |
| |
| await handler.stop_all_tasks() |
|
|
| |
| msg_ids: set[str] = set() |
|
|
| |
| try: |
| for mid in handler.session_store.get_message_ids_for_chat( |
| incoming.platform, incoming.chat_id |
| ): |
| if mid is not None: |
| msg_ids.add(str(mid)) |
| except Exception as e: |
| logger.debug(f"Failed to read message log for /clear: {e}") |
|
|
| try: |
| msg_ids.update( |
| handler.tree_queue.get_message_ids_for_chat( |
| incoming.platform, incoming.chat_id |
| ) |
| ) |
| except Exception as e: |
| logger.warning(f"Failed to gather messages for /clear: {e}") |
|
|
| |
| if incoming.message_id is not None: |
| msg_ids.add(str(incoming.message_id)) |
|
|
| await _delete_message_ids(handler, incoming.chat_id, msg_ids) |
|
|
| |
| try: |
| handler.session_store.clear_all() |
| except Exception as e: |
| logger.warning(f"Failed to clear session store: {e}") |
|
|
| handler.replace_tree_queue( |
| TreeQueueManager( |
| queue_update_callback=handler.update_queue_positions, |
| node_started_callback=handler.mark_node_processing, |
| ) |
| ) |
|
|