@ryan_nookpi/pi-extension-ask-user-question 0.1.0 → 0.2.0

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.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { Editor, type EditorTheme } from "@mariozechner/pi-tui";
3
+
4
+ import { createFormController } from "./controller.ts";
5
+ import { createAnswerState } from "./state.ts";
6
+ import type { FormResult, NormalizedQuestion, RenderTheme } from "./types.ts";
7
+
8
+ export function createEditorTheme(theme: Pick<RenderTheme, "fg">): EditorTheme {
9
+ return {
10
+ borderColor: (value) => theme.fg("accent", value),
11
+ selectList: {
12
+ selectedPrefix: (value) => theme.fg("accent", value),
13
+ selectedText: (value) => theme.fg("accent", value),
14
+ description: (value) => theme.fg("muted", value),
15
+ scrollInfo: (value) => theme.fg("dim", value),
16
+ noMatch: (value) => theme.fg("warning", value),
17
+ },
18
+ };
19
+ }
20
+
21
+ export async function runAskUserQuestionForm(
22
+ ctx: ExtensionContext,
23
+ params: { title?: string; description?: string },
24
+ questions: NormalizedQuestion[],
25
+ ): Promise<FormResult> {
26
+ return ctx.ui.custom<FormResult>((tui, theme, _keybindings, done) => {
27
+ return createFormController({
28
+ title: params.title,
29
+ description: params.description,
30
+ questions,
31
+ answerState: createAnswerState(questions),
32
+ editor: new Editor(tui, createEditorTheme(theme)),
33
+ theme,
34
+ requestRender: () => tui.requestRender(),
35
+ done,
36
+ });
37
+ });
38
+ }
package/index.test.ts ADDED
@@ -0,0 +1,113 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { createExtensionApiMock } from "../../tests/mock-extension-api.ts";
5
+ import askUserQuestionExtension, {
6
+ AskUserQuestionParams,
7
+ buildRenderCallText,
8
+ buildRenderResultText,
9
+ errorResult,
10
+ normalizeQuestions,
11
+ } from "./index.ts";
12
+
13
+ const theme = {
14
+ fg: (_color: string, text: string) => text,
15
+ bg: (_color: string, text: string) => `[${text}]`,
16
+ bold: (text: string) => text,
17
+ } as const;
18
+
19
+ describe("ask-user-question extension", () => {
20
+ it("exports helpers", () => {
21
+ expect(AskUserQuestionParams).toBeTruthy();
22
+ expect(errorResult("boom").content[0].text).toBe("boom");
23
+ expect(normalizeQuestions([{ id: "x", type: "text", prompt: "Q" }])[0].label).toBe("Q1");
24
+ expect(buildRenderCallText({ questions: [{ id: "x", type: "text", prompt: "Q" }] }, theme)).toContain("1 question");
25
+ expect(buildRenderResultText({ content: [{ type: "text", text: "plain" }] }, theme)).toBe("plain");
26
+ });
27
+
28
+ it("returns errors when UI is unavailable or no questions are provided", async () => {
29
+ const apiMock = createExtensionApiMock();
30
+ askUserQuestionExtension(apiMock.api);
31
+ const tool = apiMock.getTool("ask_user_question");
32
+ if (!tool.execute) throw new Error("execute is missing");
33
+
34
+ const noUi = await tool.execute(
35
+ "call-1",
36
+ { title: "Title", questions: [{ id: "x", type: "text", prompt: "Q" }] },
37
+ undefined,
38
+ undefined,
39
+ { hasUI: false } as unknown as ExtensionContext,
40
+ );
41
+ const noQuestions = await tool.execute("call-2", { title: "Title", questions: [] }, undefined, undefined, {
42
+ hasUI: true,
43
+ ui: { custom: vi.fn() },
44
+ } as unknown as ExtensionContext);
45
+
46
+ expect(noUi).toMatchObject({
47
+ content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
48
+ });
49
+ expect(noQuestions).toMatchObject({ content: [{ type: "text", text: "Error: No questions provided" }] });
50
+ });
51
+
52
+ it("returns cancelled and successful execution results", async () => {
53
+ const apiMock = createExtensionApiMock();
54
+ askUserQuestionExtension(apiMock.api);
55
+ const tool = apiMock.getTool("ask_user_question");
56
+ if (!tool.execute || !tool.renderCall || !tool.renderResult) throw new Error("tool renderers are missing");
57
+
58
+ const cancelledResult = await tool.execute(
59
+ "call-3",
60
+ { title: "Title", questions: [{ id: "x", type: "text", prompt: "Q" }] },
61
+ undefined,
62
+ undefined,
63
+ {
64
+ hasUI: true,
65
+ ui: {
66
+ custom: vi.fn(async () => ({
67
+ title: "Title",
68
+ questions: normalizeQuestions([{ id: "x", type: "text", prompt: "Q" }]),
69
+ answers: [{ id: "x", type: "text", value: "", wasCustom: true }],
70
+ cancelled: true,
71
+ })),
72
+ },
73
+ } as unknown as ExtensionContext,
74
+ );
75
+
76
+ const successResult = await tool.execute(
77
+ "call-4",
78
+ {
79
+ title: "Title",
80
+ questions: [
81
+ { id: "x", type: "text", prompt: "Q" },
82
+ { id: "y", type: "checkbox", prompt: "Q2", options: [{ value: "a", label: "Alpha" }] },
83
+ ],
84
+ },
85
+ undefined,
86
+ undefined,
87
+ {
88
+ hasUI: true,
89
+ ui: {
90
+ custom: vi.fn(async () => ({
91
+ title: "Title",
92
+ questions: normalizeQuestions([
93
+ { id: "x", type: "text", prompt: "Q" },
94
+ { id: "y", type: "checkbox", prompt: "Q2", options: [{ value: "a", label: "Alpha" }] },
95
+ ]),
96
+ answers: [
97
+ { id: "x", type: "text", value: "hello", wasCustom: true },
98
+ { id: "y", type: "checkbox", value: ["a"], wasCustom: false },
99
+ ],
100
+ cancelled: false,
101
+ })),
102
+ },
103
+ } as unknown as ExtensionContext,
104
+ );
105
+
106
+ expect(cancelledResult).toMatchObject({ content: [{ type: "text", text: "User cancelled the form" }] });
107
+ expect(successResult).toMatchObject({ content: [{ type: "text", text: "Q1: hello\nQ2: a" }] });
108
+ expect(
109
+ tool.renderCall({ title: "Title", questions: [{ id: "x", type: "text", prompt: "Q" }] }, theme),
110
+ ).toBeTruthy();
111
+ expect(tool.renderResult(successResult, {}, theme)).toBeTruthy();
112
+ });
113
+ });