@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/ui/native.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { draftFromDefault, toAnswer } from "../answers.ts";
|
|
2
|
+
import { nativeInputHint, parseNativeAnswer } from "../parse.ts";
|
|
3
|
+
import type { AskUIInput, AskUIOutcome, AskUserAnswer, NativeDialogRunner, NormalizedQuestion, NormalizedRequest } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
/** Minimal shape of the Pi UI context needed by the native route. */
|
|
6
|
+
export interface NativeDialogUI {
|
|
7
|
+
input(
|
|
8
|
+
title: string,
|
|
9
|
+
placeholder?: string,
|
|
10
|
+
opts?: { signal?: AbortSignal; timeout?: number },
|
|
11
|
+
): Promise<string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function placeholderFor(question: NormalizedQuestion): string {
|
|
15
|
+
switch (question.kind) {
|
|
16
|
+
case "single":
|
|
17
|
+
return question.options.length > 0 ? 'e.g. 2 or "2 | note" or type free text' : "type free text";
|
|
18
|
+
case "multi":
|
|
19
|
+
return question.options.length > 0 ? 'e.g. 1,3 or "1,3 | note" or type free text' : "type free text";
|
|
20
|
+
case "input":
|
|
21
|
+
return "type free text";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildPrompt(
|
|
26
|
+
request: NormalizedRequest,
|
|
27
|
+
question: NormalizedQuestion,
|
|
28
|
+
invalid: string | undefined,
|
|
29
|
+
): string {
|
|
30
|
+
const total = request.questions.length;
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
if (request.header && request.header.trim() !== "") lines.push(request.header.trim());
|
|
33
|
+
lines.push(`[Question ${question.index + 1}/${total}] ${question.title}`);
|
|
34
|
+
if (question.prompt && question.prompt.trim() !== "") lines.push(question.prompt.trim());
|
|
35
|
+
question.options.forEach((option, index) => {
|
|
36
|
+
const description = option.description?.trim();
|
|
37
|
+
lines.push(` ${index + 1}. ${option.label}${description ? ` — ${description}` : ""}`);
|
|
38
|
+
});
|
|
39
|
+
if (question.hasDefault && question.default !== undefined) {
|
|
40
|
+
lines.push(`Default: ${question.default} (press Enter on an empty box to use it)`);
|
|
41
|
+
}
|
|
42
|
+
lines.push(nativeInputHint(question));
|
|
43
|
+
const body = lines.join("\n");
|
|
44
|
+
if (invalid) return `⚠ Invalid input: ${invalid}\n\n${body}`;
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The native route: exactly one Pi native input dialog per question. The option
|
|
50
|
+
* number and any trailing free text are typed into the same box. Invalid input
|
|
51
|
+
* re-prompts; the whole questionnaire shares one deadline and each dialog only
|
|
52
|
+
* receives the remaining time.
|
|
53
|
+
*/
|
|
54
|
+
export function createNativeRunner(ui: NativeDialogUI): NativeDialogRunner {
|
|
55
|
+
return {
|
|
56
|
+
async run({ request, deadline, signal, onUpdate }: AskUIInput): Promise<AskUIOutcome> {
|
|
57
|
+
const answers: AskUserAnswer[] = [];
|
|
58
|
+
const total = request.questions.length;
|
|
59
|
+
|
|
60
|
+
for (const question of request.questions) {
|
|
61
|
+
let invalid: string | undefined;
|
|
62
|
+
for (;;) {
|
|
63
|
+
if (deadline.expired()) return { kind: "timeout" };
|
|
64
|
+
onUpdate?.(`Waiting for the user to answer question ${question.index + 1}/${total}…`);
|
|
65
|
+
const remaining = deadline.remainingMs();
|
|
66
|
+
const opts: { signal?: AbortSignal; timeout?: number } = { timeout: remaining };
|
|
67
|
+
if (signal) opts.signal = signal;
|
|
68
|
+
|
|
69
|
+
let raw: string | undefined;
|
|
70
|
+
try {
|
|
71
|
+
raw = await ui.input(buildPrompt(request, question, invalid), placeholderFor(question), opts);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (raw === undefined) {
|
|
77
|
+
if (deadline.expired()) return { kind: "timeout" };
|
|
78
|
+
if (signal?.aborted) return { kind: "abort" };
|
|
79
|
+
return { kind: "cancelled" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parsed = parseNativeAnswer(question, raw);
|
|
83
|
+
if (!parsed.ok) {
|
|
84
|
+
invalid = parsed.error;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (parsed.value.empty) {
|
|
88
|
+
const fallback = draftFromDefault(question);
|
|
89
|
+
if (fallback) {
|
|
90
|
+
answers.push(toAnswer(question, fallback));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
invalid =
|
|
94
|
+
question.kind === "input"
|
|
95
|
+
? "This question is required and cannot be empty."
|
|
96
|
+
: "This question is required; enter an option number or free text.";
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
answers.push(
|
|
100
|
+
toAnswer(question, {
|
|
101
|
+
selections: parsed.value.selections,
|
|
102
|
+
freeText: parsed.value.freeText ?? "",
|
|
103
|
+
usedDefault: false,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { kind: "submitted", answers };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|