"""Choice-picker callbacks: clarify + exec approval. Build the ``on_choice_selected`` closures the adapter stores in ``_pending_pickers``; a ``picker.select`` from the app runs the closure and delivers its reply text as a normal message. """ def _clarify_is_multi(clarify_id: str) -> bool: """True when the pending clarify [clarify_id] allows multiple selections. The flag lives on the gateway's pending entry; a missing/expired entry (or any lookup error) is treated as single-select. """ try: from tools import clarify_gateway as _cg with _cg._lock: _entry = _cg._entries.get(clarify_id) return bool(_entry and getattr(_entry, "multi_select", False)) except Exception: return False def _clarify_picker_callback(clarify_id: str, choices: list[str]): """Build the ``on_choice_selected`` callback for a clarify picker. Option values are positional (``c0``..``cN``) plus an ``other`` sentinel; the closure maps them back to the real choice strings. A real pick resolves the clarify (the agent then continues and replies); "Other" flips the entry to text-capture so the next typed message is the answer. An unmappable value also flips to text so a clarify never dead-ends. """ async def on_choice_selected(chat_id: str, value: str) -> str | None: from tools.clarify_gateway import mark_awaiting_text, resolve_gateway_clarify if value == "other": mark_awaiting_text(clarify_id) return "✏️ Type your answer:" try: idx = int(value[1:]) if value.startswith("c") else -1 except ValueError: idx = -1 if 0 <= idx < len(choices): resolve_gateway_clarify(clarify_id, choices[idx]) return None mark_awaiting_text(clarify_id) return "✏️ Type your answer:" return on_choice_selected # The four exec-approval outcomes hermes understands (tools.approval). _APPROVAL_CHOICES = ("once", "session", "always", "deny") def _approval_picker_callback(session_key: str): """Build the ``on_choice_selected`` callback for an exec-approval picker. The option values are the raw hermes approval outcomes (``once`` / ``session`` / ``always`` / ``deny``); a tap resolves the waiting agent thread via ``resolve_gateway_approval`` (the same primitive the text ``/approve`` / ``/deny`` handlers use) and returns a short confirmation label, which the picker handler delivers as a normal message. An unknown value is treated as a deny so a stray tap never approves a command. """ async def on_choice_selected(chat_id: str, value: str) -> str | None: from tools.approval import resolve_gateway_approval choice = value if value in _APPROVAL_CHOICES else "deny" count = resolve_gateway_approval(session_key, choice) label = { "once": "✅ Approved once", "session": "✅ Approved for this session", "always": "✅ Approved permanently", "deny": "❌ Denied", }[choice] if not count: label = "⌛ Approval expired — no command was waiting." return label return on_choice_selected