acpmux: ask an MCP server's elicitation form as a question card
Leo's ask, in his words: "ask user question tool and its various states need looking at allegedly there's some sort of vibe-coded thing that agents can do to have prettier ask question situations...? idk but we could look into that/research that then support it..."
Leo-ask: ask-427cc7d8f5
What was wrong
This is part 4 of 4 of the ask. Parts 1 to 3 have landed: #24348 (HTML previews), #24378 (inline Markdown) and #24450 (Skipped, Stopped and Answered on another device).
An MCP server can ask the person a question with elicitation/create: a form with a flat JSON schema. Claude Code forwards that to its SDK host as a control_request with subtype elicitation. acpmux answered every one with "acpmux does not support control request elicitation", so in cmux a server that asked the person anything got nothing back.
The fix
Mostly acpmux's Claude translator (claude_stdio/elicitation.rs), plus one small hub check below. The web card draws it with no changes.
- What is asked. A form whose fields are all flat becomes an agent question, one item per field, in the server's own order (
property_order). It rides astoolCall._meta.acpmux.question, answered by question text like AskUserQuestion. The flat field shapes are:- a string, which takes a typed answer
- a number or integer, also typed
- a boolean, offered as Yes and No
- a single choice (
enumwithenumNames, oroneOfwith titles) - a multiple choice (an array of
enumwithenumNames, oranyOf)
- How it reads.
- The server's message leads the first field's question.
- A one-field form asks the message itself, under the server's name.
- An optional choice, yes/no or number field adds "No answer", which leaves it out.
- A form with no fields is a confirmation.
- Who may answer. Only a person. It isn't AskUserQuestion, so no session above this one may answer it (
hub/questions.rsancestor_answerable), and policy never answers a question. - What Claude Code gets back.
- Submit accepts, with the answers as the form's
content, typed per the schema (a choice's value, not its label; numbers and booleans as JSON). - Skip declines.
- A stopped turn cancels.
- An answer the form does not offer cancels, so the server never gets content the person did not give. That covers a choice not listed, a Yes/No that is neither, a number that does not parse, and a required field left empty.
- The hub also refuses such an answer up front. An item that takes no typed answer accepts only its own labels, each once (
hub/questions.rscheck_answers).
- Submit accepts, with the answers as the form's
- When Claude Code withdraws the form. Claude Code sends
control_cancel_requestwhen the turn is interrupted or the asking tool call ends. acpmux forgets the form, so a late answer to its card sends Claude Code nothing. The turn ends as usual. The card itself stays until it is answered or the turn is stopped. Having the hub take the card back is a follow-up, #24562: it was built here and taken out after review found it needs recovery and restart handling. - What still gets the unsupported reply:
- URL mode
- a nested schema
- a schema with more than 8 fields
- duplicate choice labels
- a multiple-choice label holding ", ", since the answer could not be split back exactly
- an optional choice labelled "No answer"
- a
property_orderthat does not name every field exactly once - two fields whose prompts stay the same even after each gets its field's key
- an optional string field. The card answers each field with one string, so typed "No answer" could not be told from the "No answer" option. Such a form falls back to the reply acpmux gave every form before this PR, "acpmux does not support control request elicitation", which Claude Code passes to the server as an error. A required string field is asked, and takes a typed "No answer" as written.
Do not touch: AskUserQuestion, permissions, every other control request, every default.
Follow-ups, not here:
- taking a withdrawn form's card back from the hub (#24562)
- URL mode (a link the person opens)
- Codex's own extended questions
Gallery (before/after)
Before: no card. acpmux answered every elicitation with "acpmux does not support control request elicitation", so the gallery has no such state at base (the advisory run lists all 8 elicitation states as "new"; the other 24 question-card states it rendered are unchanged).
After: the stills are in the gallery advisory run at 61307a46, artifact gallery-advisory-24468-61307a46126345bd761d0554deb06ea8a38f394c (run https://github.com/manaflow-ai/cmux/actions/runs/40249745465), dark and light, synthetic fixtures only.
agent-pane.question-card-elicitation shows three looks of where the server's message goes in a form of several fields:
- lead (shipped, my pick): it leads the first field's question. The person reads why once, and each tab stays short.
- every: it leads every field.
- fields: only the fields.
It also shows a one-field form. These are synthetic fixtures only.
Before/after: https://cmux-lawrences-mac-mini.tail137216.ts.net:18796/compare/tp-askquestion-elicitation/?entry=question-card (tailnet). The gallery advisory run attaches the stills.
No capture: the card is the existing webview card.
Test
claude_stdio/elicitation_tests.rs:
- A form is asked as a question in the server's order, with choices, Yes and No, typed fields and "No answer", and the person's options only.
- Submit sends typed content: a
oneOfvalue, an integer, a boolean, and a multiple choice labelled byenumNames. - An answer the form does not offer cancels. Six cases: a bad number, an unlisted choice, a Yes/No that is neither, a choice picked twice, an unlisted multi-select pick, and a required field left out.
- A withdrawn form is forgotten: the hub is sent nothing, and a late answer sends Claude Code nothing.
- An answer that comes before the withdrawal goes out once, and the withdrawal after it changes nothing. The answer and the withdrawal take the form under one lock, so only one of them wins.
- "No answer" leaves an optional number out, and a form with an optional string field is not asked.
- Every prompt is distinct: a repeated one gets its field's key, and a form where even that repeats is not asked.
- Ambiguous forms keep the unsupported reply: a label holding ", ", a "No answer" choice, and a repeated or non-string
property_order. - Skip declines and a stopped turn cancels.
- A one-field form asks the message under the server's name.
- URL mode and a nested schema keep the unsupported reply.
hub/questions.rs: an item that takes no typed answer accepts only its own labels, each once.
Observed / Not run
- Fleet, at c39b2735:
cmux-tui-rust-check.sh all elicitation: fmt and clippy are clean, and the 11 elicitation tests pass.- At d79fdf51,
test hub::questionspassed all 4 tests; this head does not touch the hub. - Earlier, at 0db42776: 132 of 133 hub tests passed. The one red,
hub::media_read::tests::a_file_swapped_for_a_symlink_is_never_followed, also fails at base 3aa13401 (it came in with #24372).
- huge-blue, webviews at ce9027fc: lint and tsc are clean, and
bun test src/agent-session/acpmux/questionpasses. The gallery fixture's labels changed since then (two strings). - Not run: a live MCP server's elicitation through Claude Code. I took the request shape and
control_cancel_requestfrom Claude Code 2.1.307's SDK schema:subtype:"elicitation", mcp_server_name, message, mode, url, elicitation_id, requested_schema, property_order, title, display_name, description, answered{action, content}, and withdrawn with{type:"control_cancel_request", request_id}.
Changelog
Added: An MCP server's question to you (an elicitation form) shows as a question card in the agent chat instead of failing.
Adopted, and the last review rounds (hb-orphan-24468)
The lane that opened this died on cmux16; hb-acpmux adopted it. The branch is now squashed onto feat-cmux-next (7f0446ad) as one commit, plus these:
- Codex xhigh at c39b2735 (BLOCK): 1 the f64 number rounding was already fixed at 4a97e0d6; 2 (nit)
acpmux answersplit a free-text answer on commas, so "a, b" became "a, b". Now an item with no options takes the whole value (question_answer.rs, test first: 904a93a1 red, 2677b249 green). - Codex xhigh at 2677b249 (BLOCK): 1 the TUI answer flow took digits 1 to 9 as option shortcuts on a number field ("3" became "no option 3"; an optional number's "1" picked "No answer"). Number items now carry
input: "number", and there the digits are text. 2 the translator trimmed a typed string. It now sends the hub's string as is (33880267 red, 8e9996f5 green). - Codex xhigh at 006a9a2b (BLOCK): 1 the TUI still ate a leading digit on an item with no options ("123abc" became "abc"). Now digits are shortcuts only on an item with options. 2 a multi-select label holding a bare comma could be split by
acpmux answer. Any comma now keeps the form unsupported (e7095cf3 red, 61307a46 green). - Codex gpt-6-astra xhigh at 61307a46: SHIP. All 11 findings from every round are fixed (the hub withdrawal ones by its removal, follow-up #24562).
Fleet (fast-checks, Blacksmith) at 61307a46: cargo fmt -p acpmux --check is clean, clippy -p acpmux --all-targets -D warnings is clean, and cargo test -p acpmux --lib passes 800 of 800.
CI reds at 61307a46 are none of this PR's. The cmux-next checks red check is "Script tests (pin-cmux-tui, embed-cef, ensure-cef store)", the same on other PRs. Release-notary's test_the_repository_highlights_validate and Guard status are red the same way on base pushes. Same-tree cmux-tui is unavailable on base too. Familiar intent, merge-tree publish, the fast-gates status post and the gallery/review-line comments failed on the CI app's installation rate limit (HTTP 403).
No new timer or observer.
Agent session: big-red
š¤ Generated with Claude Code