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:
|
||||
|
||||
- `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;
|
||||
- `required: false`: lets the user explicitly skip the question;
|
||||
- multiple questions: progress tabs plus a final review page.
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
|
||||
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`.
|
||||
|
||||
@@ -45,9 +45,6 @@ export function normalizeQuestions(input: readonly QuestionInput[]): NormalizedQ
|
||||
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") {
|
||||
throw new Error(`questions[${index}].required must be a boolean`);
|
||||
}
|
||||
@@ -77,7 +74,6 @@ export function normalizeQuestions(input: readonly QuestionInput[]): NormalizedQ
|
||||
prompt,
|
||||
type: question.type,
|
||||
options,
|
||||
allowCustom: question.type === "select" && question.allowCustom !== 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" }),
|
||||
options: Type.Optional(Type.Array(OptionSchema, {
|
||||
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" })),
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface QuestionInput {
|
||||
prompt: string;
|
||||
type: QuestionType;
|
||||
options?: QuestionOptionInput[];
|
||||
allowCustom?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
@@ -22,7 +21,6 @@ export interface NormalizedQuestion {
|
||||
prompt: string;
|
||||
type: QuestionType;
|
||||
options: QuestionOptionInput[];
|
||||
allowCustom: boolean;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export class AskUserView implements Component, Focusable {
|
||||
const question = this.currentQuestion();
|
||||
if (!question || question.type !== "select") return [];
|
||||
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" });
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
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", () => {
|
||||
const questions = normalizeQuestions([
|
||||
@@ -27,14 +27,26 @@ test("normalizes select and text questions with bounded defaults", () => {
|
||||
prompt: "Choose a language",
|
||||
type: "select",
|
||||
options: [{ value: "ts", label: "TypeScript" }],
|
||||
allowCustom: true,
|
||||
required: true,
|
||||
});
|
||||
assert.equal(questions[1]?.label, "Q2");
|
||||
assert.equal(questions[1]?.allowCustom, 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", () => {
|
||||
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/);
|
||||
|
||||
Reference in New Issue
Block a user