from __future__ import annotations from typing import Any, TYPE_CHECKING from yuxi.utils.logging_config import logger if TYPE_CHECKING: from .client import UrbitClient from .settings import SettingsStore class ThreadManager: def __init__(self, max_history: int = 20, context_lines: int = 10): self._participated: set[str] = set() self._max_history = max_history self._context_lines = context_lines self._settings_store: SettingsStore | None = None def bind_settings_store(self, store: SettingsStore) -> None: self._settings_store = store async def restore_from_store(self) -> None: if not self._settings_store: return stored = self._settings_store.get("participatedThreads", []) if isinstance(stored, list): self._participated = set(stored) async def persist_participated(self) -> None: if not self._settings_store: return self._settings_store.update_key("participatedThreads", list(self._participated)) def is_thread_reply(self, raw_data: dict[str, Any]) -> bool: graph_update = raw_data.get("graph-update", {}) additions = graph_update.get("additions", {}) for node_data in additions.values(): post = node_data.get("post", {}) contents = post.get("contents", []) for item in contents: if isinstance(item, dict) and "reply" in item: return True return False def get_reply_parent_id(self, raw_data: dict[str, Any]) -> str | None: graph_update = raw_data.get("graph-update", {}) additions = graph_update.get("additions", {}) for node_data in additions.values(): post = node_data.get("post", {}) contents = post.get("contents", []) for item in contents: if isinstance(item, dict) and "reply" in item: reply_data = item["reply"] return reply_data.get("id") or reply_data return None def add_participated(self, thread_id: str) -> None: self._participated.add(thread_id) def has_participated(self, thread_id: str) -> bool: return thread_id in self._participated async def fetch_thread_history( self, client: UrbitClient, ship_name: str, resource_path: str, parent_id: str ) -> list[dict[str, Any]]: from .scry import scry_get try: path = f"/graph/{ship_name}/{resource_path}" result = await scry_get(client, "graph", path) if not result or not isinstance(result, dict): return [] nodes = result.get("graph", {}).get("nodes", {}) thread_messages: list[dict[str, Any]] = [] for _node_id, node in nodes.items(): post = node.get("post", {}) if not post: continue if _is_child_of(parent_id, node): thread_messages.append(_extract_message_content(node)) thread_messages.sort(key=lambda m: m.get("time", 0)) return thread_messages[-self._max_history :] except Exception as e: logger.warning(f"[Urbit] Failed to fetch thread history: {e}") return [] def build_thread_context(self, history: list[dict[str, Any]]) -> str: recent = history[-self._context_lines :] if not recent: return "" lines: list[str] = [ f"[Thread conversation - {len(recent)} previous replies...]", "[Previous messages]", ] for msg in recent: author = msg.get("author", "unknown") content = msg.get("content", "") lines.append(f"~{author}: {content}") lines.append("[Current message]") return "\n".join(lines) def _is_child_of(parent_id: str, node: dict[str, Any]) -> bool: children = node.get("children", {}) if parent_id in children: return True for child_node in children.values(): if isinstance(child_node, dict) and _is_child_of(parent_id, child_node): return True return False def _extract_message_content(node: dict[str, Any]) -> dict[str, Any]: post = node.get("post", {}) author = post.get("author", "") index = post.get("index", "") time_sent = node.get("post", {}).get("time-sent", 0) contents = post.get("contents", []) text_parts: list[str] = [] for item in contents: if "text" in item: text_parts.append(item["text"]) elif "mention" in item: text_parts.append(f"~{item['mention']}") elif "url" in item: text_parts.append(item["url"]) elif "code" in item: code_block = item["code"] if isinstance(code_block, dict): text_parts.append(code_block.get("code", "")) else: text_parts.append(str(code_block)) return { "author": author, "index": index, "content": "".join(text_parts), "time": time_sent, }