Render single-select clarify prompts as interactive pickers
Clarify questions with a finite option set now render as the same tappable picker card used by /reasoning and /fast, instead of a numbered text list. The change is gateway-only: it reuses the existing picker.choice frame and the app's PickerCard UI, so no app change or reinstall is needed. - send_clarify: single-select + live device emits a picker.choice frame (one button per option + an "Other (type your answer)" button) and registers a pending picker; the selection resolves via resolve_gateway_clarify (the agent then continues and replies). - "Other" flips the entry to text-capture (mark_awaiting_text) and prompts the user to type; an unmappable value also flips to text so a clarify never dead-ends. - Multi-select, open-ended, and no-live-device clarifies keep the numbered-text fallback (unchanged behavior). - New helpers _clarify_is_multi / _clarify_picker_callback; positional option values (c0..cN, other) mirror the relay adapter. Tests: updated test_clarify_emits_banner_and_message to multi-select (text fallback) + 3 new tests (single-select emits picker & resolves, Other flips to text, no-device falls back to text). Full suite 90/90. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
82c5a20848
commit
1ff2ef380c
2 files changed
+175
-17
No files matched your search
+80
-16
@@ -566,6 +566,51 @@ def _mint_picker_id() -> str:
|
||||
return f"pc_{uuid.uuid4().hex[:16]}"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _thread_id_from_metadata(metadata: dict[str, Any] | None) -> str | None:
|
||||
if not metadata:
|
||||
return None
|
||||
@@ -2847,10 +2892,14 @@ class IrisAdapter(BasePlatformAdapter):
|
||||
) -> SendResult:
|
||||
"""Banner + push for a clarify prompt.
|
||||
|
||||
Renders the prompt as a proper ``message`` frame (the base text
|
||||
fallback would route through ``send()`` and be misclassified as
|
||||
commentary/tool progress) and keeps the gateway's text intercept
|
||||
working via ``mark_awaiting_text``.
|
||||
Single-select clarifies with a live device render as an interactive
|
||||
``picker.choice`` card (one tap per option + an "Other" free-text
|
||||
button), reusing the slash-command picker mechanism. A real pick
|
||||
resolves via ``resolve_gateway_clarify`` (the agent then continues and
|
||||
replies); "Other" flips the entry to text-capture. Multi-select,
|
||||
open-ended, and no-live-device clarifies fall back to a numbered text
|
||||
list whose reply the gateway's text-intercept captures via
|
||||
``mark_awaiting_text``.
|
||||
"""
|
||||
thread_id = _thread_id_from_metadata(metadata)
|
||||
await self._broadcast_or_log(
|
||||
@@ -2863,23 +2912,38 @@ class IrisAdapter(BasePlatformAdapter):
|
||||
thread_id=thread_id,
|
||||
),
|
||||
)
|
||||
if choices:
|
||||
# Multi-select clarifies register their flag on the pending entry;
|
||||
# look it up by id (mirrors the base text fallback).
|
||||
_is_multi = False
|
||||
try:
|
||||
from tools import clarify_gateway as _cg
|
||||
# Single-select + live device → interactive picker card.
|
||||
if choices and not _clarify_is_multi(clarify_id) and self._http_server.has_devices():
|
||||
picker_id = _mint_picker_id()
|
||||
self._pending_pickers[picker_id] = {
|
||||
"chat_id": chat_id,
|
||||
"thread_id": thread_id,
|
||||
"on_choice_selected": _clarify_picker_callback(
|
||||
clarify_id, [str(c) for c in choices]
|
||||
),
|
||||
}
|
||||
frame_choices = [
|
||||
{"value": f"c{i}", "label": str(c)[:75], "is_current": False}
|
||||
for i, c in enumerate(choices)
|
||||
]
|
||||
frame_choices.append(
|
||||
{"value": "other", "label": "✏️ Other (type your answer)", "is_current": False}
|
||||
)
|
||||
await self._broadcast_or_log(
|
||||
chat_id,
|
||||
protocol.picker_choice(
|
||||
picker_id, f"❓ {question}", frame_choices, chat_id, thread_id=thread_id
|
||||
),
|
||||
)
|
||||
return SendResult(success=True, message_id=picker_id)
|
||||
|
||||
with _cg._lock:
|
||||
_entry = _cg._entries.get(clarify_id)
|
||||
_is_multi = bool(_entry and getattr(_entry, "multi_select", False))
|
||||
except Exception:
|
||||
_is_multi = False
|
||||
# Text fallback (multi-select / open-ended / no live device).
|
||||
if choices:
|
||||
lines = [f"❓ {question}", ""]
|
||||
for i, choice in enumerate(choices, start=1):
|
||||
lines.append(f" {i}. {choice}")
|
||||
lines.append("")
|
||||
if _is_multi:
|
||||
if _clarify_is_multi(clarify_id):
|
||||
lines.append(
|
||||
"Multiple selections allowed — reply with the numbers "
|
||||
'separated by commas or spaces (e.g. "1, 3"), the option '
|
||||
|
||||
Reference in new issue
Block a user