Agent-First UI v0.7.0: A Question Without a Page
`afui ask` puts a few questions with suggested answers in front of a person and returns one structured result, with no Provider, page, or button bindings to write. Notes sit on the choice they qualify, a written answer is one more card, and a caller whose commands time out can start a question, walk away, and collect the answer later.
The most common thing an agent needs from a person is not a page. It is an answer: which of these three, and is there something I did not think of. AFUI could already deliver that, but only the expensive way — a Provider program, an HTML page, button bindings, a typed runtime — for an interaction that is the same every time. This release ships that interaction as part of AFUI itself.
$ afui ask --question 'What should happen next?' \
--options '[{"id":"retry","label":"Retry","recommended":true},{"id":"details","label":"Inspect details"},{"id":"stop","label":"Stop"}]'
A window opens with the question, the options, and one more card: Write your own answer. The command waits, and prints one result:
{"kind":"result","result":{"code":"ask_answered","answers":[
{"question_id":"question","selected":[{"option_id":"retry","label":"Retry","note":"after the logs are saved"}]}
]},"trace":{}}
Delivery is the same as every other AFUI session: a window here, a link for another device, or a registered session someone opens from the remote view.
The shape of an answer
The first version of the page had one text box per question, below the options, whose meaning followed the selection: an answer when nothing was chosen, a note when something was. It was correct, and nobody could feel it. A box at the bottom of the list did not read as belonging to the option at the top of it.
So the page now puts each kind of text inside the card it belongs to, and shows it only for a chosen card. Choose an option and a one-line note opens in that card, growing as the person writes. In a single-choice question that note follows the selection, so changing one’s mind does not lose the draft; in a multiple-choice question each checked option has its own. Write your own answer behaves the same way: its box opens inside it when it is chosen. Until something is chosen, nothing is open, and the page reads as a list of answers rather than a form to fill.
The result follows the page. A note is a field of the selection it sits on,
and text only ever means a written answer — there is no field whose meaning
depends on another field. A single-choice question has a selection or a
written answer, never both; a multiple-choice question may have both, the text
standing for what the options did not cover. The server checks all of this
again, so a page that sends something else gets an error it can correct
rather than an ambiguous answer.
Two things the page will not do. It never preselects: a recommended option
carries a badge and nothing more, because a default is an answer the person
did not give. And choosing never submits; there is one Submit, enabled once
every question has an answer, and a Cancel before it.
Several questions, and what they are about
Related questions belong on one page, not in a sequence of windows, so
--questions takes up to eight, each single- or multiple-choice. Background
goes in --context as typed blocks — paragraphs, lists, code — instead of
being crammed into the question sentence:
$ afui ask \
--context '[{"type":"paragraph","text":"The review found two issues."},{"type":"list","items":["README lists feature details","The session subject is the whole question"]}]' \
--questions '[{"id":"scope","question":"Which should be fixed?","multiple":true,"options":[{"id":"readme","label":"README"},{"id":"subject","label":"Subject"}]}]'
The blocks are typed rather than Markdown on purpose. Every string in the request is shown as text, never parsed as markup; a block type is a decision about structure that the page makes, not markup the request smuggles in.
Asking without waiting
afui ask blocks until the question ends, which is right for a script and
wrong for a caller whose commands are killed after a few minutes. --detach
starts the question in a background process and returns as soon as it is
ready:
$ afui ask --detach --question 'Ship it?' --options '[{"id":"yes","label":"Yes"},{"id":"no","label":"No"}]'
{"kind":"result","result":{"code":"ask_started","mode":"window","session_id":"4181c256…"},"trace":{}}
$ afui ask wait 4181c256… --timeout-s 60
{"kind":"result","result":{"code":"ask_pending","session_id":"4181c256…","waiting_s":60},"trace":{}}
afui ask wait returns exactly what afui ask would have printed, and a
result is collected once. The background process leaves the caller’s process
group, so a caller that kills its command on timeout does not take the
question with it.
ask_pending carries waiting_s and nothing else. Reminding the person is
the caller’s decision: AFUI does not know where the person is, and the caller
already has whatever channel reaches them.
Smaller fixes
- A question’s registry subject is the first line of its first question, shortened to 120 characters. It used to be the whole question, up to 8 KiB, in a session list meant to be one line per session.
- A malformed JSON argument names the flag, the position, and the field that
was unknown, missing, or duplicated —
--options has unknown field `extra`— without echoing a value, which serde’s own message would have done. registry_opted_out()reports whetherAFUI_NO_REGISTRYkeeps this process’s sessions out of the registry, so a caller need not reimplement the rule.
Breaking changes
Error::InvalidAskis a new variant.Erroris exhaustive on purpose, so amatchover it needs the new arm.
The ask module is new in this release, and its types are the contract
above: AskRequest holds context and questions; an answer is
AskResponse::Answered { answers } with AskSelection { option_id, label, note } and an optional written text.
Also in this release: AFUI moves to scraper 0.29.
Getting it
$ brew install agentfirstkit/tap/afui
$ cargo install agent-first-ui