Yash030/claude-code-proxy
2
1"""Command handlers for messaging platform commands (/stop, /stats, /clear).2 3Extracted from ClaudeMessageHandler to keep handler.py focused on4core message processing logic.5"""6 7from __future__ import annotations8 9from typing import TYPE_CHECKING10 11from loguru import logger12 13if TYPE_CHECKING:14 from messaging.handler import ClaudeMessageHandler15 from messaging.models import IncomingMessage16 17 18async def handle_stop_command(19 handler: ClaudeMessageHandler, incoming: IncomingMessage20) -> None:21 """Handle /stop command from messaging platform."""22 # Reply-scoped stop: reply "/stop" to stop only that task.23 if incoming.is_reply() and incoming.reply_to_message_id:24 reply_id = incoming.reply_to_message_id25 tree = handler.tree_queue.get_tree_for_node(reply_id)26 node_id = handler.tree_queue.resolve_parent_node_id(reply_id) if tree else None27 28 if not node_id:29 msg_id = await handler.platform.queue_send_message(30 incoming.chat_id,31 handler.format_status(32 "โน", "Stopped.", "Nothing to stop for that message."33 ),34 fire_and_forget=False,35 message_thread_id=incoming.message_thread_id,36 )37 handler.record_outgoing_message(38 incoming.platform, incoming.chat_id, msg_id, "command"39 )40 return41 42 count = await handler.stop_task(node_id)43 noun = "request" if count == 1 else "requests"44 msg_id = await handler.platform.queue_send_message(45 incoming.chat_id,46 handler.format_status("โน", "Stopped.", f"Cancelled {count} {noun}."),47 fire_and_forget=False,48 message_thread_id=incoming.message_thread_id,49 )50 handler.record_outgoing_message(51 incoming.platform, incoming.chat_id, msg_id, "command"52 )53 return54 55 # Global stop: legacy behavior (stop everything)56 count = await handler.stop_all_tasks()57 msg_id = await handler.platform.queue_send_message(58 incoming.chat_id,59 handler.format_status(60 "โน", "Stopped.", f"Cancelled {count} pending or active requests."61 ),62 fire_and_forget=False,63 message_thread_id=incoming.message_thread_id,64 )65 handler.record_outgoing_message(66 incoming.platform, incoming.chat_id, msg_id, "command"67 )68 69 70async def handle_stats_command(71 handler: ClaudeMessageHandler, incoming: IncomingMessage72) -> None:73 """Handle /stats command."""74 stats = handler.cli_manager.get_stats()75 tree_count = handler.tree_queue.get_tree_count()76 ctx = handler.get_render_ctx()77 msg_id = await handler.platform.queue_send_message(78 incoming.chat_id,79 "๐ "80 + ctx.bold("Stats")81 + "\n"82 + ctx.escape_text(f"โข Active CLI: {stats['active_sessions']}")83 + "\n"84 + ctx.escape_text(f"โข Message Trees: {tree_count}"),85 fire_and_forget=False,86 message_thread_id=incoming.message_thread_id,87 )88 handler.record_outgoing_message(89 incoming.platform, incoming.chat_id, msg_id, "command"90 )91 92 93async def _delete_message_ids(94 handler: ClaudeMessageHandler, chat_id: str, msg_ids: set[str]95) -> None:96 """Best-effort delete messages by ID. Sorts numeric IDs descending."""97 if not msg_ids:98 return99 100 def _as_int(s: str) -> int | None:101 try:102 return int(str(s))103 except Exception:104 return None105 106 numeric: list[tuple[int, str]] = []107 non_numeric: list[str] = []108 for mid in msg_ids:109 n = _as_int(mid)110 if n is None:111 non_numeric.append(mid)112 else:113 numeric.append((n, mid))114 numeric.sort(reverse=True)115 ordered = [mid for _, mid in numeric] + non_numeric116 117 try:118 CHUNK = 100119 for i in range(0, len(ordered), CHUNK):120 chunk = ordered[i : i + CHUNK]121 await handler.platform.queue_delete_messages(122 chat_id, chunk, fire_and_forget=False123 )124 except Exception as e:125 logger.debug(f"Batch delete failed: {type(e).__name__}: {e}")126 127 128async def _handle_clear_branch(129 handler: ClaudeMessageHandler,130 incoming: IncomingMessage,131 branch_root_id: str,132) -> None:133 """134 Clear a branch (replied-to node + all descendants).135 136 Order: cancel tasks, delete messages, remove branch, update session store.137 """138 tree = handler.tree_queue.get_tree_for_node(branch_root_id)139 if not tree:140 return141 142 # 1) Cancel branch tasks (no stop_all)143 cancelled = await handler.tree_queue.cancel_branch(branch_root_id)144 handler.update_cancelled_nodes_ui(cancelled)145 146 # 2) Collect message IDs from branch nodes only147 msg_ids: set[str] = set()148 branch_ids = tree.get_descendants(branch_root_id)149 for nid in branch_ids:150 node = tree.get_node(nid)151 if node:152 if node.incoming.message_id:153 msg_ids.add(str(node.incoming.message_id))154 if node.status_message_id:155 msg_ids.add(str(node.status_message_id))156 if incoming.message_id:157 msg_ids.add(str(incoming.message_id))158 159 # 3) Delete messages (best-effort)160 await _delete_message_ids(handler, incoming.chat_id, msg_ids)161 162 # 4) Remove branch from tree163 removed, root_id, removed_entire_tree = await handler.tree_queue.remove_branch(164 branch_root_id165 )166 167 # 5) Update session store168 try:169 handler.session_store.remove_node_mappings([n.node_id for n in removed])170 if removed_entire_tree:171 handler.session_store.remove_tree(root_id)172 else:173 updated_tree = handler.tree_queue.get_tree(root_id)174 if updated_tree:175 handler.session_store.save_tree(root_id, updated_tree.to_dict())176 except Exception as e:177 logger.warning(f"Failed to update session store after branch clear: {e}")178 179 180async def handle_clear_command(181 handler: ClaudeMessageHandler, incoming: IncomingMessage182) -> None:183 """184 Handle /clear command.185 186 Reply-scoped: reply to a message to clear that branch (node + descendants).187 Standalone: global clear (stop all, delete all chat messages, reset store).188 """189 from messaging.trees import TreeQueueManager190 191 if incoming.is_reply() and incoming.reply_to_message_id:192 reply_id = incoming.reply_to_message_id193 tree = handler.tree_queue.get_tree_for_node(reply_id)194 branch_root_id = (195 handler.tree_queue.resolve_parent_node_id(reply_id) if tree else None196 )197 if not branch_root_id:198 cancel_fn = getattr(handler.platform, "cancel_pending_voice", None)199 if cancel_fn is not None:200 cancelled = await cancel_fn(incoming.chat_id, reply_id)201 if cancelled is not None:202 voice_msg_id, status_msg_id = cancelled203 msg_ids_to_del: set[str] = {voice_msg_id, status_msg_id}204 if incoming.message_id is not None:205 msg_ids_to_del.add(str(incoming.message_id))206 await _delete_message_ids(handler, incoming.chat_id, msg_ids_to_del)207 msg_id = await handler.platform.queue_send_message(208 incoming.chat_id,209 handler.format_status("๐", "Cleared.", "Voice note cancelled."),210 fire_and_forget=False,211 message_thread_id=incoming.message_thread_id,212 )213 handler.record_outgoing_message(214 incoming.platform, incoming.chat_id, msg_id, "command"215 )216 return217 msg_id = await handler.platform.queue_send_message(218 incoming.chat_id,219 handler.format_status(220 "๐", "Cleared.", "Nothing to clear for that message."221 ),222 fire_and_forget=False,223 message_thread_id=incoming.message_thread_id,224 )225 handler.record_outgoing_message(226 incoming.platform, incoming.chat_id, msg_id, "command"227 )228 return229 await _handle_clear_branch(handler, incoming, branch_root_id)230 return231 232 # Global clear233 # 1) Stop tasks first (ensures no more work is running).234 await handler.stop_all_tasks()235 236 # 2) Clear chat: best-effort delete messages we can identify.237 msg_ids: set[str] = set()238 239 # Add any recorded message IDs for this chat (commands, command replies, etc).240 try:241 for mid in handler.session_store.get_message_ids_for_chat(242 incoming.platform, incoming.chat_id243 ):244 if mid is not None:245 msg_ids.add(str(mid))246 except Exception as e:247 logger.debug(f"Failed to read message log for /clear: {e}")248 249 try:250 msg_ids.update(251 handler.tree_queue.get_message_ids_for_chat(252 incoming.platform, incoming.chat_id253 )254 )255 except Exception as e:256 logger.warning(f"Failed to gather messages for /clear: {e}")257 258 # Also delete the command message itself.259 if incoming.message_id is not None:260 msg_ids.add(str(incoming.message_id))261 262 await _delete_message_ids(handler, incoming.chat_id, msg_ids)263 264 # 3) Clear persistent state and reset in-memory queue/tree state.265 try:266 handler.session_store.clear_all()267 except Exception as e:268 logger.warning(f"Failed to clear session store: {e}")269 270 handler.replace_tree_queue(271 TreeQueueManager(272 queue_update_callback=handler.update_queue_positions,273 node_started_callback=handler.mark_node_processing,274 )275 )276 