@black942026/pi-ask-user 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +365 -2
- package/index.ts +2 -0
- package/package.json +62 -4
- package/src/adapters/pi.ts +238 -0
- package/src/answers.ts +84 -0
- package/src/ask.ts +103 -0
- package/src/config.ts +390 -0
- package/src/core.ts +302 -0
- package/src/deadline.ts +78 -0
- package/src/events.ts +96 -0
- package/src/index.ts +358 -0
- package/src/mode.ts +18 -0
- package/src/parse.ts +119 -0
- package/src/route.ts +144 -0
- package/src/schema.ts +255 -0
- package/src/types.ts +343 -0
- package/src/ui/custom.ts +887 -0
- package/src/ui/native.ts +112 -0
package/src/route.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { isUIMode } from "./mode.ts";
|
|
2
|
+
import type {
|
|
3
|
+
AskUserHost,
|
|
4
|
+
AskUserMode,
|
|
5
|
+
AskUserRoute,
|
|
6
|
+
AskUserSupport,
|
|
7
|
+
AskUserUiPreferences,
|
|
8
|
+
CustomUIRenderer,
|
|
9
|
+
HostCapabilities,
|
|
10
|
+
HostImplementations,
|
|
11
|
+
NativeDialogRunner,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derive the structural capabilities of a host.
|
|
16
|
+
*
|
|
17
|
+
* A capability is bound to the implementation object that performs it, so it is
|
|
18
|
+
* impossible to declare `customUI` (or `nativeDialogs`) without a real renderer.
|
|
19
|
+
* This is the anti-false-declaration guarantee: capabilities are structural, not
|
|
20
|
+
* a boolean the caller can assert independently.
|
|
21
|
+
*/
|
|
22
|
+
export function hostCapabilities(host: HostImplementations): HostCapabilities {
|
|
23
|
+
return {
|
|
24
|
+
customUI: typeof host.customUI?.render === "function",
|
|
25
|
+
nativeDialogs: typeof host.nativeDialogs?.run === "function",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The modes that can really run here, in probe-priority order: `custom` first
|
|
31
|
+
* when both are available, then `native`.
|
|
32
|
+
*/
|
|
33
|
+
export function probeRoutes(capabilities: HostCapabilities): AskUserRoute[] {
|
|
34
|
+
const routes: AskUserRoute[] = [];
|
|
35
|
+
if (capabilities.customUI) routes.push("custom");
|
|
36
|
+
if (capabilities.nativeDialogs) routes.push("native");
|
|
37
|
+
return routes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a host's route **once**, from the explicit configuration and the
|
|
42
|
+
* probe result:
|
|
43
|
+
*
|
|
44
|
+
* 1. an explicit `mode` wins whenever it is supplied — if its implementation is
|
|
45
|
+
* bound it becomes the route (`source: "configured"`), otherwise the result
|
|
46
|
+
* is `configured_unavailable` and the other bound mode is *not* substituted;
|
|
47
|
+
* 2. with no explicit `mode`, the probe result decides: `custom` when both can
|
|
48
|
+
* run, otherwise the single available mode (`source: "probed"`);
|
|
49
|
+
* 3. with nothing available, the result is `no_available_ui` — no route is
|
|
50
|
+
* fabricated.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveSupport(mode: AskUserMode | undefined, capabilities: HostCapabilities): AskUserSupport {
|
|
53
|
+
const available = probeRoutes(capabilities);
|
|
54
|
+
if (mode !== undefined) {
|
|
55
|
+
if (!isUIMode(mode)) {
|
|
56
|
+
return {
|
|
57
|
+
status: "invalid_config",
|
|
58
|
+
capabilities,
|
|
59
|
+
available,
|
|
60
|
+
reason: `Invalid UI mode ${JSON.stringify(mode)}; available modes: custom, native.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const bound = mode === "custom" ? capabilities.customUI : capabilities.nativeDialogs;
|
|
64
|
+
if (bound) {
|
|
65
|
+
return { status: "available", route: mode, source: "configured", capabilities, available };
|
|
66
|
+
}
|
|
67
|
+
const detail =
|
|
68
|
+
available.length > 0
|
|
69
|
+
? `Modes that can run here: ${available.join(", ")}`
|
|
70
|
+
: "This environment has no usable interactive UI (neither a custom TUI nor a callable native input dialog)";
|
|
71
|
+
return {
|
|
72
|
+
status: "configured_unavailable",
|
|
73
|
+
configured: mode,
|
|
74
|
+
capabilities,
|
|
75
|
+
available,
|
|
76
|
+
reason: `UI mode "${mode}" was configured explicitly, but this environment cannot run it; ${detail}. No other mode will be used.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (available.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
status: "no_available_ui",
|
|
82
|
+
capabilities,
|
|
83
|
+
available: [],
|
|
84
|
+
reason: "This environment has no usable interactive UI: neither a custom TUI nor a callable native input dialog.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { status: "available", route: available[0]!, source: "probed", capabilities, available };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CreateAskUserHostOptions {
|
|
91
|
+
/** Human-readable adapter name, used in diagnostics. */
|
|
92
|
+
name: string;
|
|
93
|
+
/** Explicit UI route. When omitted, the route is probed from the implementations below. */
|
|
94
|
+
mode?: AskUserMode;
|
|
95
|
+
customUI?: CustomUIRenderer;
|
|
96
|
+
nativeDialogs?: NativeDialogRunner;
|
|
97
|
+
/** User presentation preferences, resolved once by the caller (option > file). */
|
|
98
|
+
preferences?: AskUserUiPreferences;
|
|
99
|
+
/**
|
|
100
|
+
* When set, the user configuration was invalid: the host is created
|
|
101
|
+
* unusable, with `support.status === "invalid_config"` and this actionable
|
|
102
|
+
* reason. Capabilities are still probed so the error can name what could
|
|
103
|
+
* have run — but no route is fabricated and nothing is ever asked.
|
|
104
|
+
*/
|
|
105
|
+
configError?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a host adapter, binding only the implementations that were supplied and
|
|
110
|
+
* resolving {@link AskUserHost.support} here — at creation. Nothing is read from
|
|
111
|
+
* the environment and no route is selected again later.
|
|
112
|
+
*
|
|
113
|
+
* An invalid user configuration makes the host unusable: its support is
|
|
114
|
+
* `invalid_config` with that reason, so every call is refused actionably rather
|
|
115
|
+
* than falling back to a probed route.
|
|
116
|
+
*/
|
|
117
|
+
export function createAskUserHost(options: CreateAskUserHostOptions): AskUserHost {
|
|
118
|
+
const capabilities = hostCapabilities(options);
|
|
119
|
+
const host: AskUserHost = {
|
|
120
|
+
name: options.name,
|
|
121
|
+
support:
|
|
122
|
+
options.configError !== undefined
|
|
123
|
+
? {
|
|
124
|
+
status: "invalid_config",
|
|
125
|
+
capabilities,
|
|
126
|
+
available: probeRoutes(capabilities),
|
|
127
|
+
reason: options.configError,
|
|
128
|
+
}
|
|
129
|
+
: resolveSupport(options.mode, capabilities),
|
|
130
|
+
};
|
|
131
|
+
if (options.preferences !== undefined) host.preferences = options.preferences;
|
|
132
|
+
if (options.customUI) host.customUI = options.customUI;
|
|
133
|
+
if (options.nativeDialogs) host.nativeDialogs = options.nativeDialogs;
|
|
134
|
+
return host;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Inspect what a host can prompt with, without prompting. Returns the same
|
|
139
|
+
* value resolved at host creation, so callers can decide up front whether a
|
|
140
|
+
* request would be serviced, refused, or impossible — and through which route.
|
|
141
|
+
*/
|
|
142
|
+
export function askUserSupport(host: AskUserHost): AskUserSupport {
|
|
143
|
+
return host.support;
|
|
144
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { AskUserOption, AskUserQuestion, NormalizedQuestion, NormalizedRequest, QuestionKind } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const MAX_QUESTIONS = 5;
|
|
5
|
+
export const MAX_OPTIONS = 5;
|
|
6
|
+
export const DEFAULT_TIMEOUT_PER_QUESTION_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
export const LIMITS = {
|
|
9
|
+
title: 200,
|
|
10
|
+
prompt: 500,
|
|
11
|
+
label: 80,
|
|
12
|
+
description: 240,
|
|
13
|
+
preview: 4000,
|
|
14
|
+
defaultValue: 500,
|
|
15
|
+
header: 200,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const ID_MAX_LENGTH = 64;
|
|
19
|
+
const QUESTION_KINDS = ["single", "multi", "input"] as const;
|
|
20
|
+
const QUESTION_FIELDS = ["id", "title", "prompt", "kind", "options", "default"] as const;
|
|
21
|
+
const OPTION_FIELDS = ["label", "description", "preview"] as const;
|
|
22
|
+
const REQUEST_FIELDS = ["questions", "header", "displayMode", "timeoutPerQuestionMs"] as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Flat string enum. Emitted as `{ type: "string", enum: [...] }` because some
|
|
26
|
+
* provider proxies reject `anyOf`/`union` shapes.
|
|
27
|
+
*/
|
|
28
|
+
function StringEnum<T extends readonly string[]>(values: T, options: Record<string, unknown> = {}) {
|
|
29
|
+
return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const OptionSchema = Type.Object({
|
|
33
|
+
label: Type.String({ minLength: 1, maxLength: LIMITS.label }),
|
|
34
|
+
description: Type.Optional(Type.String({ maxLength: LIMITS.description })),
|
|
35
|
+
preview: Type.Optional(Type.String({ maxLength: LIMITS.preview })),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const QuestionSchema = Type.Object({
|
|
39
|
+
id: Type.Optional(Type.String({ minLength: 1, maxLength: ID_MAX_LENGTH })),
|
|
40
|
+
title: Type.String({ minLength: 1, maxLength: LIMITS.title }),
|
|
41
|
+
prompt: Type.Optional(Type.String({ maxLength: LIMITS.prompt })),
|
|
42
|
+
kind: Type.Optional(StringEnum(QUESTION_KINDS)),
|
|
43
|
+
options: Type.Optional(Type.Array(OptionSchema, { maxItems: MAX_OPTIONS })),
|
|
44
|
+
default: Type.Optional(Type.String({ maxLength: LIMITS.defaultValue })),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parameter schema registered for the `ask_user` tool, and the single input
|
|
49
|
+
* contract for direct `askUser` calls too. Only these fields and types are read;
|
|
50
|
+
* anything else is rejected as `invalid_request` rather than coerced.
|
|
51
|
+
*
|
|
52
|
+
* The UI route is host configuration, resolved by the host adapter — it is never
|
|
53
|
+
* part of a request.
|
|
54
|
+
*/
|
|
55
|
+
export const AskUserParams = Type.Object({
|
|
56
|
+
questions: Type.Array(QuestionSchema, { minItems: 1, maxItems: MAX_QUESTIONS }),
|
|
57
|
+
header: Type.Optional(Type.String({ maxLength: LIMITS.header })),
|
|
58
|
+
displayMode: Type.Optional(StringEnum(["overlay", "inline"] as const)),
|
|
59
|
+
timeoutPerQuestionMs: Type.Optional(Type.Number({ minimum: 0 })),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export class AskUserValidationError extends Error {
|
|
63
|
+
readonly code = "invalid_request";
|
|
64
|
+
|
|
65
|
+
constructor(message: string) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "AskUserValidationError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Reject any field that is not part of the declared contract. */
|
|
76
|
+
function assertKnownKeys(record: Record<string, unknown>, allowed: readonly string[], where: string): void {
|
|
77
|
+
for (const key of Object.keys(record)) {
|
|
78
|
+
if (!allowed.includes(key)) throw new AskUserValidationError(`${where} has an unknown field "${key}".`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A required, non-empty, length-bounded string; returns the trimmed value. */
|
|
83
|
+
function requireText(value: unknown, where: string, maxLength: number): string {
|
|
84
|
+
if (typeof value !== "string") throw new AskUserValidationError(`${where} must be a string.`);
|
|
85
|
+
const text = value.trim();
|
|
86
|
+
if (text === "") throw new AskUserValidationError(`${where} must not be empty.`);
|
|
87
|
+
if (text.length > maxLength) throw new AskUserValidationError(`${where} exceeds the ${maxLength}-character limit.`);
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** An optional, length-bounded string; absent stays absent. */
|
|
92
|
+
function optionalText(value: unknown, where: string, maxLength: number): string | undefined {
|
|
93
|
+
if (value === undefined) return undefined;
|
|
94
|
+
if (typeof value !== "string") throw new AskUserValidationError(`${where} must be a string.`);
|
|
95
|
+
if (value.length > maxLength) throw new AskUserValidationError(`${where} exceeds the ${maxLength}-character limit.`);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function optionalQuestionId(value: unknown, where: string): string | undefined {
|
|
100
|
+
if (value === undefined) return undefined;
|
|
101
|
+
if (typeof value !== "string") throw new AskUserValidationError(`${where} "id" must be a string.`);
|
|
102
|
+
const id = value.trim();
|
|
103
|
+
if (id === "") throw new AskUserValidationError(`${where} "id" must not be empty.`);
|
|
104
|
+
if (id.length > ID_MAX_LENGTH) throw new AskUserValidationError(`${where} "id" exceeds the ${ID_MAX_LENGTH}-character limit.`);
|
|
105
|
+
return id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A canonical option object: `{ label, description?, preview? }`. */
|
|
109
|
+
function readOption(value: unknown, where: string, position: number): AskUserOption {
|
|
110
|
+
const at = `${where} option ${position + 1}`;
|
|
111
|
+
if (!isRecord(value)) throw new AskUserValidationError(`${at} must be an object with a "label".`);
|
|
112
|
+
assertKnownKeys(value, OPTION_FIELDS, at);
|
|
113
|
+
const label = requireText(value.label, `${at} label`, LIMITS.label);
|
|
114
|
+
const option: AskUserOption = { label };
|
|
115
|
+
const description = optionalText(value.description, `${at} description`, LIMITS.description);
|
|
116
|
+
if (description !== undefined) option.description = description;
|
|
117
|
+
const preview = optionalText(value.preview, `${at} preview`, LIMITS.preview);
|
|
118
|
+
if (preview !== undefined) option.preview = preview;
|
|
119
|
+
return option;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readQuestion(value: unknown, index: number): { question: AskUserQuestion; warnings: string[] } {
|
|
123
|
+
const warnings: string[] = [];
|
|
124
|
+
const where = `Question ${index + 1}`;
|
|
125
|
+
if (!isRecord(value)) throw new AskUserValidationError(`${where} must be an object.`);
|
|
126
|
+
assertKnownKeys(value, QUESTION_FIELDS, where);
|
|
127
|
+
|
|
128
|
+
const title = requireText(value.title, `${where} title`, LIMITS.title);
|
|
129
|
+
|
|
130
|
+
let kind: QuestionKind | undefined;
|
|
131
|
+
if (value.kind !== undefined) {
|
|
132
|
+
if (typeof value.kind !== "string" || !QUESTION_KINDS.includes(value.kind as QuestionKind)) {
|
|
133
|
+
throw new AskUserValidationError(`${where} "kind" must be "single", "multi", or "input".`);
|
|
134
|
+
}
|
|
135
|
+
kind = value.kind as QuestionKind;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rawOptions = value.options;
|
|
139
|
+
if (rawOptions !== undefined && !Array.isArray(rawOptions)) {
|
|
140
|
+
throw new AskUserValidationError(`${where} "options" must be an array.`);
|
|
141
|
+
}
|
|
142
|
+
const rawOptionList = rawOptions ?? [];
|
|
143
|
+
if (rawOptionList.length > MAX_OPTIONS) {
|
|
144
|
+
throw new AskUserValidationError(`${where} has more than ${MAX_OPTIONS} options.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const options: AskUserOption[] = [];
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
rawOptionList.forEach((rawOption, position) => {
|
|
150
|
+
const option = readOption(rawOption, where, position);
|
|
151
|
+
if (seen.has(option.label)) {
|
|
152
|
+
warnings.push(`${where} has a duplicate option "${option.label}"; de-duplicated.`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
seen.add(option.label);
|
|
156
|
+
options.push(option);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// When `kind` is omitted it is inferred from `options`: present ⇒ `single`,
|
|
160
|
+
// absent ⇒ `input`. `multi` is only ever explicit.
|
|
161
|
+
if (kind === undefined) kind = options.length > 0 ? "single" : "input";
|
|
162
|
+
if (kind !== "input" && options.length === 0) {
|
|
163
|
+
throw new AskUserValidationError(`${where} is a "${kind}" question but has no options.`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const question: AskUserQuestion = { title, kind };
|
|
167
|
+
const prompt = optionalText(value.prompt, `${where} prompt`, LIMITS.prompt);
|
|
168
|
+
if (prompt !== undefined) question.prompt = prompt;
|
|
169
|
+
if (kind === "input") {
|
|
170
|
+
delete question.options;
|
|
171
|
+
} else {
|
|
172
|
+
question.options = options;
|
|
173
|
+
}
|
|
174
|
+
const id = optionalQuestionId(value.id, where);
|
|
175
|
+
if (id) question.id = id;
|
|
176
|
+
const fallback = optionalText(value.default, `${where} default`, LIMITS.defaultValue);
|
|
177
|
+
if (fallback !== undefined) question.default = fallback;
|
|
178
|
+
return { question, warnings };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readDisplayMode(value: unknown): "overlay" | "inline" {
|
|
182
|
+
if (value === undefined) return "overlay";
|
|
183
|
+
if (value !== "overlay" && value !== "inline") {
|
|
184
|
+
throw new AskUserValidationError('displayMode must be "overlay" or "inline".');
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readTimeoutPerQuestionMs(value: unknown): number {
|
|
190
|
+
if (value === undefined) return DEFAULT_TIMEOUT_PER_QUESTION_MS;
|
|
191
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
192
|
+
throw new AskUserValidationError("timeoutPerQuestionMs must be a non-negative finite number.");
|
|
193
|
+
}
|
|
194
|
+
return Math.floor(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Normalize and validate a request against the {@link AskUserParams} contract. */
|
|
198
|
+
export function normalizeAskUserRequest(raw: unknown): { request: NormalizedRequest; warnings: string[] } {
|
|
199
|
+
if (!isRecord(raw)) throw new AskUserValidationError("Missing the questions array.");
|
|
200
|
+
for (const key of Object.keys(raw)) {
|
|
201
|
+
if (!REQUEST_FIELDS.includes(key as (typeof REQUEST_FIELDS)[number])) {
|
|
202
|
+
throw new AskUserValidationError(`Unknown field "${key}".`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const rawQuestions = raw.questions;
|
|
206
|
+
if (!Array.isArray(rawQuestions)) throw new AskUserValidationError("Missing the questions array.");
|
|
207
|
+
if (rawQuestions.length === 0) throw new AskUserValidationError("At least 1 question is required.");
|
|
208
|
+
if (rawQuestions.length > MAX_QUESTIONS) {
|
|
209
|
+
throw new AskUserValidationError(`At most ${MAX_QUESTIONS} questions; received ${rawQuestions.length}.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const warnings: string[] = [];
|
|
213
|
+
const usedIds = new Set<string>();
|
|
214
|
+
const questions: NormalizedQuestion[] = [];
|
|
215
|
+
rawQuestions.forEach((rawQuestion, index) => {
|
|
216
|
+
const { question, warnings: questionWarnings } = readQuestion(rawQuestion, index);
|
|
217
|
+
warnings.push(...questionWarnings);
|
|
218
|
+
|
|
219
|
+
let id = question.id ?? "";
|
|
220
|
+
if (id === "" || usedIds.has(id)) {
|
|
221
|
+
let candidate = `q${index + 1}`;
|
|
222
|
+
let suffix = 1;
|
|
223
|
+
while (usedIds.has(candidate)) {
|
|
224
|
+
candidate = `q${index + 1}_${suffix++}`;
|
|
225
|
+
}
|
|
226
|
+
id = candidate;
|
|
227
|
+
}
|
|
228
|
+
usedIds.add(id);
|
|
229
|
+
|
|
230
|
+
const normalized: NormalizedQuestion = {
|
|
231
|
+
id,
|
|
232
|
+
index,
|
|
233
|
+
title: question.title,
|
|
234
|
+
kind: question.kind,
|
|
235
|
+
options: question.options ?? [],
|
|
236
|
+
hasDefault: question.default !== undefined,
|
|
237
|
+
};
|
|
238
|
+
if (question.prompt !== undefined) normalized.prompt = question.prompt;
|
|
239
|
+
if (question.default !== undefined) normalized.default = question.default;
|
|
240
|
+
questions.push(normalized);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const header = optionalText(raw.header, "header", LIMITS.header);
|
|
244
|
+
const displayMode = readDisplayMode(raw.displayMode);
|
|
245
|
+
const timeoutPerQuestionMs = readTimeoutPerQuestionMs(raw.timeoutPerQuestionMs);
|
|
246
|
+
|
|
247
|
+
const request: NormalizedRequest = {
|
|
248
|
+
questions,
|
|
249
|
+
displayMode,
|
|
250
|
+
timeoutPerQuestionMs,
|
|
251
|
+
totalTimeoutMs: timeoutPerQuestionMs * questions.length,
|
|
252
|
+
};
|
|
253
|
+
if (header !== undefined) request.header = header;
|
|
254
|
+
return { request, warnings };
|
|
255
|
+
}
|