Skip to content

chat/voice: land voice answers on question carousels (fix Skipped) - #323161

Merged
Megan Rogge (meganrogge) merged 3 commits into
microsoft:meganrogge/voice-mode-fixesfrom
accnops:arthurcnops/voice-integration-latest
Jul 6, 2026
Merged

chat/voice: land voice answers on question carousels (fix Skipped)#323161
Megan Rogge (meganrogge) merged 3 commits into
microsoft:meganrogge/voice-mode-fixesfrom
accnops:arthurcnops/voice-integration-latest

Conversation

@accnops

@accnops Arthur Cnops (accnops) commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets voice users answer VS Code chat question carousel multi-question forms (and approvals) by voice. Fixes the end-to-end break where a voice-spoken selection never landed and the card showed "Skipped".

Three commits, stacked on meganrogge/voice-mode-fixes:

  1. respond_to_session answer/approval plumbing — voice tool dispatch routes question-carousel answers and approvals back into the chat session.
  2. Complete agent-host carousels so voice answers land — agent-host carousels resolve only via their completion promise (carousel.dismiss(answers)ChatInputCompleted), not the event the voice path fired. The answer branch now calls carousel.dismiss(toCarouselAnswers(answers)) in addition to notifyQuestionCarouselAnswer. An empty answer record maps to undefined so a genuine skip becomes Cancel rather than a truthy empty Accept. Adds a pure toCarouselAnswers helper + unit tests.
  3. Guard against stale skip overwrite — after an external dismiss(), a later onDidSubmitRequest auto-skip could call handleSubmit(defaults) and overwrite the resolved answers. skip()/ignore() now also guard on carousel.isUsed. Adds 2 regression tests.

Testing

  • tsc -p src/tsconfig.json --noEmit — 0 errors
  • eslint on changed files — 0 errors
  • Added unit + regression tests for toCarouselAnswers and the carousel skip/ignore guards.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/8157

Arthur Cnops added 3 commits June 26, 2026 12:43
…ssion

The voice client collapsed five distinct pending chat constructs into a single
agent_state:"waiting_for_confirmation" and dropped the option list, question
ids, and routing ids, so a voice answer to a questionCarousel form could never
land. The dispatch layer also only knew how to confirmWith a toolInvocation.

Break A (wire): voiceSessionController now emits a typed `pending` payload per
session carrying type (questions|approval|elicitation), the exact
pending_id/request_id/resolve_id/approval_kind, and the per-question schema with
options serialized in displayed (defaults-first) order so a spoken ordinal
matches the screen. The generic delta computer ships it and nulls it on clear.

Break C (apply): voiceToolDispatchService handles respond_to_session by exact
session+part resolution and applies per kind — confirmWith for
toolInvocation/postApproval, accept/reject for elicitation2, confirmation
resend, and notifyQuestionCarouselAnswer for carousels. Answers are resolved
deterministically against the live options (value -> 1-based ordinal -> label,
freeform fallback) and a structured {ok,reason} result lets the backend narrate
corrections.

Extract the carousel option-ordering into chatQuestionCarouselHelpers.ts and
reuse it from the widget, controller, and dispatch so all three agree on order;
add a unit test for ordering and answer resolution.

Lockstep with the voice_code backend respond_to_session protocol change.
A voice answer to a questionCarousel showed "Skipped" and never reached the
agent for vscode-chat-session://local/... (agent-host) sessions. The dispatch
layer resolved a carousel only by firing notifyQuestionCarouselAnswer, an event
consumed solely by the askQuestions and openBrowser tools. Agent-host carousels
resolve exclusively via their completion promise (carousel.completion.complete),
which dispatches ChatInputCompleted back to the agent; voice never settled it,
so the carousel was abandoned and later disposed empty -> isUsed -> "Skipped".

Fix: the answer path now calls carousel.dismiss(answers) when the live part is a
ChatQuestionCarouselData, in addition to the event. dismiss settles the
completion promise (the only agent-host resolution path) and marks the carousel
used in one idempotent call, mirroring the real UI submit which does both.

Empty resolved answers (the user answered nothing) map to undefined via a new
toCarouselAnswers helper so agent-host treats it as a cancel/skip rather than
submitting an empty accept. Unit-tested.
…nswers

ChatQuestionCarouselPart.skip()/ignore() only guarded on the part's own
_isSkipped and allowSkip. If the underlying carousel was resolved elsewhere
(e.g. a voice answer calling dismiss()) after the part had already rendered
interactively, a subsequent onDidSubmitRequest auto-skip would still invoke
onSubmit and overwrite carousel.data with default answers, so the summary
showed defaults instead of the answer that actually landed.

Guard both methods on carousel.isUsed so an already-resolved carousel is never
re-submitted from the UI part, regardless of how it was resolved. Adds
regression tests for the external-resolution race.
@meganrogge Megan Rogge (meganrogge) added this to the 1.129.0 milestone Jul 6, 2026

@meganrogge Megan Rogge (meganrogge) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@meganrogge
Megan Rogge (meganrogge) merged commit 38c5274 into microsoft:meganrogge/voice-mode-fixes Jul 6, 2026
6 of 9 checks passed
@vs-code-engineering vs-code-engineering Bot locked and limited conversation to collaborators Aug 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants