Render ACP elicitation forms - #797
Open
OSadovy wants to merge 4 commits into
Open
Conversation
`agent-shell-ui--echo-action-hint' hardcoded `agent-shell-ui-toggle-fragment' and `agent-shell-ui-fragment-map', so it could only ever describe folding. Chrome bound to a different shared map wants the same "Press KEY to VERB" hint pointing at its own binding. Both arguments are optional and default to what was hardcoded, so existing callers are unchanged.
Agents can ask a structured question mid-turn and wait for the answer. agent-shell did not implement `elicitation/create', and because the capability is opt-in per client, agents cannot tell it is merely absent: the Claude adapter drops `AskUserQuestion' from the model's toolset entirely, and auto-declines elicitations coming from MCP servers. Advertising `elicitation.form' is a promise to render any form the schema permits, so every `ElicitationPropertySchema' variant is covered: single- and multi-select in both spellings, boolean, string, number, integer, and the forward-compat unknown type. What is deferred is constraint validation, not coverage -- only a required-presence gate runs on submit, without which we would send a response the schema forbids. `url' mode is a separate capability and is deliberately not advertised: an agent must not send a mode the client did not ask for. Form state lives in `agent-shell--state' under `:elicitations', keyed by the JSON-RPC request id. Rendered text carries identity only, read back by named commands at point. Fragment-local state would not do, since `agent-shell--update-fragment' writes into both the shell buffer and the viewport buffer, so a fragment's own state exists in two copies. Agents bridging an ask-the-user tool onto elicitation send a select plus a free-text "Other" field, marking the second as belonging to the first through an un-namespaced `_meta' key. Rendered as two independent fields, one question reads as two and can be answered twice in contradictory ways, so the companion folds into its select as one more option. For a single-select the two replace one another. For a multi-select they combine into one string, because those agents read the companion first and return early: a response carrying both silently loses the ticks. Such a bridge calls the tool with the questions as its input, which agent-shell renders as a JSON dump for want of anything better, beside a form saying the same thing. A tool call carrying a questionnaire is now recognised from that input: it reads as a question rather than the catch-all "other" kind, says nothing while its form is open, and once answered is left to speak for itself -- its own content already spells the answers out in prose. The form is removed at that point, so an answer reads the same whether it was just given or replayed from a restored session, which never brings the form back. An elicitation with no such tool call behind it, from an MCP server or scoped to a request, keeps its settled form as the only record of what was asked. Input is corrected rather than coerced: text that does not parse comes back prefilled for editing, and an integer field refuses a fraction rather than answering 1.9 with 1. A pending form arms the same idle timer an unanswered permission does, so subscribers hear about a question left unattended.
A fragment tags itself with its qualified-id for debugging, and adds nothing when that is off. Nothing was what it failed to do: the tag went on through `add-text-properties' either way, so with debugging off it set `help-echo' to nil across the region rather than skipping it, erasing any help the content there carried. No body carried any until now, which is why it never showed. A body holds arbitrary rendered content, and `display-local-help' is the way to read the help that content offers, so the property has to survive. Debugging on still covers the body with the id, since surfacing qualified-ids is what that mode is for.
An agent may attach a preview to an answer -- a mockup, a snippet, the shape of what picking it would do -- so the user can see what they are choosing before they choose it. Ignoring it left that unreachable. The preview hangs off the option rather than becoming a control of its own. A disclosure glyph, the same one every fold in the buffer uses, marks an answer that has one; the glyph acts but is not navigable, so a question offering a preview is still a single TAB stop rather than two. Opening it is bound on the option as well as on the glyph, since `end-of-line' lands after the glyph rather than on it, and RET past the end of a line should go on meaning what it means everywhere else. The preview is also offered as `help-echo', which `display-local-help' reads out without opening anything, so it costs no keybinding of ours. Which previews are open is kept in the elicitation beside the field values, not in the rendered text, since the form is drawn into both the shell buffer and the viewport and text-local state would exist there in two copies that could disagree. The `_meta' key carrying a preview is namespaced to the agent that sends it, unlike the free-text companion marker which is deliberately un-namespaced for use across agents. Previews are therefore Claude's alone today; an option without one renders as it did before.
OSadovy
force-pushed
the
elicitation-forms
branch
from
August 23, 2026 12:36
4eb52fe to
587a0e8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #792.
What it does
elicitation.formininitialize.urlis not supported yet.elicitation/createas an in-buffer form covering everyElicitationPropertySchemavariant, both spellings of each enum shape (oneOf/enum,items.anyOf/items.enum), and the forward-compat unknown type.minLength,pattern,minimum,minItemsand friends are not implemented yet - left for the agent to reject.blockedwhile a form is pending, so a queued prompt isn't steered into a turn that's waiting on an answer.Slightly opionated behaviour that isn't obvious
_metakey. Rendered as two independent fields, one question reads as two and can be answered twice in contradictory ways.?) or its disclosure glyph, and are offered ashelp-echosoC-h .reads them without opening.?is bound only on options that actually carry a preview, so it stays free everywhere else - the viewport still opens its help menu with it. That_metakey is agent-namespaced, so previews are Claude-only and their absence changes nothing.Incidental fixes
Happy to submit those as separate PRs.
agent-shell-ui--echo-action-hinttakes a command and keymap, so chrome bound to its own map names its own key.help-echoits body's content carried - it set the property to nil whenever debugging was off.Not included
urlmode; constraint validation; digit shortcuts; eldoc.Where this differs from #792
I wrote the issue before building any of it, and a few things changed on the way.
Cancelledwithout preserving whatever had been typed into it.One question
acp-make-initialize-requesthardcodesclientCapabilitiesto thefspair, so there's no way to advertise elicitation through it. I build the request locally instead (agent-shell--make-initialize-request), which does mean agent-shell no longer picks up future changes to that constructor. Would you rather acp.el grew a capabilities argument? Happy to send that first and keep using the helper here.Checklist
M-x checkdocandM-x byte-compile-file.