@ryan_nookpi/pi-extension-ask-user-question 0.1.1 → 0.2.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.
- package/README.md +78 -14
- package/constants.ts +10 -0
- package/controller.test.ts +281 -0
- package/controller.ts +280 -0
- package/form-ui.test.ts +57 -0
- package/form-ui.ts +38 -0
- package/index.test.ts +113 -0
- package/index.ts +62 -510
- package/output.test.ts +102 -0
- package/output.ts +89 -0
- package/package.json +2 -2
- package/schema.ts +36 -0
- package/state.test.ts +136 -0
- package/state.ts +131 -0
- package/types.ts +85 -0
- package/view.test.ts +337 -0
- package/view.ts +218 -0
package/form-ui.test.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createEditorTheme, runAskUserQuestionForm } from "./form-ui.ts";
|
|
5
|
+
import { normalizeQuestions } from "./state.ts";
|
|
6
|
+
import type { FormResult, RenderTheme } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const theme: RenderTheme = {
|
|
9
|
+
fg: (_color, text) => text,
|
|
10
|
+
bg: (_color, text) => `[${text}]`,
|
|
11
|
+
bold: (text) => text,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("ask-user-question/form-ui", () => {
|
|
15
|
+
it("creates the editor theme mapping", () => {
|
|
16
|
+
const editorTheme = createEditorTheme(theme);
|
|
17
|
+
expect(editorTheme.borderColor("x")).toBe("x");
|
|
18
|
+
expect(editorTheme.selectList.selectedPrefix("a")).toBe("a");
|
|
19
|
+
expect(editorTheme.selectList.selectedText("b")).toBe("b");
|
|
20
|
+
expect(editorTheme.selectList.description("c")).toBe("c");
|
|
21
|
+
expect(editorTheme.selectList.scrollInfo("d")).toBe("d");
|
|
22
|
+
expect(editorTheme.selectList.noMatch("e")).toBe("e");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("wires the custom UI factory to the controller", async () => {
|
|
26
|
+
const questions = normalizeQuestions([{ id: "text", type: "text", prompt: "Explain", default: "seed" }]);
|
|
27
|
+
let captured: FormResult | undefined;
|
|
28
|
+
const done = vi.fn((result: unknown) => {
|
|
29
|
+
captured = result as FormResult;
|
|
30
|
+
});
|
|
31
|
+
const requestRender = vi.fn();
|
|
32
|
+
const custom = vi.fn(async (factory: Parameters<NonNullable<ExtensionContext["ui"]>["custom"]>[0]) => {
|
|
33
|
+
const component = (await factory({ requestRender } as never, theme as never, {} as never, done)) as {
|
|
34
|
+
handleInput(data: string): void;
|
|
35
|
+
};
|
|
36
|
+
component.handleInput("!");
|
|
37
|
+
component.handleInput("\r");
|
|
38
|
+
expect(requestRender).toHaveBeenCalled();
|
|
39
|
+
if (!captured) throw new Error("result was not captured");
|
|
40
|
+
return captured;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ctx = {
|
|
44
|
+
hasUI: true,
|
|
45
|
+
ui: { custom },
|
|
46
|
+
} as unknown as ExtensionContext;
|
|
47
|
+
|
|
48
|
+
const result = await runAskUserQuestionForm(ctx, { title: "Title", description: "Desc" }, questions);
|
|
49
|
+
expect(custom).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(result).toEqual({
|
|
51
|
+
title: "Title",
|
|
52
|
+
questions,
|
|
53
|
+
answers: [{ id: "text", type: "text", value: "seed!", wasCustom: true }],
|
|
54
|
+
cancelled: false,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
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개 문항");
|
|
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: "오류: UI를 사용할 수 없습니다. 현재 비대화형 모드에서 실행 중입니다." }],
|
|
48
|
+
});
|
|
49
|
+
expect(noQuestions).toMatchObject({ content: [{ type: "text", text: "오류: 질문이 제공되지 않았습니다." }] });
|
|
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: "사용자가 입력 폼을 취소했습니다" }] });
|
|
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
|
+
});
|