@janvitos/pi-plan-build 0.1.28 → 0.1.30
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/package.json +2 -2
- package/question-ui.ts +36 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@janvitos/pi-plan-build",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "Plan safely, approve explicitly, then implement here or in a clean session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"test": "node --experimental-strip-types --test utils.test.ts",
|
|
32
|
+
"test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts",
|
|
33
33
|
"prepublishOnly": "npm test"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
package/question-ui.ts
CHANGED
|
@@ -22,6 +22,16 @@ export const QuestionParameters = Type.Object({
|
|
|
22
22
|
|
|
23
23
|
export type QuestionAnswer = QuestionAnswerData;
|
|
24
24
|
|
|
25
|
+
const QUESTION_NOTICE_ENTRY_TYPE = "pi-plan-build-question-notice";
|
|
26
|
+
const QUESTION_CANCELLED_MESSAGE = "You chose not to answer the question(s). Awaiting your instructions.";
|
|
27
|
+
|
|
28
|
+
class QuestionCancelledError extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super("User cancelled the question");
|
|
31
|
+
this.name = "QuestionCancelledError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
async function askOne(
|
|
26
36
|
ctx: any,
|
|
27
37
|
prompt: {
|
|
@@ -31,6 +41,7 @@ async function askOne(
|
|
|
31
41
|
multiple?: boolean;
|
|
32
42
|
custom?: boolean;
|
|
33
43
|
},
|
|
44
|
+
signal: AbortSignal | undefined,
|
|
34
45
|
): Promise<QuestionAnswer> {
|
|
35
46
|
const allowCustom = prompt.custom !== false;
|
|
36
47
|
const labels = prompt.options.map((option) =>
|
|
@@ -41,11 +52,11 @@ async function askOne(
|
|
|
41
52
|
|
|
42
53
|
if (!prompt.multiple) {
|
|
43
54
|
const choices = [...labels, ...(allowCustom ? ["Type your own answer"] : [])];
|
|
44
|
-
const choice = await ctx.ui.select(`${prompt.header}: ${prompt.question}`, choices);
|
|
45
|
-
if (!choice) throw new
|
|
55
|
+
const choice = await ctx.ui.select(`${prompt.header}: ${prompt.question}`, choices, { signal });
|
|
56
|
+
if (!choice) throw new QuestionCancelledError();
|
|
46
57
|
if (allowCustom && choice === "Type your own answer") {
|
|
47
|
-
const custom = await ctx.ui.input(prompt.header, prompt.question);
|
|
48
|
-
if (!custom?.trim()) throw new
|
|
58
|
+
const custom = await ctx.ui.input(prompt.header, prompt.question, { signal });
|
|
59
|
+
if (!custom?.trim()) throw new QuestionCancelledError();
|
|
49
60
|
selected.push(custom.trim());
|
|
50
61
|
usedCustom = true;
|
|
51
62
|
} else {
|
|
@@ -59,11 +70,11 @@ async function askOne(
|
|
|
59
70
|
...(allowCustom ? ["Add a custom answer"] : []),
|
|
60
71
|
...(selected.length ? [`Done (${selected.join(", ")})`] : []),
|
|
61
72
|
];
|
|
62
|
-
const choice = await ctx.ui.select(`${prompt.header}: ${prompt.question}`, choices);
|
|
63
|
-
if (!choice) throw new
|
|
73
|
+
const choice = await ctx.ui.select(`${prompt.header}: ${prompt.question}`, choices, { signal });
|
|
74
|
+
if (!choice) throw new QuestionCancelledError();
|
|
64
75
|
if (choice.startsWith("Done (")) break;
|
|
65
76
|
if (choice === "Add a custom answer") {
|
|
66
|
-
const custom = await ctx.ui.input(prompt.header, prompt.question);
|
|
77
|
+
const custom = await ctx.ui.input(prompt.header, prompt.question, { signal });
|
|
67
78
|
if (custom?.trim()) {
|
|
68
79
|
selected.push(custom.trim());
|
|
69
80
|
usedCustom = true;
|
|
@@ -81,17 +92,31 @@ async function askOne(
|
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
95
|
+
pi.registerEntryRenderer<{ message: string }>(QUESTION_NOTICE_ENTRY_TYPE, (entry, _options, theme) => {
|
|
96
|
+
const message = typeof entry.data?.message === "string" ? entry.data.message : QUESTION_CANCELLED_MESSAGE;
|
|
97
|
+
return new Text(theme.fg("warning", message), 0, 0);
|
|
98
|
+
});
|
|
84
99
|
pi.registerTool({
|
|
85
100
|
name: "question",
|
|
86
101
|
label: "Question",
|
|
87
102
|
description: `Use this tool when you need to ask the user questions during execution. This allows you to gather preferences, clarify ambiguous instructions, get implementation decisions, or offer choices. When custom is enabled (default), do not add an Other option yourself. Put the recommended option first and suffix its label with "(Recommended)".`,
|
|
88
103
|
parameters: QuestionParameters,
|
|
89
104
|
executionMode: "sequential",
|
|
90
|
-
async execute(_toolCallId, params,
|
|
105
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
91
106
|
if (!ctx.hasUI) throw new Error("The question tool requires an interactive TUI or RPC client");
|
|
92
107
|
if (params.questions.length === 0) throw new Error("At least one question is required");
|
|
93
108
|
const answers: QuestionAnswer[] = [];
|
|
94
|
-
|
|
109
|
+
try {
|
|
110
|
+
for (const prompt of params.questions) answers.push(await askOne(ctx, prompt, signal));
|
|
111
|
+
} catch (error: unknown) {
|
|
112
|
+
if (!(error instanceof QuestionCancelledError)) throw error;
|
|
113
|
+
pi.appendEntry(QUESTION_NOTICE_ENTRY_TYPE, { message: QUESTION_CANCELLED_MESSAGE });
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: QUESTION_CANCELLED_MESSAGE }],
|
|
116
|
+
details: { cancelled: true },
|
|
117
|
+
terminate: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
95
120
|
const formatted = formatQuestionAnswers(answers);
|
|
96
121
|
return {
|
|
97
122
|
content: [{ type: "text", text: `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.` }],
|
|
@@ -103,7 +128,8 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
103
128
|
return new Text(theme.fg("toolTitle", theme.bold(`question (${count})`)), 0, 0);
|
|
104
129
|
},
|
|
105
130
|
renderResult(result, _options, theme) {
|
|
106
|
-
const details = result.details as { answers?: QuestionAnswer[] } | undefined;
|
|
131
|
+
const details = result.details as { answers?: QuestionAnswer[]; cancelled?: boolean } | undefined;
|
|
132
|
+
if (details?.cancelled) return new Text(theme.fg("warning", "Question(s) skipped"), 0, 0);
|
|
107
133
|
if (!details?.answers) return new Text(theme.fg("warning", "Question cancelled"), 0, 0);
|
|
108
134
|
return new Text(details.answers.map((a) => `${theme.fg("success", "✓")} ${a.header}: ${a.answers.join(", ")}`).join("\n"), 0, 0);
|
|
109
135
|
},
|