mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
fix(ask): always allow custom select answers
This commit is contained in:
@@ -6,7 +6,7 @@ A locally maintained Pi extension that consolidates the upstream `question.ts` a
|
|||||||
|
|
||||||
The extension registers `ask_user_question`. The model can present one to eight questions in one sequential TUI interaction:
|
The extension registers `ask_user_question`. The model can present one to eight questions in one sequential TUI interaction:
|
||||||
|
|
||||||
- `select`: one to eight stable `{ value, label, description? }` choices, with an optional free-form choice;
|
- `select`: one to eight stable `{ value, label, description? }` choices, always followed by a free-form choice that the model cannot disable;
|
||||||
- `text`: a free-form Editor answer;
|
- `text`: a free-form Editor answer;
|
||||||
- `required: false`: lets the user explicitly skip the question;
|
- `required: false`: lets the user explicitly skip the question;
|
||||||
- multiple questions: progress tabs plus a final review page.
|
- multiple questions: progress tabs plus a final review page.
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
|
|
||||||
Initial reference snapshot: `dcd461925db2edf69a43c8135db1180d418afd54` (`main`, inspected 2026-08-24).
|
Initial reference snapshot: `dcd461925db2edf69a43c8135db1180d418afd54` (`main`, inspected 2026-08-24).
|
||||||
|
|
||||||
The upstream `qna.ts` command is intentionally not included: this package only supports model-initiated questions. Local changes consolidate single and multi-question flows into one tool, add text questions, validation, bounded schemas, cancellation/abort handling, width-aware rendering, and IME focus propagation.
|
The upstream `qna.ts` command is intentionally not included: this package only supports model-initiated questions. Local changes consolidate single and multi-question flows into one tool, add text questions, keep a user-controlled free-form escape hatch on every select question, validation, bounded schemas, cancellation/abort handling, width-aware rendering, and IME focus propagation.
|
||||||
|
|
||||||
The copied MIT license and original copyright notice are retained in `LICENSE`.
|
The copied MIT license and original copyright notice are retained in `LICENSE`.
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ export function normalizeQuestions(input: readonly QuestionInput[]): NormalizedQ
|
|||||||
throw new Error(`questions[${index}].type must be select or text`);
|
throw new Error(`questions[${index}].type must be select or text`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (question.allowCustom !== undefined && typeof question.allowCustom !== "boolean") {
|
|
||||||
throw new Error(`questions[${index}].allowCustom must be a boolean`);
|
|
||||||
}
|
|
||||||
if (question.required !== undefined && typeof question.required !== "boolean") {
|
if (question.required !== undefined && typeof question.required !== "boolean") {
|
||||||
throw new Error(`questions[${index}].required must be a boolean`);
|
throw new Error(`questions[${index}].required must be a boolean`);
|
||||||
}
|
}
|
||||||
@@ -77,7 +74,6 @@ export function normalizeQuestions(input: readonly QuestionInput[]): NormalizedQ
|
|||||||
prompt,
|
prompt,
|
||||||
type: question.type,
|
type: question.type,
|
||||||
options,
|
options,
|
||||||
allowCustom: question.type === "select" && question.allowCustom !== false,
|
|
||||||
required: question.required !== false,
|
required: question.required !== false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ const QuestionSchema = Type.Object({
|
|||||||
type: Type.String({ enum: ["select", "text"], description: "select for choices; text for a free-form answer" }),
|
type: Type.String({ enum: ["select", "text"], description: "select for choices; text for a free-form answer" }),
|
||||||
options: Type.Optional(Type.Array(OptionSchema, {
|
options: Type.Optional(Type.Array(OptionSchema, {
|
||||||
maxItems: MAX_OPTIONS,
|
maxItems: MAX_OPTIONS,
|
||||||
description: "Required and non-empty for select questions; omit for text questions",
|
description: "Required and non-empty for select questions; omit for text questions. The tool always appends a free-form answer choice.",
|
||||||
})),
|
})),
|
||||||
allowCustom: Type.Optional(Type.Boolean({ description: "For select questions, append a free-form answer choice (default: true)" })),
|
|
||||||
required: Type.Optional(Type.Boolean({ description: "Whether the question must be answered (default: true); optional questions can be skipped" })),
|
required: Type.Optional(Type.Boolean({ description: "Whether the question must be answered (default: true); optional questions can be skipped" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export interface QuestionInput {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
type: QuestionType;
|
type: QuestionType;
|
||||||
options?: QuestionOptionInput[];
|
options?: QuestionOptionInput[];
|
||||||
allowCustom?: boolean;
|
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ export interface NormalizedQuestion {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
type: QuestionType;
|
type: QuestionType;
|
||||||
options: QuestionOptionInput[];
|
options: QuestionOptionInput[];
|
||||||
allowCustom: boolean;
|
|
||||||
required: boolean;
|
required: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class AskUserView implements Component, Focusable {
|
|||||||
const question = this.currentQuestion();
|
const question = this.currentQuestion();
|
||||||
if (!question || question.type !== "select") return [];
|
if (!question || question.type !== "select") return [];
|
||||||
const options: RenderOption[] = question.options.map((option) => ({ ...option, kind: "option" }));
|
const options: RenderOption[] = question.options.map((option) => ({ ...option, kind: "option" }));
|
||||||
if (question.allowCustom) options.push({ value: "", label: "Type something.", kind: "custom" });
|
options.push({ value: "", label: "Type something.", kind: "custom" });
|
||||||
if (!question.required) options.push({ value: "", label: "Skip this question.", kind: "skip" });
|
if (!question.required) options.push({ value: "", label: "Skip this question.", kind: "skip" });
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import { formatAnswers, normalizeQuestions, orderedAnswers } from "../src/normalize.ts";
|
import { formatAnswers, normalizeQuestions, orderedAnswers } from "../src/normalize.ts";
|
||||||
import type { UserAnswer } from "../src/types.ts";
|
import type { QuestionInput, UserAnswer } from "../src/types.ts";
|
||||||
|
|
||||||
test("normalizes select and text questions with bounded defaults", () => {
|
test("normalizes select and text questions with bounded defaults", () => {
|
||||||
const questions = normalizeQuestions([
|
const questions = normalizeQuestions([
|
||||||
@@ -27,14 +27,26 @@ test("normalizes select and text questions with bounded defaults", () => {
|
|||||||
prompt: "Choose a language",
|
prompt: "Choose a language",
|
||||||
type: "select",
|
type: "select",
|
||||||
options: [{ value: "ts", label: "TypeScript" }],
|
options: [{ value: "ts", label: "TypeScript" }],
|
||||||
allowCustom: true,
|
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
assert.equal(questions[1]?.label, "Q2");
|
assert.equal(questions[1]?.label, "Q2");
|
||||||
assert.equal(questions[1]?.allowCustom, false);
|
|
||||||
assert.equal(questions[1]?.required, false);
|
assert.equal(questions[1]?.required, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ignores the removed allowCustom input from legacy callers", () => {
|
||||||
|
const legacyQuestion = {
|
||||||
|
id: "language",
|
||||||
|
prompt: "Choose a language",
|
||||||
|
type: "select",
|
||||||
|
options: [{ value: "ts", label: "TypeScript" }],
|
||||||
|
allowCustom: false,
|
||||||
|
} satisfies QuestionInput & { allowCustom: boolean };
|
||||||
|
|
||||||
|
const [question] = normalizeQuestions([legacyQuestion]);
|
||||||
|
assert.ok(question);
|
||||||
|
assert.equal("allowCustom" in question, false);
|
||||||
|
});
|
||||||
|
|
||||||
test("rejects invalid ids, duplicate ids, invalid option combinations, and duplicate values", () => {
|
test("rejects invalid ids, duplicate ids, invalid option combinations, and duplicate values", () => {
|
||||||
assert.throws(() => normalizeQuestions([{ id: "bad id", prompt: "Bad", type: "text" }]), /must start with a letter/);
|
assert.throws(() => normalizeQuestions([{ id: "bad id", prompt: "Bad", type: "text" }]), /must start with a letter/);
|
||||||
assert.throws(() => normalizeQuestions([null as never]), /must be an object/);
|
assert.throws(() => normalizeQuestions([null as never]), /must be an object/);
|
||||||
|
|||||||
Reference in New Issue
Block a user