ask_user upgrades: rich options, grouped questions, option previews (OPE-51)

Options accept {label, description, recommended, preview} objects (plain
strings unchanged — old sessions render as today's pills), and `questions`
groups up to 4 questions into one call, rendered as a stepper via the
header chips. Any option preview switches the card to a two-pane layout:
options left, monospace pane right, following hover/focus.

Grouped calls resolve with a JSON map keyed by header-or-question and
return {answers: {...}} to the agent (single stays {answer: ...});
a grouped item's first question doubles as its title/options so channel
mirrors and legacy surfaces degrade sensibly. Channel buttons use option
labels; grouped items mirror as text with the open-the-app hint.
This commit is contained in:
Devika Verma
2026-07-29 15:52:05 +05:30
parent f96ad4c8e6
commit 70cd1fa3d4
12 changed files with 930 additions and 100 deletions
+8 -1
View File
@@ -826,6 +826,13 @@ class TurnEngine:
from the Inbox when unattended), and return it as the tool result."""
args = tool_call.arguments or {}
question = str(args.get("question", "")).strip()
# Grouped form (OPE-51): `questions` alone is a valid call — the singular field may be
# empty. The asker normalizes/validates the entries; here only "is anything asked?".
if not question:
for entry in args.get("questions") or []:
if isinstance(entry, dict) and str(entry.get("question", "")).strip():
question = str(entry["question"]).strip()
break
if self.question_asker is None or not question:
result: dict[str, Any] = {
"answer": "",
@@ -847,7 +854,7 @@ class TurnEngine:
"error": "no response",
}
status = "ok" if result.get("answer") else "denied"
status = "ok" if (result.get("answer") or result.get("answers")) else "denied"
self.messages.append(_tool_result_message(tool_call, result))
self._audit(
tool_call,