@henryqw/pi-ask-question 0.1.0 → 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.
@@ -17,6 +17,7 @@ interface QuestionOption {
17
17
 
18
18
  type DisplayOption = QuestionOption & { isOther?: boolean };
19
19
 
20
+ const CUSTOM_OPTION_LABEL = "Something else.";
20
21
  const NUMBER_KEYS = ["1", "2", "3", "4"] as const;
21
22
 
22
23
  interface QuestionDetails {
@@ -25,6 +26,7 @@ interface QuestionDetails {
25
26
  answer: string | null;
26
27
  wasCustom?: boolean;
27
28
  selectedIndex?: number;
29
+ error?: string;
28
30
  }
29
31
 
30
32
  const QuestionOptionSchema = Type.Object({
@@ -45,10 +47,10 @@ export default function askQuestionExtension(pi: ExtensionAPI): void {
45
47
  pi.registerTool({
46
48
  name: "ask_question",
47
49
  label: "Ask Question",
48
- description: "Ask user one question with up to three options or a custom answer. First option is shown as recommended. Use when user input is needed to proceed.",
49
- promptSnippet: "Ask user one interactive question with up to three options or a custom answer",
50
+ description: "In interactive TUI sessions, ask user one question with up to three options or a custom answer. First option is shown as recommended.",
51
+ promptSnippet: "In interactive TUI sessions, ask user one question with up to three options or a custom answer",
50
52
  promptGuidelines: [
51
- "Use ask_question instead of plain assistant text whenever user input is needed to proceed.",
53
+ "In interactive TUI sessions, use ask_question instead of plain assistant text whenever user input is needed to proceed; in non-interactive sessions, ask in plain assistant text.",
52
54
  "Give ask_question one to three concise, meaningful options without inventing filler, put recommended option first, and omit '(Recommended)' from its label.",
53
55
  "Give ask_question option descriptions only when they explain meaningful tradeoffs; never repeat option labels.",
54
56
  ],
@@ -63,9 +65,10 @@ export default function askQuestionExtension(pi: ExtensionAPI): void {
63
65
  }));
64
66
  const options = suppliedOptions.map((option) => option.label);
65
67
  if (ctx.mode !== "tui") {
68
+ const error = "UI not available (running in non-interactive mode)";
66
69
  return {
67
- content: [{ type: "text" as const, text: "Error: UI not available (running in non-interactive mode)" }],
68
- details: { question, options, answer: null } satisfies QuestionDetails,
70
+ content: [{ type: "text" as const, text: `Error: ${error}` }],
71
+ details: { question, options, answer: null, error } satisfies QuestionDetails,
69
72
  };
70
73
  }
71
74
  let validationError: string | undefined;
@@ -73,14 +76,15 @@ export default function askQuestionExtension(pi: ExtensionAPI): void {
73
76
  else if (!question) validationError = "Question must not be blank";
74
77
  else if (suppliedOptions.some((option) => !option.label)) validationError = "Option labels must not be blank";
75
78
  else if (new Set(options.map((option) => option.toLowerCase())).size !== options.length) validationError = "Option labels must be unique";
79
+ else if (options.some((option) => option.toLowerCase() === CUSTOM_OPTION_LABEL.toLowerCase())) validationError = `Option label "${CUSTOM_OPTION_LABEL}" is reserved`;
76
80
  if (validationError) {
77
81
  return {
78
82
  content: [{ type: "text" as const, text: `Error: ${validationError}` }],
79
- details: { question, options, answer: null } satisfies QuestionDetails,
83
+ details: { question, options, answer: null, error: validationError } satisfies QuestionDetails,
80
84
  };
81
85
  }
82
86
 
83
- const allOptions: DisplayOption[] = [...suppliedOptions, { label: "Something else.", isOther: true }];
87
+ const allOptions: DisplayOption[] = [...suppliedOptions, { label: CUSTOM_OPTION_LABEL, isOther: true }];
84
88
  const result = await ctx.ui.custom<{ answer: string; wasCustom: boolean; index?: number } | null>(
85
89
  (tui, theme, _kb, done) => {
86
90
  let optionIndex = 0;
@@ -226,7 +230,7 @@ export default function askQuestionExtension(pi: ExtensionAPI): void {
226
230
  const options = Array.isArray(args.options) ? args.options : [];
227
231
  if (options.length) {
228
232
  const labels = options.map((option: QuestionOption) => option.label);
229
- const numbered = [...labels, "Something else."].map((option, index) => `${index + 1}. ${option}${index === 0 ? " (Recommended)" : ""}`);
233
+ const numbered = [...labels, CUSTOM_OPTION_LABEL].map((option, index) => `${index + 1}. ${option}${index === 0 ? " (Recommended)" : ""}`);
230
234
  text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
231
235
  }
232
236
  return new Text(text, 0, 0);
@@ -238,6 +242,7 @@ export default function askQuestionExtension(pi: ExtensionAPI): void {
238
242
  const content = result.content[0];
239
243
  return new Text(content?.type === "text" ? content.text : "", 0, 0);
240
244
  }
245
+ if (details.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
241
246
  if (details.answer === null) return new Text(theme.fg("warning", "Cancelled"), 0, 0);
242
247
  if (details.wasCustom) {
243
248
  return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "(wrote) ") + theme.fg("accent", details.answer), 0, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-ask-question",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Ask Pi users one interactive question with choices or a custom answer.",
5
5
  "keywords": [
6
6
  "pi-package",