Files
deepseek-harness/packages/ui/user-interaction/README.md
creatixchu 6d7bd7e703 fix(web): keep the plan card to decisions it can actually answer
Review follow-ups on the presentation intent.

The card claimed any single-question request declaring the intent, then sent one
of two labels — so a producer offering a third option, or a multi-select batch,
lost answers the generic flow would have shown. That contradicts the intent's
own contract, so `planReviewOf` now claims only a binary single choice and
leaves everything else to the flow that can express it.

`ask()` also rejects a plan-review intent on a question with no `detail`: the
intent declares detail IS the plan, and without one a honouring UI asks the
user to approve something invisible. The client keeps its own fallback — it sits
downstream of a wire boundary — but the misconfiguration now fails at the asker.

`planReviewOf` stops being a value export of the client contract face (client
export discipline: pure helpers stay internal; the tests already import it
relatively), and the ui-question README fallback list, both languages, now
states every condition the code enforces.
2026-07-30 19:38:18 +08:00

3.6 KiB

@deepseek-ai/dsh-user-interaction

English | 中文

Abstract user-interaction seam. It owns ctx.userInteraction, the service a model-facing tool or permission plugin uses when it needs to pause work and ask the human for a decision.

Service: UserInteractionService (ctx key: userInteraction)

Public API

  • ctx.userInteraction.registerProvider(provider): () => void Register the UI-side provider. Only one provider may be active in a context; disposal unregisters it.
  • ctx.userInteraction.ask(request): Promise<AskUserQuestionAnswer> Ask the active provider and wait for the answer.

Key Types

  • AskUserQuestionRequest{ questions: [{ id, question, detail?, header?, options?, multiSelect?, intent? }], agent?, signal? }; detail supplies supporting text that providers render with the question without turning it into an option label.
  • AskUserQuestionOption{ label, description? }.
  • AskUserQuestionIntent{ kind: 'plan-review', approve }; the tagged presentation intent below.
  • AskUserQuestionAnswer{ answers: [{ id, selected, custom? }] }.
  • UserInteractionProvider — UI implementation with ask(request).
  • UserInteractionErrorHarnessError subclass with codes such as EMPTY_QUESTIONS, BAD_INTENT, NO_PROVIDER, DUPLICATE_PROVIDER, and ASK_ABORTED.

When an answer includes custom, selected is empty; custom text is an override rather than a supplement to selected choices. A UI may preserve a skipped item as { id, selected: [] }, keeping the existing answer shape while retaining other answers in the batch.

Presentation intent

intent declares that a question IS a decision of a known shape, so a UI that recognises the tag may present it as such — plan-review says detail is a plan under review, and dsh-plan-mode sets it on the exit_plan_mode question. An intent shapes presentation only: a UI honouring it answers with the same option labels a generic UI would send, and a UI that does not know the tag renders the generic option list, so callers read one answer shape either way. approve names the label that approves rather than relying on option order. ask() rejects with BAD_INTENT the two assertions no type can carry: an approve naming none of that question's own options, and an intent on a question with no detail — the thing it declares itself a review of.

Role

This is the interface package. Model-facing consumers such as @deepseek-ai/dsh-tool-ask-user depend on this seam; dsh-tui and the host runtime provide interactive implementations. The loop stays unchanged: a tool call awaits a promise, and the tool result resumes the normal agent loop.

Model Experience

Indirectly, through dsh-tool-ask-user, which retains a successful provider answer as compact JSON or one of these failures: Error: ask_user_question was aborted before the user answered, Error: ask_user_question requires at least one question, Error: no user-interaction provider is registered, or Error: <message>. Waiting for the human adds no tokens.

KV Cache effect

No direct invalidation; the named consumer owns any request-prefix changes.

Known Limitations and Deferred Work

  • One provider per context — there is no routing or fan-out to multiple UIs; a second registration throws DUPLICATE_PROVIDER, and with none registered ask() throws NO_PROVIDER rather than degrading.
  • The vocabulary is the question-form shape only — selectable options plus optional custom text; richer interaction shapes (file pickers, diff-preview confirmations) have no seam vocabulary yet.