feat(desktop): render and answer agent questions in the channel - #20
Conversation
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>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Runtime E2E — desktop question card (real app + real relay + real
|
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>
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.Points worth reviewing rather than reading off the diff:
{"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.requirednow 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 withNone. The field defaults tofalseso pre-existing request events still deserialize.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.ChannelPaneoverlay, not insideMessageComposer— that component is shared with the thread, inbox and new-message surfaces, and this phase is channel-only.buzz-sdkbuilder and thesubmit_eventsign/submit seam.activeChannel.idis 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.profiles[agentPubkey].ownerPubkeyis 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).{"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_notesis 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_callrow (needstoolCallIdpromoted 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-coreserde 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, plusrequiredround-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-acpharness 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