Agent-First UI v0.7.0: A Question Without a Page

by Agent-First Kit Contributors

`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

Breaking changes

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