Skip to content

feat(desktop): render and answer agent questions in the channel - #20

Merged
oscarlehuu merged 5 commits into
mainfrom
devin/1785592312-desktop-question-card
Aug 1, 2026
Merged

feat(desktop): render and answer agent questions in the channel#20
oscarlehuu merged 5 commits into
mainfrom
devin/1785592312-desktop-question-card

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Phase 2 of the ACP question work (#14 landed the harness side). Until now a question published by the harness was only answerable through buzz user-input answer; this puts a card in the channel and publishes the 46041 answer from the desktop identity. It also adds the terminal half of the lifecycle, which real-process E2E showed was missing.

46040 arrives (backfill + live REQ, #h scoped)
  → derive pending = requests with no answer authored by me and no resolution
  → card docked above the composer in ChannelPane
  → submit → Tauri send_channel_user_input_answer
           → build_agent_user_input_answer (h + e tags) → submit_event
harness → 46042 resolution (answered | declined | cancelled) → card goes terminal

Points worth reviewing rather than reading off the diff:

  • A question whose ACP side is gone must stop looking answerable. E2E caught the real failure: after cancelling the turn the harness correctly sent one {"action":"cancel"}, but the card stayed editable and Submit dismissed it exactly like a real answer while the agent received nothing. So the harness now publishes a durable 46042 resolution for every terminal path it can observe — answered, declined, turn-cancelled, graceful shutdown — exactly once, and the card renders terminal. A crashed harness publishes nothing, so a card can still go stale; that limit is commented rather than papered over with a fake heartbeat.
  • Terminal is not a history feed. A resolution only turns into a terminal card for a request that was pending during this mount; anything resolved before we ever showed it stays invisible, otherwise backfill would resurrect every question ever asked as a permanent stack above the composer. Terminal cards are dismissible.
  • required now reaches the client. The harness knew which schema fields were required (FieldMapping.required) but never told anyone, so Submit demanded an answer to every question — with a required question answered, an optional multi-select still blocked submission and the only escape nulled everything. Submit now gates on required questions plus at least one answer, and unanswered optional questions are omitted, which is what the answer reconstruction already does with None. The field defaults to false so pre-existing request events still deserialize.
  • Questions deliberately never enter the message timeline. They get their own per-channel REQ instead of joining CHANNEL_EVENT_KINDS, because the channel-window projection drops unrecognized kinds anyway and a question is not a message row. Backfill (fetchEvents) plus a live subscription, so a question asked while the app was closed shows up on open.
  • The card is docked at the ChannelPane overlay, not inside MessageComposer — that component is shared with the thread, inbox and new-message surfaces, and this phase is channel-only.
  • Nothing is hand-rolled on the wire. The Tauri command reuses the existing buzz-sdk builder and the submit_event sign/submit seam. activeChannel.id is the NIP-29 channel UUID — the conversation-key mix-up that broke feat(acp): agent-directed user questions via ACP form elicitation #14 in real channels cannot recur here.
  • Publishing failures keep the question pending. A rejected answer is silently ignored by the harness, so the UI never implies success it cannot observe: a publish error surfaces inline with the draft intact, and a successful publish shows "sent, waiting for the agent" rather than resolving the request outright.
  • Owner gating is best-effort and says so. If profiles[agentPubkey].ownerPubkey is known and isn't the current user the card is read-only; when the profile hasn't been fetched we let the attempt through and defer to the harness, which is the actual authority (and the only one that can judge same-owner siblings).
  • "Answer nothing" rather than "Skip". It publishes {"qN": null} = None, which omits optional fields and declines the form when the engine marked a field required — not a local dismissal, so the label doesn't pretend otherwise.
  • allow_notes is hard-coded false in the harness because ACP has no notes concept; the desktop's {selected, choice_notes} path is therefore unreachable today and untested. Commented at the source so it doesn't read as a bug.

Not in this PR: mobile card, chat-reply fallback, inline collapse next to the originating tool_call row (needs toolCallId promoted to a first-class transcript field), and a durable pending-interaction projection — pending state is still derived client-side from request/answer/resolution events.

Related issue

None found. Follows #14.

Testing

Unit: pending derivation (answers and resolutions resolve a request, foreign-authored answers do not, dedupe, optimistic resolution), all four answer wire shapes against the buzz-core serde contract, custom/selection exclusivity, per-option notes pruning, publish-failure handling, required-only Submit gating and empty-submit rejection; harness-side, exactly one resolution per terminal outcome and never two for the same request, plus required round-trip and legacy-event defaulting.

Playwright smoke: question arrives → card renders → option selected → submit → exact bridge payload asserted → card leaves pending; plus the terminal/dismiss path.

Real-process E2E (real Tauri app + local relay + real buzz-acp harness driving an ACP-speaking fake agent, in a normal stream channel) — full evidence in the PR comment: happy path with engine-native keys and no spurious turn, backfill, custom text, multi-select, multi-question forms, {"action":"decline"} on answer-nothing, relay-down inline error and retry, duplicate answers ignored, 12-option form scrolling, and timeline/thread/inbox isolation. The one failure it found (cancelled question still answerable) is what the 46042 work above fixes.

Link to Devin session: https://app.devin.ai/sessions/c69bb63719224c65964381e6b09210dc
Requested by: @oscarlehuu

devin-ai-integration Bot and others added 2 commits August 1, 2026 14:07
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
@oscarlehuu oscarlehuu self-assigned this Aug 1, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Author

Runtime E2E — desktop question card (real app + real relay + real buzz-acp)

Real Tauri app from this branch against a local relay and a real buzz-acp harness driving a fake ACP agent that speaks the protocol, in a normal open stream channel (not a DM). Every wire claim below is an actual JSON-RPC frame. Devin session

Happy path — passed

Question card above the composer

RECV {"id": "elicit-1", "result": {"action": "accept",
      "content": {"question_0": "production", "question_1": ["lint"]}}}

Engine-native keys, exactly one response frame, card leaves pending, no question/answer row in the timeline, and no spurious turn (7 session/prompt calls for 7 owner mentions — 46041 answers never start a turn).

⚠️ Failure — a cancelled question stays answerable and answering it silently looks successful

With a card open, !cancel correctly produced exactly one {"action":"cancel"} for elicit-5. The desktop card stayed fully pending; selecting an option and pressing Submit dismissed it exactly like a real answer. On the wire the agent received nothing more:

46041 1fd576a6… tags [["h","a64699ed-…"],["e","c03a27eb…"]]  {"q0":"production","q1":["lint"]}
46040 c03a27eb… → request_id "elicit-5"  (already answered with action:cancel)

Same stale-card class after a harness restart (a question stays "pending" forever). There is no terminal/expired state for a dead request — being fixed in this PR.

Variants, backfill and adversarial cases — passed
  • Backfill: question published while #general was open renders immediately on opening the channel (fetchEvents leg).
    Backfilled card
  • Custom text + multi-select: {"question_0_custom":"deploy to eu-west-1 only","question_1":["lint","tests"]}; two-question form delivered in one response.
  • Answer nothing: publishes {"q0":null,"q1":null}{"action":"decline"} (required field not falsely accepted).
  • Relay down at Submit: card stays editable with an inline error, no 46041 written, agent gets nothing; retry after restart delivers exactly one accept.
    Inline error when relay is down
  • Duplicate answer: second owner answer accepted by the relay, ignored by the harness (response count stays 1), card does not reappear.
  • Long form (12 options): card scrolls internally, composer stays visible and usable.
    Long form scrolls
  • Isolation: 7×46040 + 8×46041 stored for the channel, 0 rendered as message rows; no card in the thread composer or Inbox.
    No card in thread composer
Not covered / minor
  • allowNotes untested — unreachable: crates/buzz-acp/src/elicitation.rs:281 hard-codes allow_notes: false, so the {selected, choice_notes} UI cannot be produced by any real ACP schema.
  • Optional questions block Submit: with the required question_0 answered, Submit stayed disabled until the optional multi-select was ticked.
    Submit disabled with required question answered
  • Headless-VM note: the card renders as a blur unless the app is launched with WEBKIT_DISABLE_COMPOSITING_MODE=1 (not a PR defect).

devin-ai-integration Bot and others added 3 commits August 1, 2026 15:22
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
@oscarlehuu
oscarlehuu merged commit 6d7669e into main Aug 1, 2026
6 checks passed
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.

1 participant