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.
45 lines
3.6 KiB
Markdown
45 lines
3.6 KiB
Markdown
# @deepseek-ai/dsh-user-interaction
|
|
|
|
English | [中文](README.zh.md)
|
|
|
|
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)`.
|
|
- `UserInteractionError` — `HarnessError` 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.
|