mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-04-28 03:20:01 +00:00
283 lines
10 KiB
Python
283 lines
10 KiB
Python
"""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."""
|
|
# Reply-scoped stop: reply "/stop" to stop only that task.
|
|
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
|
|
|
|
# Global stop: legacy behavior (stop everything)
|
|
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
|
|
|
|
# 1) Cancel branch tasks (no stop_all)
|
|
cancelled = await handler.tree_queue.cancel_branch(branch_root_id)
|
|
handler.update_cancelled_nodes_ui(cancelled)
|
|
|
|
# 2) Collect message IDs from branch nodes only
|
|
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))
|
|
|
|
# 3) Delete messages (best-effort)
|
|
await _delete_message_ids(handler, incoming.chat_id, msg_ids)
|
|
|
|
# 4) Remove branch from tree
|
|
removed, root_id, removed_entire_tree = await handler.tree_queue.remove_branch(
|
|
branch_root_id
|
|
)
|
|
|
|
# 5) Update session store
|
|
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
|
|
|
|
# Global clear
|
|
# 1) Stop tasks first (ensures no more work is running).
|
|
await handler.stop_all_tasks()
|
|
|
|
# 2) Clear chat: best-effort delete messages we can identify.
|
|
msg_ids: set[str] = set()
|
|
|
|
# Add any recorded message IDs for this chat (commands, command replies, etc).
|
|
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}")
|
|
|
|
# Also delete the command message itself.
|
|
if incoming.message_id is not None:
|
|
msg_ids.add(str(incoming.message_id))
|
|
|
|
await _delete_message_ids(handler, incoming.chat_id, msg_ids)
|
|
|
|
# 3) Clear persistent state and reset in-memory queue/tree state.
|
|
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,
|
|
)
|
|
)
|