Skip to content

feat: interactive question cards (AskUserQuestion → tappable Buzz cards) - #5120

Open
billy-mangaroa wants to merge 4 commits into
block:mainfrom
billy-mangaroa:feat/interactive-question-cards
Open

feat: interactive question cards (AskUserQuestion → tappable Buzz cards)#5120
billy-mangaroa wants to merge 4 commits into
block:mainfrom
billy-mangaroa:feat/interactive-question-cards

Conversation

@billy-mangaroa

Copy link
Copy Markdown

Interactive question cards (AskUserQuestion → tappable Buzz cards)

Renders Claude Code's built-in AskUserQuestion tool (used by discovery skills like /interview) as tappable cards inside a Buzz channel, instead of silently degrading to prose.

Why it was broken

AskUserQuestion surfaces over ACP as an elicitation/create form (mode:"form"). claude-agent-acp only enables the tool when the client advertises the elicitation.form capability; buzz-acp advertised none, so the adapter dropped AskUserQuestion into disallowedTools and the model fell back to prose.

What this does

  1. buzz-acp advertises elicitation: { form: {} } in initialize, dispatches inbound elicitation/create, and (in the pool) stands up a per-turn servicer that publishes one owner-locked question card per question, polls for the human's answer, and folds it back into the ACP CreateElicitationResponse. The wait is unbounded (the loop parks on the await, so idle/hard deadlines can't fire mid-wait; both are renewed after).
  2. buzz-core adds KIND_ELICITATION_REQUEST = 44300 (card) and KIND_ELICITATION_RESPONSE = 44301 (answer); buzz-relay scopes both as MessagesWrite.
  3. desktop adds a QuestionCard renderer (owner-locked, single/multi-select, "Other…" free-text, answered-state via live subscription).

Fail-closed throughout: no servicer / non-form / no questions / cancelled turn all answer { "action": "cancel" }, so the model cleanly falls back.

Tests

  • Pure bridge logic (schema→cards parse, answer→ACP-response fold): 11 unit tests.
  • Capability advertised; relay scope-allowlist guard covers the new kinds.
  • Live-relay e2e (e2e_elicitation.rs): the relay accepts both kinds and the owner's answer is retrievable via the exact filter the servicer polls with.
  • Desktop screenshot spec renders the interactive card.

clippy clean; cargo fmt applied.

🤖 Generated with Claude Code

@billy-mangaroa
billy-mangaroa requested a review from a team as a code owner August 6, 2026 22:34
…able Buzz cards)

Render Claude Code's AskUserQuestion (used by /interview) as tappable cards
inside a Buzz channel instead of degrading to prose. AskUserQuestion surfaces
over ACP as an `elicitation/create` form (gated on the client advertising the
`elicitation.form` capability); this lands the foundation + card renderer +
the pure bridge core, ahead of wiring the async round-trip in buzz-acp/pool.

- buzz-core: KIND_ELICITATION_REQUEST (44300, agent's question card) and
  KIND_ELICITATION_RESPONSE (44301, member's answer); registered in ALL_KINDS.
- buzz-relay: both kinds scoped as MessagesWrite in required_scope_for_kind.
- buzz-acp: pure, tested helpers (elicitation.rs) — parse a request's
  requestedSchema into per-question cards, parse an answer event, and fold
  answers back into the ACP CreateElicitationResponse keyed by the schema's
  question_<n>/question_<n>_custom properties (custom answer wins).
- desktop: QuestionCard renderer (owner-locked, single/multi-select, "Other…"
  free-text, answered-state via live subscription) wired into MessageRow;
  publishes a 44301 answer referencing the card.

Remaining (next commit): advertise elicitation.form in buzz-acp initialize,
dispatch `elicitation/create`, and add the pool servicer that publishes one
card per question and awaits the owner's answer. See
PLANS/INTERACTIVE_QUESTION_CARDS.md in the Buzz workspace.

Signed-off-by: Billy Lewis <hello@billylewis.co.nz>
Co-authored-by: Billy Lewis <hello@billylewis.co.nz>
Completes the interactive question-card feature: AskUserQuestion now
publishes tappable cards to the channel and feeds the owner's tap back to
the running skill.

- acp.rs: advertise `elicitation: { form: {} }` in initialize (this is the
  switch claude-agent-acp gates AskUserQuestion on); dispatch inbound
  `elicitation/create` in the prompt read loop via `service_elicitation`,
  which hands the parsed form to the pool servicer and writes the ACP
  `CreateElicitationResponse`. The wait for a tap is legitimately long
  (no timeout), so the loop is parked on the await — no idle/hard-deadline
  can fire mid-wait — and both deadlines are renewed afterwards.
- pool.rs: `ElicitationBridgeRequest` + a per-turn `run_elicitation_servicer`
  (installed only for channel turns with a known owner). It publishes one
  owner-locked 44300 card per question, polls the relay for the owner's
  44301 answers, and folds them into the ACP response. `AbortOnDrop` tears
  the servicer down on every turn-exit path; `send_prompt_result` clears the
  installed sender so heartbeats never inherit it.
- Fail-closed: no servicer / non-form / no questions / cancelled turn all
  answer `{ "action": "cancel" }`, so the model cleanly falls back to prose.

Tests: capability advertised; existing initialize-format test still green;
the pure parse/fold helpers covered by 11 unit tests. clippy clean.

Signed-off-by: Billy Lewis <hello@billylewis.co.nz>
Co-authored-by: Billy Lewis <hello@billylewis.co.nz>
Proves against a running relay what the unit tests cannot: the relay accepts
kind:44300 (card) and kind:44301 (answer), and the owner's answer is
retrievable via the exact filter the buzz-acp servicer polls with
(kind:44301 & author & #e=<card id>). Both tests green against the branch
relay (BUZZ_GIT_CONFORMANCE_PROBE=false, seeded localhost community).

Marked #[ignore] like the other e2e_* suites; run with:
  RELAY_URL=ws://localhost:3001 cargo test --test e2e_elicitation -- --ignored

Signed-off-by: Billy Lewis <hello@billylewis.co.nz>
Co-authored-by: Billy Lewis <hello@billylewis.co.nz>
Renders a 44300 card owner-locked to the mock identity and asserts the
open/interactive state (options as buttons + Other free-text), capturing
test-results/screenshots/question-card.png. Registered in the smoke project.

Signed-off-by: Billy Lewis <hello@billylewis.co.nz>
Co-authored-by: Billy Lewis <hello@billylewis.co.nz>
@billy-mangaroa
billy-mangaroa force-pushed the feat/interactive-question-cards branch from 34f9707 to 4f9ddce Compare August 6, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants