@black942026/pi-ask-user 0.0.0-stage → 0.1.1
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 +389 -2
- package/index.ts +2 -0
- package/package.json +62 -4
- package/src/adapters/pi.ts +239 -0
- package/src/answers.ts +84 -0
- package/src/ask.ts +103 -0
- package/src/config.ts +392 -0
- package/src/core.ts +322 -0
- package/src/deadline.ts +94 -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 +345 -0
- package/src/ui/custom.ts +908 -0
- package/src/ui/native.ts +112 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User configuration for AskUser.
|
|
3
|
+
*
|
|
4
|
+
* Configuration is a plain JSON file — never environment variables. It lives in
|
|
5
|
+
* a per-user directory and is read **once per host/session creation**, never per
|
|
6
|
+
* request:
|
|
7
|
+
*
|
|
8
|
+
* ~/.pi/ask-user/config.json
|
|
9
|
+
*
|
|
10
|
+
* No file (or an empty file) means the built-in defaults. An existing file is
|
|
11
|
+
* validated strictly: malformed JSON, unknown keys, and bad values are reported
|
|
12
|
+
* as actionable `invalid_config` errors instead of being silently ignored or
|
|
13
|
+
* coerced. There are deliberately no environment-variable knobs.
|
|
14
|
+
*
|
|
15
|
+
* Effective precedence for every knob is:
|
|
16
|
+
*
|
|
17
|
+
* explicit programmatic option > user config file > built-in default
|
|
18
|
+
*
|
|
19
|
+
* Built-in defaults (which the model's request may override when the user did
|
|
20
|
+
* not configure anything) are applied by the core, not here: this module only
|
|
21
|
+
* reports what the user actually configured, leaving unset knobs `undefined`.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import type { AskUserDisplayMode, AskUserMode, AskUserUiPreferences } from "./types.ts";
|
|
27
|
+
|
|
28
|
+
/** Directory holding the user config file. */
|
|
29
|
+
export const USER_CONFIG_DIR = join(homedir(), ".pi", "ask-user");
|
|
30
|
+
/** The documented user config file. */
|
|
31
|
+
export const USER_CONFIG_PATH = join(USER_CONFIG_DIR, "config.json");
|
|
32
|
+
|
|
33
|
+
/** Built-in default key that toggles the overlay between hidden and visible. */
|
|
34
|
+
export const DEFAULT_OVERLAY_TOGGLE_KEY = "alt+o";
|
|
35
|
+
/** Cancel the current questionnaire's countdown without dismissing the UI. */
|
|
36
|
+
export const CANCEL_TIMEOUT_KEY = "alt+q";
|
|
37
|
+
/** Built-in default custom-UI presentation. */
|
|
38
|
+
export const DEFAULT_DISPLAY_MODE: AskUserDisplayMode = "overlay";
|
|
39
|
+
|
|
40
|
+
const CONFIG_KEYS = ["mode", "displayMode", "overlayToggleKey", "timeoutPerQuestionMs"] as const;
|
|
41
|
+
const DISPLAY_MODES: readonly AskUserDisplayMode[] = ["overlay", "inline"];
|
|
42
|
+
/** Values that explicitly turn a configurable shortcut off. */
|
|
43
|
+
const SHORTCUT_DISABLE_VALUES = new Set(["off", "none", "disabled", ""]);
|
|
44
|
+
|
|
45
|
+
/** Modifiers pi-tui's KeyId grammar understands. */
|
|
46
|
+
const MODIFIER_NAMES = new Set(["ctrl", "shift", "alt", "super"]);
|
|
47
|
+
/** Printable base keys (a key that produces text and would steal typing). */
|
|
48
|
+
const SYMBOL_BASES = new Set([
|
|
49
|
+
"`",
|
|
50
|
+
"-",
|
|
51
|
+
"=",
|
|
52
|
+
"[",
|
|
53
|
+
"]",
|
|
54
|
+
"\\",
|
|
55
|
+
";",
|
|
56
|
+
"'",
|
|
57
|
+
",",
|
|
58
|
+
".",
|
|
59
|
+
"/",
|
|
60
|
+
"!",
|
|
61
|
+
"@",
|
|
62
|
+
"#",
|
|
63
|
+
"$",
|
|
64
|
+
"%",
|
|
65
|
+
"^",
|
|
66
|
+
"&",
|
|
67
|
+
"*",
|
|
68
|
+
"(",
|
|
69
|
+
")",
|
|
70
|
+
"_",
|
|
71
|
+
"+",
|
|
72
|
+
"|",
|
|
73
|
+
"~",
|
|
74
|
+
"{",
|
|
75
|
+
"}",
|
|
76
|
+
":",
|
|
77
|
+
"<",
|
|
78
|
+
">",
|
|
79
|
+
"?",
|
|
80
|
+
]);
|
|
81
|
+
const FUNCTION_BASES = new Set(
|
|
82
|
+
Array.from({ length: 12 }, (_, index) => `f${index + 1}`),
|
|
83
|
+
);
|
|
84
|
+
/** Every non-printable base key the KeyId grammar accepts. */
|
|
85
|
+
const SPECIAL_BASES = new Set([
|
|
86
|
+
"escape",
|
|
87
|
+
"esc",
|
|
88
|
+
"enter",
|
|
89
|
+
"return",
|
|
90
|
+
"tab",
|
|
91
|
+
"space",
|
|
92
|
+
"backspace",
|
|
93
|
+
"delete",
|
|
94
|
+
"insert",
|
|
95
|
+
"clear",
|
|
96
|
+
"home",
|
|
97
|
+
"end",
|
|
98
|
+
"pageup",
|
|
99
|
+
"pagedown",
|
|
100
|
+
"up",
|
|
101
|
+
"down",
|
|
102
|
+
"left",
|
|
103
|
+
"right",
|
|
104
|
+
...FUNCTION_BASES,
|
|
105
|
+
]);
|
|
106
|
+
/**
|
|
107
|
+
* Special keys the questionnaire itself uses. A raw toggle listener consumes a
|
|
108
|
+
* match *before* the component sees it, so any of these would shadow the
|
|
109
|
+
* interaction — `Esc` (cancel) above all. They are rejected, never silently
|
|
110
|
+
* swapped for something else.
|
|
111
|
+
*/
|
|
112
|
+
const RESERVED_BASES = new Set([
|
|
113
|
+
"escape",
|
|
114
|
+
"esc",
|
|
115
|
+
"enter",
|
|
116
|
+
"return",
|
|
117
|
+
"tab",
|
|
118
|
+
"space",
|
|
119
|
+
"backspace",
|
|
120
|
+
"delete",
|
|
121
|
+
"insert",
|
|
122
|
+
"clear",
|
|
123
|
+
"home",
|
|
124
|
+
"end",
|
|
125
|
+
"pageup",
|
|
126
|
+
"pagedown",
|
|
127
|
+
"up",
|
|
128
|
+
"down",
|
|
129
|
+
"left",
|
|
130
|
+
"right",
|
|
131
|
+
]);
|
|
132
|
+
/** Modifier chords that conflict with a questionnaire control. */
|
|
133
|
+
const RESERVED_SIGNATURES = new Set(
|
|
134
|
+
["ctrl+c", "ctrl+d", "ctrl+u", "ctrl+p", "ctrl+n", "ctrl+enter", CANCEL_TIMEOUT_KEY].map((id) => {
|
|
135
|
+
const parts = id.split("+");
|
|
136
|
+
const base = parts.pop()!;
|
|
137
|
+
return [...parts.sort(), base].join("+");
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
interface ParsedShortcut {
|
|
142
|
+
base: string;
|
|
143
|
+
modifiers: string[];
|
|
144
|
+
signature: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isPrintableBase(base: string): boolean {
|
|
148
|
+
return /^[a-z0-9]$/.test(base) || SYMBOL_BASES.has(base);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hasChordModifier(modifiers: string[]): boolean {
|
|
152
|
+
return modifiers.some((modifier) => modifier === "ctrl" || modifier === "alt" || modifier === "super");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Parse a spec against the supported KeyId grammar; `null` when malformed. */
|
|
156
|
+
function parseOverlayToggleSpec(spec: string): ParsedShortcut | null {
|
|
157
|
+
const parts = spec.toLowerCase().split("+");
|
|
158
|
+
if (parts.some((part) => part === "")) return null;
|
|
159
|
+
const base = parts.pop()!;
|
|
160
|
+
const modifiers = parts;
|
|
161
|
+
if (modifiers.some((modifier) => !MODIFIER_NAMES.has(modifier))) return null;
|
|
162
|
+
if (new Set(modifiers).size !== modifiers.length) return null;
|
|
163
|
+
if (!isPrintableBase(base) && !SPECIAL_BASES.has(base)) return null;
|
|
164
|
+
return { base, modifiers, signature: [...modifiers].sort().join("+") + "+" + base };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether a spec is safe to bind as the overlay show/hide key: a syntactically
|
|
169
|
+
* valid KeyId chord that neither collides with a questionnaire control nor
|
|
170
|
+
* captures ordinary typing. Used by the renderer as a defensive guard; the
|
|
171
|
+
* config layer reports the specific reason instead.
|
|
172
|
+
*/
|
|
173
|
+
export function isAllowedOverlayToggleSpec(spec: string): boolean {
|
|
174
|
+
const parsed = parseOverlayToggleSpec(spec.toLowerCase());
|
|
175
|
+
if (!parsed) return false;
|
|
176
|
+
if (RESERVED_BASES.has(parsed.base)) return false;
|
|
177
|
+
if (isPrintableBase(parsed.base) && !hasChordModifier(parsed.modifiers)) return false;
|
|
178
|
+
if (RESERVED_SIGNATURES.has(parsed.signature)) return false;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate a shortcut spec, or `null` to disable; throws an actionable
|
|
184
|
+
* {@link AskUserConfigError} on anything malformed or conflicting. Never
|
|
185
|
+
* substitutes a different key.
|
|
186
|
+
*/
|
|
187
|
+
export function normalizeOverlayToggleKey(value: unknown, source: string): string | null {
|
|
188
|
+
const where = errorSource(source);
|
|
189
|
+
if (value === null) return null;
|
|
190
|
+
if (typeof value !== "string") {
|
|
191
|
+
throw new AskUserConfigError(where, '"overlayToggleKey" must be a key chord like "alt+o", or null/"off"/"none"/"disabled" to disable it');
|
|
192
|
+
}
|
|
193
|
+
const spec = value.trim().toLowerCase();
|
|
194
|
+
if (SHORTCUT_DISABLE_VALUES.has(spec)) return null;
|
|
195
|
+
const parsed = parseOverlayToggleSpec(spec);
|
|
196
|
+
if (!parsed) {
|
|
197
|
+
throw new AskUserConfigError(where, `"overlayToggleKey" is not a supported key: ${JSON.stringify(value)} (expected a chord like "alt+o", or "off" to disable)`);
|
|
198
|
+
}
|
|
199
|
+
if (RESERVED_BASES.has(parsed.base)) {
|
|
200
|
+
throw new AskUserConfigError(where, `"overlayToggleKey" cannot be ${JSON.stringify(value)}: it is reserved by the questionnaire (for example Esc cancels and Enter submits). Choose another chord or set it to "off" to disable`);
|
|
201
|
+
}
|
|
202
|
+
if (isPrintableBase(parsed.base) && !hasChordModifier(parsed.modifiers)) {
|
|
203
|
+
throw new AskUserConfigError(where, `"overlayToggleKey" ${JSON.stringify(value)} would capture ordinary typing; use a chord with ctrl/alt/super such as "alt+o", or "off" to disable`);
|
|
204
|
+
}
|
|
205
|
+
if (RESERVED_SIGNATURES.has(parsed.signature)) {
|
|
206
|
+
throw new AskUserConfigError(where, `"overlayToggleKey" cannot be ${JSON.stringify(value)}: it conflicts with a questionnaire control. Choose another chord or set it to "off" to disable`);
|
|
207
|
+
}
|
|
208
|
+
return spec;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The knobs the user may set, in the order they are documented. Every field is
|
|
213
|
+
* optional: `undefined` means "not configured here", so a later layer (or the
|
|
214
|
+
* built-in default) applies.
|
|
215
|
+
*/
|
|
216
|
+
export interface AskUserConfigOverrides {
|
|
217
|
+
mode?: AskUserMode;
|
|
218
|
+
/** `undefined` = not configured. */
|
|
219
|
+
displayMode?: AskUserDisplayMode;
|
|
220
|
+
/** `undefined` = not configured (built-in `alt+o`); `null` = explicitly disabled. */
|
|
221
|
+
overlayToggleKey?: string | null;
|
|
222
|
+
/** `undefined` = not configured. */
|
|
223
|
+
timeoutPerQuestionMs?: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The outcome of resolving the config for one host/session. */
|
|
227
|
+
export interface ResolvedAskUserConfig {
|
|
228
|
+
/** Only the knobs that were actually configured; unset knobs are absent. */
|
|
229
|
+
config: AskUserConfigOverrides;
|
|
230
|
+
/** True when a config file existed and was parsed. */
|
|
231
|
+
loaded: boolean;
|
|
232
|
+
/** The config file path, when one was read (whether or not it existed). */
|
|
233
|
+
path?: string;
|
|
234
|
+
/** Present when the file or programmatic options were invalid. Never thrown. */
|
|
235
|
+
error?: AskUserConfigError;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** An actionable configuration failure. */
|
|
239
|
+
export class AskUserConfigError extends Error {
|
|
240
|
+
readonly code = "invalid_config";
|
|
241
|
+
readonly source: string;
|
|
242
|
+
|
|
243
|
+
constructor(source: string, message: string) {
|
|
244
|
+
super(`${message} (${source})`);
|
|
245
|
+
this.name = "AskUserConfigError";
|
|
246
|
+
this.source = source;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
251
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function errorSource(source: string): string {
|
|
255
|
+
return source === "options" ? "programmatic options" : source;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function readMode(value: unknown, source: string): AskUserMode {
|
|
259
|
+
const where = errorSource(source);
|
|
260
|
+
if (value === "custom" || value === "native") return value;
|
|
261
|
+
throw new AskUserConfigError(where, `"mode" must be "custom" or "native", received ${JSON.stringify(value)}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readDisplayMode(value: unknown, source: string): AskUserDisplayMode {
|
|
265
|
+
const where = errorSource(source);
|
|
266
|
+
if (typeof value === "string" && (DISPLAY_MODES as readonly string[]).includes(value)) {
|
|
267
|
+
return value as AskUserDisplayMode;
|
|
268
|
+
}
|
|
269
|
+
throw new AskUserConfigError(where, `"displayMode" must be "overlay" or "inline", received ${JSON.stringify(value)}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readTimeout(value: unknown, source: string): number {
|
|
273
|
+
const where = errorSource(source);
|
|
274
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
275
|
+
throw new AskUserConfigError(where, `"timeoutPerQuestionMs" must be a non-negative finite number, received ${JSON.stringify(value)}`);
|
|
276
|
+
}
|
|
277
|
+
return Math.floor(value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Strictly validate an already-parsed config object (from the file or from
|
|
282
|
+
* programmatic options). Unknown keys, wrong types, and out-of-range values all
|
|
283
|
+
* throw {@link AskUserConfigError}. `mode` is accepted without a range check so
|
|
284
|
+
* an invalid programmatic mode can surface through route resolution as
|
|
285
|
+
* `invalid_config` with the list of runnable modes, exactly as before.
|
|
286
|
+
*/
|
|
287
|
+
export function parseAskUserConfig(raw: unknown, source: string): AskUserConfigOverrides {
|
|
288
|
+
if (!isRecord(raw)) {
|
|
289
|
+
throw new AskUserConfigError(errorSource(source), `the configuration must be a JSON object, received ${Array.isArray(raw) ? "an array" : typeof raw}`);
|
|
290
|
+
}
|
|
291
|
+
for (const key of Object.keys(raw)) {
|
|
292
|
+
if (!(CONFIG_KEYS as readonly string[]).includes(key)) {
|
|
293
|
+
throw new AskUserConfigError(errorSource(source), `unknown key ${JSON.stringify(key)}; allowed keys: ${CONFIG_KEYS.join(", ")}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const out: AskUserConfigOverrides = {};
|
|
297
|
+
if ("mode" in raw) out.mode = readMode(raw.mode, source);
|
|
298
|
+
if ("displayMode" in raw) out.displayMode = readDisplayMode(raw.displayMode, source);
|
|
299
|
+
if ("overlayToggleKey" in raw) out.overlayToggleKey = normalizeOverlayToggleKey(raw.overlayToggleKey, source);
|
|
300
|
+
if ("timeoutPerQuestionMs" in raw) out.timeoutPerQuestionMs = readTimeout(raw.timeoutPerQuestionMs, source);
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Validate programmatic options, leaving `mode` to route resolution. */
|
|
305
|
+
export function parseAskUserOptions(raw: AskUserConfigOverrides): AskUserConfigOverrides {
|
|
306
|
+
const out: AskUserConfigOverrides = {};
|
|
307
|
+
if (raw.mode !== undefined) out.mode = raw.mode;
|
|
308
|
+
if (raw.displayMode !== undefined) out.displayMode = readDisplayMode(raw.displayMode, "options");
|
|
309
|
+
if ("overlayToggleKey" in raw) out.overlayToggleKey = normalizeOverlayToggleKey(raw.overlayToggleKey, "options");
|
|
310
|
+
if (raw.timeoutPerQuestionMs !== undefined) out.timeoutPerQuestionMs = readTimeout(raw.timeoutPerQuestionMs, "options");
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Read and validate the config file. A missing file yields no overrides;
|
|
316
|
+
* unreadable, malformed, or invalid content throws {@link AskUserConfigError}.
|
|
317
|
+
* An empty/whitespace-only file is treated as "no configuration".
|
|
318
|
+
*/
|
|
319
|
+
export function readAskUserConfigFile(path: string): { overrides: AskUserConfigOverrides; loaded: boolean } {
|
|
320
|
+
let text: string;
|
|
321
|
+
try {
|
|
322
|
+
text = readFileSync(path, "utf8");
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
325
|
+
if (code === "ENOENT") return { overrides: {}, loaded: false };
|
|
326
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
327
|
+
throw new AskUserConfigError(path, `could not read the AskUser config file: ${message}`);
|
|
328
|
+
}
|
|
329
|
+
if (text.trim() === "") return { overrides: {}, loaded: false };
|
|
330
|
+
let parsed: unknown;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(text);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
335
|
+
throw new AskUserConfigError(path, `the file is not valid JSON: ${message}`);
|
|
336
|
+
}
|
|
337
|
+
return { overrides: parseAskUserConfig(parsed, path), loaded: true };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Resolve the effective user configuration for one host/session.
|
|
342
|
+
*
|
|
343
|
+
* Precedence per knob is programmatic option > config file. Invalid input is
|
|
344
|
+
* returned as `error` (never thrown) so the host can refuse every request with
|
|
345
|
+
* an actionable `invalid_config` result instead of falling back.
|
|
346
|
+
*/
|
|
347
|
+
export function resolveAskUserConfig(
|
|
348
|
+
programmatic: AskUserConfigOverrides = {},
|
|
349
|
+
options: { configFile?: string | false } = {},
|
|
350
|
+
): ResolvedAskUserConfig {
|
|
351
|
+
try {
|
|
352
|
+
const clean = parseAskUserOptions(programmatic);
|
|
353
|
+
let file: { overrides: AskUserConfigOverrides; loaded: boolean } = { overrides: {}, loaded: false };
|
|
354
|
+
const path = options.configFile === false ? undefined : (options.configFile ?? USER_CONFIG_PATH);
|
|
355
|
+
if (path !== undefined) file = readAskUserConfigFile(path);
|
|
356
|
+
|
|
357
|
+
const config: AskUserConfigOverrides = {};
|
|
358
|
+
const mode = clean.mode ?? file.overrides.mode;
|
|
359
|
+
if (mode !== undefined) config.mode = mode;
|
|
360
|
+
const displayMode = clean.displayMode ?? file.overrides.displayMode;
|
|
361
|
+
if (displayMode !== undefined) config.displayMode = displayMode;
|
|
362
|
+
const overlayToggleKey =
|
|
363
|
+
"overlayToggleKey" in clean ? clean.overlayToggleKey : file.overrides.overlayToggleKey;
|
|
364
|
+
if (overlayToggleKey !== undefined) config.overlayToggleKey = overlayToggleKey;
|
|
365
|
+
const timeoutPerQuestionMs = clean.timeoutPerQuestionMs ?? file.overrides.timeoutPerQuestionMs;
|
|
366
|
+
if (timeoutPerQuestionMs !== undefined) config.timeoutPerQuestionMs = timeoutPerQuestionMs;
|
|
367
|
+
|
|
368
|
+
const resolved: ResolvedAskUserConfig = { config, loaded: file.loaded };
|
|
369
|
+
if (path !== undefined) resolved.path = path;
|
|
370
|
+
return resolved;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (error instanceof AskUserConfigError) return { config: {}, loaded: false, error };
|
|
373
|
+
return {
|
|
374
|
+
config: {},
|
|
375
|
+
loaded: false,
|
|
376
|
+
error: new AskUserConfigError("options", error instanceof Error ? error.message : String(error)),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Project resolved overrides onto the per-host UI preferences. Unset knobs stay
|
|
383
|
+
* absent so the core can let the model's request fill the gap; `overlayToggleKey`
|
|
384
|
+
* is preserved as `null` when explicitly disabled.
|
|
385
|
+
*/
|
|
386
|
+
export function toUiPreferences(config: AskUserConfigOverrides): AskUserUiPreferences {
|
|
387
|
+
const preferences: AskUserUiPreferences = {};
|
|
388
|
+
if (config.displayMode !== undefined) preferences.displayMode = config.displayMode;
|
|
389
|
+
if (config.overlayToggleKey !== undefined) preferences.overlayToggleKey = config.overlayToggleKey;
|
|
390
|
+
if (config.timeoutPerQuestionMs !== undefined) preferences.timeoutPerQuestionMs = config.timeoutPerQuestionMs;
|
|
391
|
+
return preferences;
|
|
392
|
+
}
|