@ryan_nookpi/pi-extension-ask-user-question 0.3.3 → 0.3.4
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/form-ui.test.ts +44 -0
- package/form-ui.ts +94 -1
- package/index.ts +2 -1
- package/package.json +1 -1
package/form-ui.test.ts
CHANGED
|
@@ -22,6 +22,50 @@ describe("ask-user-question/form-ui", () => {
|
|
|
22
22
|
expect(editorTheme.selectList.noMatch("e")).toBe("e");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
it("prefers the native askUserQuestion bridge when available", async () => {
|
|
26
|
+
const questions = normalizeQuestions([
|
|
27
|
+
{ id: "choice", type: "radio", prompt: "Pick", options: [{ value: "a", label: "A" }] },
|
|
28
|
+
{ id: "items", type: "checkbox", prompt: "Items", options: [{ value: "x", label: "X" }] },
|
|
29
|
+
{ id: "note", type: "text", prompt: "Note" },
|
|
30
|
+
]);
|
|
31
|
+
const askUserQuestion = vi.fn(async () => ({ choice: "custom", items: ["x", "extra"], note: "ship it" }));
|
|
32
|
+
const custom = vi.fn();
|
|
33
|
+
const ctx = {
|
|
34
|
+
hasUI: true,
|
|
35
|
+
ui: { askUserQuestion, custom },
|
|
36
|
+
} as unknown as ExtensionContext;
|
|
37
|
+
|
|
38
|
+
const result = await runAskUserQuestionForm(ctx, { title: "Title", description: "Desc" }, questions);
|
|
39
|
+
|
|
40
|
+
expect(askUserQuestion).toHaveBeenCalledWith({ title: "Title", description: "Desc", questions });
|
|
41
|
+
expect(custom).not.toHaveBeenCalled();
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
title: "Title",
|
|
44
|
+
questions,
|
|
45
|
+
answers: [
|
|
46
|
+
{ id: "choice", type: "radio", value: "custom", wasCustom: true },
|
|
47
|
+
{ id: "items", type: "checkbox", value: ["x", "extra"], wasCustom: true },
|
|
48
|
+
{ id: "note", type: "text", value: "ship it", wasCustom: true },
|
|
49
|
+
],
|
|
50
|
+
cancelled: false,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("supports the snake_case bridge and maps undefined to cancellation", async () => {
|
|
55
|
+
const questions = normalizeQuestions([{ id: "text", type: "text", prompt: "Explain" }]);
|
|
56
|
+
const signal = new AbortController().signal;
|
|
57
|
+
const askUserQuestion = vi.fn(async () => undefined);
|
|
58
|
+
const ctx = {
|
|
59
|
+
hasUI: true,
|
|
60
|
+
ui: { ask_user_question: askUserQuestion },
|
|
61
|
+
} as unknown as ExtensionContext;
|
|
62
|
+
|
|
63
|
+
const result = await runAskUserQuestionForm(ctx, { title: "Title" }, questions, { signal });
|
|
64
|
+
|
|
65
|
+
expect(askUserQuestion).toHaveBeenCalledWith({ title: "Title", description: undefined, questions }, { signal });
|
|
66
|
+
expect(result).toEqual({ title: "Title", questions, answers: [], cancelled: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
25
69
|
it("wires the custom UI factory to the controller", async () => {
|
|
26
70
|
const questions = normalizeQuestions([{ id: "text", type: "text", prompt: "Explain", default: "seed" }]);
|
|
27
71
|
let captured: FormResult | undefined;
|
package/form-ui.ts
CHANGED
|
@@ -3,7 +3,90 @@ import { Editor, type EditorTheme } from "@mariozechner/pi-tui";
|
|
|
3
3
|
|
|
4
4
|
import { createFormController } from "./controller.ts";
|
|
5
5
|
import { createAnswerState } from "./state.ts";
|
|
6
|
-
import type { FormResult, NormalizedQuestion, RenderTheme } from "./types.ts";
|
|
6
|
+
import type { Answer, FormResult, NormalizedQuestion, RenderTheme } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
interface AskUserQuestionBridgeRequest {
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
questions: NormalizedQuestion[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type AskUserQuestionBridge = (
|
|
15
|
+
request: AskUserQuestionBridgeRequest,
|
|
16
|
+
options?: { signal?: AbortSignal },
|
|
17
|
+
) => Promise<Record<string, unknown> | undefined>;
|
|
18
|
+
|
|
19
|
+
interface AskUserQuestionBridgeUI {
|
|
20
|
+
askUserQuestion?: AskUserQuestionBridge;
|
|
21
|
+
ask_user_question?: AskUserQuestionBridge;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getAskUserQuestionBridge(ctx: ExtensionContext): AskUserQuestionBridge | undefined {
|
|
25
|
+
const ui = ctx.ui as ExtensionContext["ui"] & AskUserQuestionBridgeUI;
|
|
26
|
+
if (typeof ui.askUserQuestion === "function") return ui.askUserQuestion.bind(ui);
|
|
27
|
+
if (typeof ui.ask_user_question === "function") return ui.ask_user_question.bind(ui);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function valueAsString(value: unknown): string {
|
|
32
|
+
return typeof value === "string" ? value : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function valueAsStringArray(value: unknown): string[] {
|
|
36
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string");
|
|
37
|
+
return typeof value === "string" ? [value] : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildAnswersFromBridgeResult(questions: NormalizedQuestion[], result: Record<string, unknown>): Answer[] {
|
|
41
|
+
return questions.map((question) => {
|
|
42
|
+
const rawValue = result[question.id];
|
|
43
|
+
if (question.type === "radio") {
|
|
44
|
+
const value = valueAsString(rawValue);
|
|
45
|
+
const optionValues = new Set(question.options.map((option) => option.value));
|
|
46
|
+
return {
|
|
47
|
+
id: question.id,
|
|
48
|
+
type: "radio",
|
|
49
|
+
value,
|
|
50
|
+
wasCustom: value.length > 0 && !optionValues.has(value),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (question.type === "checkbox") {
|
|
55
|
+
const value = valueAsStringArray(rawValue);
|
|
56
|
+
const optionValues = new Set(question.options.map((option) => option.value));
|
|
57
|
+
return {
|
|
58
|
+
id: question.id,
|
|
59
|
+
type: "checkbox",
|
|
60
|
+
value,
|
|
61
|
+
wasCustom: value.some((item) => !optionValues.has(item)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
id: question.id,
|
|
67
|
+
type: "text",
|
|
68
|
+
value: valueAsString(rawValue),
|
|
69
|
+
wasCustom: true,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildFormResultFromBridgeResult(
|
|
75
|
+
params: { title?: string },
|
|
76
|
+
questions: NormalizedQuestion[],
|
|
77
|
+
result: Record<string, unknown> | undefined,
|
|
78
|
+
): FormResult {
|
|
79
|
+
if (!result) {
|
|
80
|
+
return { title: params.title, questions, answers: [], cancelled: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
title: params.title,
|
|
85
|
+
questions,
|
|
86
|
+
answers: buildAnswersFromBridgeResult(questions, result),
|
|
87
|
+
cancelled: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
7
90
|
|
|
8
91
|
export function createEditorTheme(theme: Pick<RenderTheme, "fg">): EditorTheme {
|
|
9
92
|
return {
|
|
@@ -22,7 +105,17 @@ export async function runAskUserQuestionForm(
|
|
|
22
105
|
ctx: ExtensionContext,
|
|
23
106
|
params: { title?: string; description?: string },
|
|
24
107
|
questions: NormalizedQuestion[],
|
|
108
|
+
options: { signal?: AbortSignal } = {},
|
|
25
109
|
): Promise<FormResult> {
|
|
110
|
+
const askUserQuestionBridge = getAskUserQuestionBridge(ctx);
|
|
111
|
+
if (askUserQuestionBridge) {
|
|
112
|
+
const request = { title: params.title, description: params.description, questions };
|
|
113
|
+
const result = options.signal
|
|
114
|
+
? await askUserQuestionBridge(request, { signal: options.signal })
|
|
115
|
+
: await askUserQuestionBridge(request);
|
|
116
|
+
return buildFormResultFromBridgeResult(params, questions, result);
|
|
117
|
+
}
|
|
118
|
+
|
|
26
119
|
return ctx.ui.custom<FormResult>((tui, theme, _keybindings, done) => {
|
|
27
120
|
return createFormController({
|
|
28
121
|
title: params.title,
|
package/index.ts
CHANGED
|
@@ -62,7 +62,7 @@ export default function askUserQuestion(pi: ExtensionAPI) {
|
|
|
62
62
|
promptSnippet: "radio, checkbox, text 입력을 사용하는 인터랙티브 질문 폼 열기",
|
|
63
63
|
promptGuidelines: PROMPT_GUIDELINES,
|
|
64
64
|
parameters: AskUserQuestionParams,
|
|
65
|
-
async execute(_toolCallId, rawParams,
|
|
65
|
+
async execute(_toolCallId, rawParams, signal, _onUpdate, ctx) {
|
|
66
66
|
if (!ctx.hasUI) {
|
|
67
67
|
return errorResult("오류: UI를 사용할 수 없습니다. 현재 비대화형 모드에서 실행 중입니다.");
|
|
68
68
|
}
|
|
@@ -77,6 +77,7 @@ export default function askUserQuestion(pi: ExtensionAPI) {
|
|
|
77
77
|
ctx,
|
|
78
78
|
{ title: params.title, description: params.description },
|
|
79
79
|
questions,
|
|
80
|
+
{ signal },
|
|
80
81
|
);
|
|
81
82
|
return result.cancelled ? buildCancelledResponse(result) : buildSuccessResponse(result);
|
|
82
83
|
},
|