Merge remote-tracking branch 'origin/master' into worktree/fix-multi-select-custom-answer
# Conflicts: # apps/web/tests/snapshots/question-composer/answered.expected.md # apps/web/tests/snapshots/question-composer/session.jsonl # docs/core-data-structures/user-interaction.i18n.yaml # packages/client/ui-question/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/ui/tui/README.i18n.yaml # packages/ui/user-interaction/README.i18n.yaml
This commit is contained in:
2065 files changed
+168375
-14240
No files matched your search
@@ -20,7 +20,8 @@ declare module 'cordis' {
|
||||
import type { AskUserQuestionAnswer, AskUserQuestionItem } from './types.ts'
|
||||
|
||||
export type {
|
||||
AskUserQuestionAnswer, AskUserQuestionAnswerItem, AskUserQuestionItem, AskUserQuestionOption,
|
||||
AskUserQuestionAnswer, AskUserQuestionAnswerItem, AskUserQuestionIntent, AskUserQuestionItem,
|
||||
AskUserQuestionOption,
|
||||
} from './types.ts'
|
||||
|
||||
/** Request for a human answer. */
|
||||
@@ -86,6 +87,28 @@ export class UserInteractionService extends Service {
|
||||
if (request.questions.length === 0) {
|
||||
throw new UserInteractionError('ask_user_question requires at least one question', 'EMPTY_QUESTIONS')
|
||||
}
|
||||
// A presentation intent asserts two things the types cannot: that the
|
||||
// named approve label is one of this question's own options, and that a
|
||||
// plan-review carries the plan it is a review of. A UI honouring the
|
||||
// intent answers with that label, and shows that detail as the plan, so
|
||||
// either gap would put a choice the asker never offered — or an approval of
|
||||
// something invisible — in front of the user. Caught at the asker, where
|
||||
// the mistake is, rather than in each UI.
|
||||
for (const question of request.questions) {
|
||||
const intent = question.intent
|
||||
if (intent === undefined) continue
|
||||
if (!(question.options ?? []).some(option => option.label === intent.approve)) {
|
||||
throw new UserInteractionError(
|
||||
`question ${question.id} declares intent ${intent.kind} whose approve label `
|
||||
+ `${JSON.stringify(intent.approve)} names none of its options`,
|
||||
'BAD_INTENT')
|
||||
}
|
||||
if (question.detail === undefined) {
|
||||
throw new UserInteractionError(
|
||||
`question ${question.id} declares intent ${intent.kind} without the detail it reviews`,
|
||||
'BAD_INTENT')
|
||||
}
|
||||
}
|
||||
if (this.provider === undefined) {
|
||||
throw new UserInteractionError('no user-interaction provider is registered', 'NO_PROVIDER')
|
||||
}
|
||||
|
||||
@@ -13,6 +13,24 @@ export interface AskUserQuestionOption {
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A caller-declared presentation intent: the question IS a decision of this
|
||||
* shape, so a UI that recognises the tag may present it as such instead of as a
|
||||
* generic option list. Tagged so further intents can be added; a UI that does
|
||||
* not know a tag renders the generic flow, and the answer encoding is identical
|
||||
* either way — an intent shapes presentation only, never the protocol.
|
||||
*/
|
||||
export type AskUserQuestionIntent = {
|
||||
/** A plan submitted for review: `detail` is the plan markdown `ask()` requires, and the decision approves or declines it. */
|
||||
kind: 'plan-review'
|
||||
/**
|
||||
* The option label that approves the plan; every other option declines it.
|
||||
* Named rather than positional so no UI infers the verdict from option order.
|
||||
* An `approve` naming no option of its own question is rejected at `ask()`.
|
||||
*/
|
||||
approve: string
|
||||
}
|
||||
|
||||
/** One question in a user-interaction request. */
|
||||
export interface AskUserQuestionItem {
|
||||
/** Stable caller-provided question id, echoed in the answer. */
|
||||
@@ -27,6 +45,8 @@ export interface AskUserQuestionItem {
|
||||
options?: AskUserQuestionOption[]
|
||||
/** Whether more than one option may be selected. Defaults to single-select. */
|
||||
multiSelect?: boolean
|
||||
/** Optional presentation intent for capable UIs; absent asks for the generic option list. */
|
||||
intent?: AskUserQuestionIntent
|
||||
}
|
||||
|
||||
/** Answer to one question. */
|
||||
|
||||
Reference in New Issue
Block a user