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

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 CHANGED
@@ -1,43 +1,90 @@
1
1
  # @ryan_nookpi/pi-extension-ask-user-question
2
2
 
3
- Interactive multi-question form tool for pi.
3
+ pi에서 `ask_user_question` 도구를 추가해, 사용자에게 여러 질문을 한 번에 구조화해서 받을 수 있게 해주는 익스텐션입니다.
4
4
 
5
- After installation, pi can use the `ask_user_question` tool to collect structured input from the user with:
6
-
7
- - `radio` questions for single choice
8
- - `checkbox` questions for multiple choice
9
- - `text` questions for free-form answers
10
- - optional `Other...` inputs for custom answers
11
-
12
- ## Install
5
+ ## 설치
13
6
 
14
7
  ```bash
15
8
  pi install npm:@ryan_nookpi/pi-extension-ask-user-question
16
9
  ```
17
10
 
18
- ## Example
11
+ ## 무엇을 할 수 있나
12
+
13
+ - `radio`: 하나만 고르는 단일 선택 질문
14
+ - `checkbox`: 여러 개를 고르는 복수 선택 질문
15
+ - `text`: 자유 입력 질문
16
+ - `allowOther: true`: 선택지 외 값을 직접 입력하는 `기타...` 경로 제공
17
+ - 여러 질문을 한 번의 폼으로 묶어 입력 받기
18
+
19
+ ## 언제 쓰면 좋은가
20
+
21
+ - 요구사항이 모호해서 확인이 필요할 때
22
+ - 여러 선택지 중 하나를 고르게 해야 할 때
23
+ - 선호도, 배포 환경, 옵션 조합 등을 한 번에 수집할 때
24
+ - 일반 텍스트 질문보다 구조화된 응답이 필요할 때
25
+
26
+ ## 파라미터 가이드
27
+
28
+ ### 최상위 필드
29
+
30
+ - `title`: 폼 상단 제목
31
+ - `description`: 폼 설명/안내 문구
32
+ - `questions`: 질문 배열
33
+
34
+ ### 질문 필드
35
+
36
+ - `id`: 질문 식별자
37
+ - `type`: `radio` | `checkbox` | `text`
38
+ - `prompt`: 사용자에게 보여줄 질문 문구
39
+ - `label`: 탭에 표시할 짧은 이름. 생략하면 `Q1`, `Q2`처럼 자동 생성
40
+ - `options`: `radio`/`checkbox`에서 사용할 선택지 목록
41
+ - `allowOther`: `기타...` 직접 입력 허용 여부
42
+ - `required`: 필수 응답 여부
43
+ - `placeholder`: `text` 입력창 플레이스홀더
44
+ - `default`: 기본값. `radio`/`text`는 문자열, `checkbox`는 문자열 배열
45
+
46
+ ### 옵션 필드
47
+
48
+ - `value`: 실제 반환값
49
+ - `label`: 화면에 보이는 문구
50
+ - `description`: 선택지 아래 보조 설명
51
+
52
+ ## 예시
19
53
 
20
54
  ```json
21
55
  {
22
- "title": "Deployment settings",
23
- "description": "Need a few decisions before continuing.",
56
+ "title": "배포 설정 확인",
57
+ "description": "진행 전에 가지 선택이 필요합니다.",
24
58
  "questions": [
25
59
  {
26
60
  "id": "env",
27
61
  "type": "radio",
28
- "prompt": "Which environment should I use?",
62
+ "prompt": "어느 환경에 배포할까요?",
29
63
  "options": [
30
- { "value": "staging", "label": "Staging" },
31
- { "value": "prod", "label": "Production" }
64
+ { "value": "staging", "label": "스테이징" },
65
+ { "value": "prod", "label": "프로덕션" }
66
+ ]
67
+ },
68
+ {
69
+ "id": "targets",
70
+ "type": "checkbox",
71
+ "prompt": "이번에 함께 반영할 항목은 무엇인가요?",
72
+ "options": [
73
+ { "value": "web", "label": "웹" },
74
+ { "value": "admin", "label": "어드민" }
32
75
  ]
33
76
  },
34
77
  {
35
78
  "id": "notes",
36
79
  "type": "text",
37
- "prompt": "Anything else I should know?",
80
+ "prompt": "추가로 알아야 점이 있나요?",
38
81
  "required": false,
39
- "placeholder": "Optional notes"
82
+ "placeholder": "선택 사항"
40
83
  }
41
84
  ]
42
85
  }
43
86
  ```
87
+
88
+ ## 반환 형태
89
+
90
+ 응답은 각 질문의 `id` 기준으로 정리되어 반환됩니다. 취소 시에는 `cancelled: true`가 내려오고, 완료 시에는 질문별 답변 목록이 포함됩니다.
@@ -112,7 +112,7 @@ describe("ask-user-question/controller", () => {
112
112
  setup.controller.handleInput("\r");
113
113
  expect(setup.controller.getState()).toMatchObject({ otherMode: true, otherQuestionId: "radio" });
114
114
  expect(setup.editor.getText()).toBe("saved");
115
- expect(setup.controller.render(80).join("\n")).toContain("Your answer:");
115
+ expect(setup.controller.render(80).join("\n")).toContain("직접 입력:");
116
116
 
117
117
  setup.controller.handleInput("x");
118
118
  setup.controller.handleInput("\t");
package/index.test.ts CHANGED
@@ -21,7 +21,7 @@ describe("ask-user-question extension", () => {
21
21
  expect(AskUserQuestionParams).toBeTruthy();
22
22
  expect(errorResult("boom").content[0].text).toBe("boom");
23
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");
24
+ expect(buildRenderCallText({ questions: [{ id: "x", type: "text", prompt: "Q" }] }, theme)).toContain("1 문항");
25
25
  expect(buildRenderResultText({ content: [{ type: "text", text: "plain" }] }, theme)).toBe("plain");
26
26
  });
27
27
 
@@ -44,9 +44,9 @@ describe("ask-user-question extension", () => {
44
44
  } as unknown as ExtensionContext);
45
45
 
46
46
  expect(noUi).toMatchObject({
47
- content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
47
+ content: [{ type: "text", text: "오류: UI 사용할 없습니다. 현재 비대화형 모드에서 실행 중입니다." }],
48
48
  });
49
- expect(noQuestions).toMatchObject({ content: [{ type: "text", text: "Error: No questions provided" }] });
49
+ expect(noQuestions).toMatchObject({ content: [{ type: "text", text: "오류: 질문이 제공되지 않았습니다." }] });
50
50
  });
51
51
 
52
52
  it("returns cancelled and successful execution results", async () => {
@@ -103,7 +103,7 @@ describe("ask-user-question extension", () => {
103
103
  } as unknown as ExtensionContext,
104
104
  );
105
105
 
106
- expect(cancelledResult).toMatchObject({ content: [{ type: "text", text: "User cancelled the form" }] });
106
+ expect(cancelledResult).toMatchObject({ content: [{ type: "text", text: "사용자가 입력 폼을 취소했습니다" }] });
107
107
  expect(successResult).toMatchObject({ content: [{ type: "text", text: "Q1: hello\nQ2: a" }] });
108
108
  expect(
109
109
  tool.renderCall({ title: "Title", questions: [{ id: "x", type: "text", prompt: "Q" }] }, theme),
package/index.ts CHANGED
@@ -13,25 +13,25 @@ import { AskUserQuestionParams } from "./schema.ts";
13
13
  import { normalizeQuestions } from "./state.ts";
14
14
  import type { AskUserQuestionParamsInput, FormResult, Question } from "./types.ts";
15
15
 
16
- const TOOL_DESCRIPTION = `Ask the user one or more questions using an interactive form. Supports three question types:
17
- - **radio**: Single-select from predefined options (like multiple choice)
18
- - **checkbox**: Multi-select from options (pick all that apply)
19
- - **text**: Free-form text input
16
+ const TOOL_DESCRIPTION = `인터랙티브 폼으로 사용자에게 하나 이상의 질문을 묻습니다. 지원하는 질문 유형은 다음 가지입니다:
17
+ - **radio**: 미리 정의된 보기 하나를 고르는 단일 선택
18
+ - **checkbox**: 여러 보기를 동시에 고르는 복수 선택
19
+ - **text**: 자유롭게 입력하는 텍스트 답변
20
20
 
21
- Each radio/checkbox question can include an "Other..." option that lets the user type a custom answer.
21
+ radio/checkbox 질문에는 사용자가 직접 값을 입력할 있는 "기타..." 옵션을 포함할 있습니다.
22
22
 
23
- Use this tool when you need user input to proceed for clarifying requirements, getting preferences, confirming decisions, or choosing between alternatives. Prefer this over asking plain-text questions in your response.`;
23
+ 요구사항을 확인하거나, 선호도를 물어보거나, 구현 방향에 대한 결정을 받아야 사용하세요. 일반 텍스트로 질문을 던지는 대신 도구를 우선 사용합니다.`;
24
24
 
25
25
  const PROMPT_GUIDELINES = [
26
- "Use ask_user_question instead of asking questions in plain text when you need structured user input.",
27
- "Prefer radio for single-choice, checkbox for multi-choice, text for open-ended answers.",
28
- "Always include an 'Other' escape hatch (allowOther: true) unless the options are exhaustive.",
29
- "Group related questions in a single call rather than making multiple separate calls.",
26
+ "구조화된 사용자 입력이 필요하면 일반 텍스트 질문 대신 ask_user_question을 사용하세요.",
27
+ "단일 선택은 radio, 복수 선택은 checkbox, 서술형 답변은 text 우선 사용하세요.",
28
+ "선택지가 완전히 닫혀 있지 않다면 allowOther: true '기타' 입력 경로를 열어두세요.",
29
+ "관련 질문은 여러 나누지 말고 번의 호출에 묶어 전달하세요.",
30
30
  ];
31
31
 
32
32
  function buildCancelledResponse(result: FormResult) {
33
33
  return {
34
- content: [{ type: "text" as const, text: "User cancelled the form" }],
34
+ content: [{ type: "text" as const, text: "사용자가 입력 폼을 취소했습니다" }],
35
35
  details: result,
36
36
  };
37
37
  }
@@ -49,19 +49,19 @@ export { AskUserQuestionParams, buildRenderCallText, buildRenderResultText, erro
49
49
  export default function askUserQuestion(pi: ExtensionAPI) {
50
50
  pi.registerTool({
51
51
  name: "ask_user_question",
52
- label: "Ask User",
52
+ label: "사용자 질문",
53
53
  description: TOOL_DESCRIPTION,
54
- promptSnippet: "Ask the user interactive questions with radio, checkbox, or text inputs",
54
+ promptSnippet: "radio, checkbox, text 입력을 사용하는 인터랙티브 질문 열기",
55
55
  promptGuidelines: PROMPT_GUIDELINES,
56
56
  parameters: AskUserQuestionParams,
57
57
  async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) {
58
58
  if (!ctx.hasUI) {
59
- return errorResult("Error: UI not available (running in non-interactive mode)");
59
+ return errorResult("오류: UI 사용할 없습니다. 현재 비대화형 모드에서 실행 중입니다.");
60
60
  }
61
61
 
62
62
  const params = rawParams as AskUserQuestionParamsInput;
63
63
  if (!params.questions.length) {
64
- return errorResult("Error: No questions provided");
64
+ return errorResult("오류: 질문이 제공되지 않았습니다.");
65
65
  }
66
66
 
67
67
  const questions = normalizeQuestions(params.questions as Question[]);
package/output.test.ts CHANGED
@@ -40,7 +40,7 @@ describe("ask-user-question/output", () => {
40
40
  });
41
41
 
42
42
  it("formats successful result content", () => {
43
- expect(formatResultContent(formResult)).toBe("One: (wrote) custom\nTwo: (none selected)\nThree: (empty)");
43
+ expect(formatResultContent(formResult)).toBe("One: (직접 입력) custom\nTwo: (선택 없음)\nThree: (비어 있음)");
44
44
  expect(
45
45
  formatResultContent({
46
46
  ...formResult,
@@ -62,8 +62,8 @@ describe("ask-user-question/output", () => {
62
62
  it("builds render-call summaries", () => {
63
63
  expect(
64
64
  buildRenderCallText({ title: "Title", questions: [{ id: "x", type: "radio", prompt: "Q" }] }, theme),
65
- ).toContain("Title 1 question (radio)");
66
- expect(buildRenderCallText({}, theme)).toContain("0 questions");
65
+ ).toContain("Title 1 문항 (radio)");
66
+ expect(buildRenderCallText({}, theme)).toContain("0 문항");
67
67
  expect(renderCall({ title: "Title", questions: [] }, theme)).toBeTruthy();
68
68
  });
69
69
 
@@ -72,9 +72,9 @@ describe("ask-user-question/output", () => {
72
72
  expect(buildRenderResultText({ content: [{ type: "text" }] }, theme)).toBe("");
73
73
  expect(buildRenderResultText({ content: [{ type: "image", text: "ignored" }] }, theme)).toBe("");
74
74
  expect(buildRenderResultText({}, theme)).toBe("");
75
- expect(buildRenderResultText({ details: { ...formResult, cancelled: true } }, theme)).toBe("Cancelled");
75
+ expect(buildRenderResultText({ details: { ...formResult, cancelled: true } }, theme)).toBe("취소됨");
76
76
  expect(buildRenderResultText({ details: formResult }, theme)).toBe(
77
- "✓ One: (wrote) custom\n✓ Two: (none)\n✓ Three: (empty)",
77
+ "✓ One: (직접 입력) custom\n✓ Two: (선택 없음)\n✓ Three: (비어 있음)",
78
78
  );
79
79
  expect(
80
80
  buildRenderResultText(
package/output.ts CHANGED
@@ -19,14 +19,14 @@ export function formatResultContent(result: FormResult): string {
19
19
  const question = result.questions.find((candidate) => candidate.id === answer.id);
20
20
  const label = question?.label || answer.id;
21
21
  if (answer.type === "radio") {
22
- const prefix = answer.wasCustom ? "(wrote) " : "";
22
+ const prefix = answer.wasCustom ? "(직접 입력) " : "";
23
23
  return `${label}: ${prefix}${answer.value}`;
24
24
  }
25
25
  if (answer.type === "checkbox") {
26
26
  const values = Array.isArray(answer.value) ? answer.value : [answer.value];
27
- return values.length === 0 ? `${label}: (none selected)` : `${label}: ${values.join(", ")}`;
27
+ return values.length === 0 ? `${label}: (선택 없음)` : `${label}: ${values.join(", ")}`;
28
28
  }
29
- return `${label}: ${answer.value || "(empty)"}`;
29
+ return `${label}: ${answer.value || "(비어 있음)"}`;
30
30
  })
31
31
  .join("\n");
32
32
  }
@@ -37,7 +37,7 @@ export function buildRenderCallText(args: { questions?: Question[]; title?: stri
37
37
  if (args.title) {
38
38
  text += `${theme.fg("accent", args.title)} `;
39
39
  }
40
- text += theme.fg("muted", `${questions.length} question${questions.length !== 1 ? "s" : ""}`);
40
+ text += theme.fg("muted", `${questions.length} 문항`);
41
41
  const types = [...new Set(questions.map((question) => question.type))].join(", ");
42
42
  if (types) {
43
43
  text += theme.fg("dim", ` (${types})`);
@@ -56,7 +56,7 @@ export function buildRenderResultText(
56
56
  }
57
57
 
58
58
  if (details.cancelled) {
59
- return theme.fg("warning", "Cancelled");
59
+ return theme.fg("warning", "취소됨");
60
60
  }
61
61
 
62
62
  return details.answers
@@ -64,15 +64,15 @@ export function buildRenderResultText(
64
64
  const question = details.questions.find((candidate) => candidate.id === answer.id);
65
65
  const label = question?.label || answer.id;
66
66
  if (answer.type === "radio") {
67
- const prefix = answer.wasCustom ? theme.fg("dim", "(wrote) ") : "";
67
+ const prefix = answer.wasCustom ? theme.fg("dim", "(직접 입력) ") : "";
68
68
  return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${prefix}${answer.value}`;
69
69
  }
70
70
  if (answer.type === "checkbox") {
71
71
  const values = Array.isArray(answer.value) ? answer.value : [answer.value];
72
- const display = values.length ? values.join(", ") : theme.fg("dim", "(none)");
72
+ const display = values.length ? values.join(", ") : theme.fg("dim", "(선택 없음)");
73
73
  return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${display}`;
74
74
  }
75
- return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${answer.value || theme.fg("dim", "(empty)")}`;
75
+ return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${answer.value || theme.fg("dim", "(비어 있음)")}`;
76
76
  })
77
77
  .join("\n");
78
78
  }
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-ask-user-question",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "AskUserQuestion tool extension for pi.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Jonghakseo/pi-extension.git",
9
+ "directory": "packages/ask-user-question"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Jonghakseo/pi-extension/issues"
13
+ },
14
+ "homepage": "https://github.com/Jonghakseo/pi-extension/tree/main/packages/ask-user-question#readme",
5
15
  "type": "module",
6
16
  "keywords": [
7
17
  "pi-package"
package/schema.ts CHANGED
@@ -2,36 +2,35 @@ import { StringEnum } from "@mariozechner/pi-ai";
2
2
  import { Type } from "@sinclair/typebox";
3
3
 
4
4
  export const OptionSchema = Type.Object({
5
- value: Type.String({ description: "Value returned when selected" }),
6
- label: Type.String({ description: "Display label" }),
7
- description: Type.Optional(Type.String({ description: "Help text shown below the label" })),
5
+ value: Type.String({ description: "선택 반환할 " }),
6
+ label: Type.String({ description: "화면에 표시할 라벨" }),
7
+ description: Type.Optional(Type.String({ description: "옵션 아래에 표시할 보조 설명" })),
8
8
  });
9
9
 
10
10
  export const QuestionSchema = Type.Object({
11
- id: Type.String({ description: "Unique identifier for this question" }),
11
+ id: Type.String({ description: "질문 고유 식별자" }),
12
12
  type: StringEnum(["radio", "checkbox", "text"] as const, {
13
- description: "Question type: radio (single-select), checkbox (multi-select), or text (free input)",
13
+ description: "질문 유형: radio(단일 선택), checkbox(복수 선택), text(자유 입력)",
14
14
  }),
15
- prompt: Type.String({ description: "The question text to display" }),
16
- label: Type.Optional(Type.String({ description: "Short label for tab bar (defaults to Q1, Q2...)" })),
17
- options: Type.Optional(Type.Array(OptionSchema, { description: "Options for radio/checkbox types" })),
15
+ prompt: Type.String({ description: "사용자에게 표시할 질문 문구" }),
16
+ label: Type.Optional(Type.String({ description: " 바에 표시할 짧은 라벨(기본값: Q1, Q2...)" })),
17
+ options: Type.Optional(Type.Array(OptionSchema, { description: "radio/checkbox 선택지 목록" })),
18
18
  allowOther: Type.Optional(
19
- Type.Boolean({ description: "Add an 'Other...' option with text input (default: true for radio/checkbox)" }),
19
+ Type.Boolean({ description: "'기타...' 직접 입력 옵션 추가 여부 (radio/checkbox 기본값: true)" }),
20
20
  ),
21
- required: Type.Optional(Type.Boolean({ description: "Whether an answer is required (default: true)" })),
22
- placeholder: Type.Optional(Type.String({ description: "Placeholder for text inputs" })),
21
+ required: Type.Optional(Type.Boolean({ description: "응답 필수 여부 (기본값: true)" })),
22
+ placeholder: Type.Optional(Type.String({ description: "text 입력 표시할 플레이스홀더" })),
23
23
  default: Type.Optional(
24
24
  Type.Union([Type.String(), Type.Array(Type.String())], {
25
- description: "Default value(s). String for radio/text, string[] for checkbox",
25
+ description: "기본값. radio/text 문자열, checkbox는 문자열 배열",
26
26
  }),
27
27
  ),
28
28
  });
29
29
 
30
30
  export const AskUserQuestionParams = Type.Object({
31
- title: Type.Optional(Type.String({ description: "Form title displayed at the top" })),
32
- description: Type.Optional(Type.String({ description: "Brief context or instructions shown under the title" })),
31
+ title: Type.Optional(Type.String({ description: " 상단에 표시할 제목" })),
32
+ description: Type.Optional(Type.String({ description: " 상단에 표시할 안내 문구" })),
33
33
  questions: Type.Array(QuestionSchema, {
34
- description:
35
- "One or more questions to ask. Use radio for single-select, checkbox for multi-select, text for free input",
34
+ description: "질문 목록. radio는 단일 선택, checkbox는 복수 선택, text는 자유 입력",
36
35
  }),
37
36
  });
package/view.test.ts CHANGED
@@ -52,10 +52,10 @@ describe("ask-user-question/view", () => {
52
52
  const input = createInput({ currentTab: 2 });
53
53
  const output = renderForm(input).join("\n");
54
54
 
55
- expect(output).toContain("Review & Submit");
55
+ expect(output).toContain("검토 제출");
56
56
  expect(output).toContain("Q1:");
57
- expect(output).toContain("Required: Q1, Q2");
58
- expect(output).toContain("Tab/←→ navigate questions • Enter submit • Esc cancel");
57
+ expect(output).toContain("필수 응답: Q1, Q2");
58
+ expect(output).toContain("Tab/←→ 질문 이동 • Enter 제출 • Esc 취소");
59
59
  });
60
60
 
61
61
  it("renders radio questions including descriptions and other-mode editor", () => {
@@ -80,12 +80,12 @@ describe("ask-user-question/view", () => {
80
80
  }),
81
81
  ).join("\n");
82
82
 
83
- expect(output).toContain("**Pick one** [single-select]");
83
+ expect(output).toContain("**Pick one** [단일 선택]");
84
84
  expect(output).toContain("Alpha");
85
85
  expect(output).toContain("first");
86
- expect(output).toContain("Other: custom");
87
- expect(output).toContain("Your answer:");
88
- expect(output).toContain("Enter submit • Esc go back");
86
+ expect(output).toContain("기타: custom");
87
+ expect(output).toContain("직접 입력:");
88
+ expect(output).toContain("Enter 제출 • Esc 돌아가기");
89
89
  });
90
90
 
91
91
  it("renders checkbox and text questions with placeholders and text footer", () => {
@@ -110,17 +110,17 @@ describe("ask-user-question/view", () => {
110
110
  const checkboxOutput = renderForm(createInput({ questions, answerState, editorLines: [], editorText: "" })).join(
111
111
  "\n",
112
112
  );
113
- expect(checkboxOutput).toContain("**Pick many** [multi-select]");
114
- expect(checkboxOutput).toContain("Other: custom option");
115
- expect(checkboxOutput).toContain("Space toggle");
113
+ expect(checkboxOutput).toContain("**Pick many** [복수 선택]");
114
+ expect(checkboxOutput).toContain("기타: custom option");
115
+ expect(checkboxOutput).toContain("Space 토글");
116
116
 
117
117
  const textOutput = renderForm(
118
118
  createInput({ questions, answerState, currentTab: 1, editorLines: ["typed line"], editorText: "" }),
119
119
  ).join("\n");
120
- expect(textOutput).toContain("**Explain** [text]");
120
+ expect(textOutput).toContain("**Explain** [텍스트]");
121
121
  expect(textOutput).toContain("Type here");
122
122
  expect(textOutput).toContain("typed line");
123
- expect(textOutput).toContain("Tab/←→ navigate • Enter submit • Esc cancel");
123
+ expect(textOutput).toContain("Tab/←→ 이동 • Enter 제출 • Esc 취소");
124
124
  });
125
125
 
126
126
  it("renders answered submit states and optional question variants", () => {
@@ -152,27 +152,27 @@ describe("ask-user-question/view", () => {
152
152
  answerState.textAnswers.set("text", "filled");
153
153
 
154
154
  const submitOutput = renderForm(createInput({ questions, answerState, currentTab: 3 })).join("\n");
155
- expect(submitOutput).toContain("Press Enter to submit");
155
+ expect(submitOutput).toContain("Enter 제출");
156
156
  expect(submitOutput).toContain("Alpha");
157
157
  expect(submitOutput).toContain("b");
158
158
  expect(submitOutput).toContain("filled");
159
159
 
160
160
  const radioOutput = renderForm(createInput({ questions, answerState, currentTab: 0, cursorIdx: 0 })).join("\n");
161
161
  expect(radioOutput).toContain("❯ ◉ Alpha");
162
- expect(radioOutput).toContain("Enter select • Esc cancel");
163
- expect(radioOutput).not.toContain("Other...");
162
+ expect(radioOutput).toContain("Enter 선택 • Esc 취소");
163
+ expect(radioOutput).not.toContain("기타...");
164
164
 
165
165
  const checkboxOutput = renderForm(createInput({ questions, answerState, currentTab: 1, cursorIdx: 0 })).join("\n");
166
166
  expect(checkboxOutput).toContain("☑ Beta");
167
167
  expect(checkboxOutput).toContain("desc");
168
- expect(checkboxOutput).toContain("Enter next");
168
+ expect(checkboxOutput).toContain("Enter 다음");
169
169
 
170
170
  const textOutput = renderForm(
171
171
  createInput({ questions, answerState, currentTab: 2, editorLines: ["filled"], editorText: "filled" }),
172
172
  ).join("\n");
173
173
  expect(textOutput).toContain("filled");
174
- expect(textOutput).toContain("Enter submit • Esc cancel");
175
- expect(textOutput).not.toContain("*required");
174
+ expect(textOutput).toContain("Enter 제출 • Esc 취소");
175
+ expect(textOutput).not.toContain("*필수");
176
176
  });
177
177
 
178
178
  it("covers direct helper branches for invalid tabs and compact variants", () => {
@@ -198,7 +198,7 @@ describe("ask-user-question/view", () => {
198
198
  submitInput.answerState.radioAnswers.set("radio", { value: "custom", label: "custom", wasCustom: true });
199
199
  submitInput.answerState.checkCustom.set("check", "custom");
200
200
  renderSubmitTab(submitInput, (text) => submitLines.push(text), 80);
201
- expect(submitLines.join("\n")).toContain("(wrote) custom");
201
+ expect(submitLines.join("\n")).toContain("(직접 입력) custom");
202
202
 
203
203
  const unansweredLines: string[] = [];
204
204
  const unansweredInput = createInput({
@@ -207,7 +207,7 @@ describe("ask-user-question/view", () => {
207
207
  });
208
208
  unansweredInput.answerState.checkAnswers.delete("check");
209
209
  renderSubmitTab(unansweredInput, (text) => unansweredLines.push(text), 80);
210
- expect(unansweredLines.join("\n")).toContain("(unanswered)");
210
+ expect(unansweredLines.join("\n")).toContain("(미응답)");
211
211
 
212
212
  const output = renderForm(createInput({ currentTab: 99 }));
213
213
  expect(output.length).toBeGreaterThanOrEqual(2);
@@ -233,7 +233,7 @@ describe("ask-user-question/view", () => {
233
233
  80,
234
234
  );
235
235
  expect(radioLines.join("\n")).toContain("❯ ◉ Alpha");
236
- expect(radioLines.join("\n")).toContain("Other...");
236
+ expect(radioLines.join("\n")).toContain("기타...");
237
237
  const radioSelectedLines: string[] = [];
238
238
  renderQuestion(
239
239
  createInput({
@@ -264,8 +264,8 @@ describe("ask-user-question/view", () => {
264
264
  }),
265
265
  ).join("\n");
266
266
  expect(checkboxOutput).toContain("☐ Alpha");
267
- expect(checkboxOutput).toContain("Other...");
268
- expect(checkboxOutput).toContain("Enter submit • Esc cancel");
267
+ expect(checkboxOutput).toContain("기타...");
268
+ expect(checkboxOutput).toContain("Enter 제출 • Esc 취소");
269
269
  checkboxState.checkAnswers.set("check", new Set(["a"]));
270
270
  const checkboxOtherCursor = renderForm(
271
271
  createInput({
@@ -278,7 +278,7 @@ describe("ask-user-question/view", () => {
278
278
  }),
279
279
  ).join("\n");
280
280
  expect(checkboxOtherCursor).toContain("☑ Alpha");
281
- expect(checkboxOtherCursor).toContain("❯ ☐ Other...");
281
+ expect(checkboxOtherCursor).toContain("❯ ☐ 기타...");
282
282
  const checkboxLines: string[] = [];
283
283
  renderQuestion(
284
284
  createInput({
@@ -319,7 +319,7 @@ describe("ask-user-question/view", () => {
319
319
  editorLines: [],
320
320
  }),
321
321
  ).join("\n");
322
- expect(radioFooterOutput).toContain("Enter select • Esc cancel");
322
+ expect(radioFooterOutput).toContain("Enter 선택 • Esc 취소");
323
323
  const textOutput = renderForm(
324
324
  createInput({
325
325
  title: undefined,
@@ -330,8 +330,8 @@ describe("ask-user-question/view", () => {
330
330
  editorText: "",
331
331
  }),
332
332
  ).join("\n");
333
- expect(textOutput).toContain("**Only** [text]");
334
- expect(textOutput).toContain("Enter submit • Esc cancel");
335
- expect(textOutput).not.toContain("*required");
333
+ expect(textOutput).toContain("**Only** [텍스트]");
334
+ expect(textOutput).toContain("Enter 제출 • Esc 취소");
335
+ expect(textOutput).not.toContain("*필수");
336
336
  });
337
337
  });
package/view.ts CHANGED
@@ -14,7 +14,7 @@ function createLineHelpers(width: number, theme: RenderFormInput["theme"]) {
14
14
 
15
15
  export function renderSubmitTab(input: RenderFormInput, add: (text: string) => void, maxWidth: number): void {
16
16
  const { questions, answerState, theme } = input;
17
- add(` ${theme.fg("accent", theme.bold("Review & Submit"))}`);
17
+ add(` ${theme.fg("accent", theme.bold("검토 제출"))}`);
18
18
  add("");
19
19
 
20
20
  for (const question of questions) {
@@ -22,10 +22,10 @@ export function renderSubmitTab(input: RenderFormInput, add: (text: string) => v
22
22
  if (question.type === "radio") {
23
23
  const answer = answerState.radioAnswers.get(question.id);
24
24
  if (answer) {
25
- const prefix = answer.wasCustom ? theme.fg("dim", "(wrote) ") : "";
25
+ const prefix = answer.wasCustom ? theme.fg("dim", "(직접 입력) ") : "";
26
26
  add(` ${label} ${prefix}${answer.label}`);
27
27
  } else {
28
- add(` ${label} ${theme.fg("warning", "(unanswered)")}`);
28
+ add(` ${label} ${theme.fg("warning", "(미응답)")}`);
29
29
  }
30
30
  continue;
31
31
  }
@@ -34,8 +34,8 @@ export function renderSubmitTab(input: RenderFormInput, add: (text: string) => v
34
34
  const selected = answerState.checkAnswers.get(question.id) ?? new Set<string>();
35
35
  const custom = answerState.checkCustom.get(question.id)?.trim();
36
36
  const values = [...selected];
37
- if (custom) values.push(`${theme.fg("dim", "(wrote)")} ${custom}`);
38
- add(` ${label} ${values.length ? values.join(", ") : theme.fg("warning", "(unanswered)")}`);
37
+ if (custom) values.push(`${theme.fg("dim", "(직접 입력)")} ${custom}`);
38
+ add(` ${label} ${values.length ? values.join(", ") : theme.fg("warning", "(미응답)")}`);
39
39
  continue;
40
40
  }
41
41
 
@@ -43,23 +43,23 @@ export function renderSubmitTab(input: RenderFormInput, add: (text: string) => v
43
43
  if (answer) {
44
44
  add(` ${label} ${truncateToWidth(answer, maxWidth - visibleWidth(question.label) - 5)}`);
45
45
  } else {
46
- add(` ${label} ${theme.fg("warning", "(unanswered)")}`);
46
+ add(` ${label} ${theme.fg("warning", "(미응답)")}`);
47
47
  }
48
48
  }
49
49
 
50
50
  add("");
51
51
  if (allRequiredAnswered(answerState, questions)) {
52
- add(` ${theme.fg("success", "Press Enter to submit")}`);
52
+ add(` ${theme.fg("success", "Enter 제출")}`);
53
53
  } else {
54
54
  const missing = questions
55
55
  .filter((question) => question.required && !isAnswered(answerState, question))
56
56
  .map((question) => question.label)
57
57
  .join(", ");
58
- add(` ${theme.fg("warning", `Required: ${missing}`)}`);
58
+ add(` ${theme.fg("warning", `필수 응답: ${missing}`)}`);
59
59
  }
60
60
 
61
61
  add("");
62
- add(theme.fg("dim", " Tab/←→ navigate questions • Enter submit • Esc cancel"));
62
+ add(theme.fg("dim", " Tab/←→ 질문 이동 • Enter 제출 • Esc 취소"));
63
63
  }
64
64
 
65
65
  export function renderTabBar(input: RenderFormInput, add: (text: string) => void): void {
@@ -76,7 +76,7 @@ export function renderTabBar(input: RenderFormInput, add: (text: string) => void
76
76
  }
77
77
 
78
78
  const submitActive = currentTab === questions.length;
79
- const submitText = ` ${SYM.submit} Submit `;
79
+ const submitText = ` ${SYM.submit} 제출 `;
80
80
  tabs.push(
81
81
  submitActive
82
82
  ? theme.bg("selectedBg", theme.fg("text", submitText))
@@ -93,13 +93,13 @@ export function renderQuestion(input: RenderFormInput, add: (text: string) => vo
93
93
 
94
94
  const typeTag =
95
95
  question.type === "radio"
96
- ? input.theme.fg("dim", "[single-select]")
96
+ ? input.theme.fg("dim", "[단일 선택]")
97
97
  : question.type === "checkbox"
98
- ? input.theme.fg("dim", "[multi-select]")
99
- : input.theme.fg("dim", "[text]");
98
+ ? input.theme.fg("dim", "[복수 선택]")
99
+ : input.theme.fg("dim", "[텍스트]");
100
100
 
101
101
  add(` ${input.theme.fg("text", input.theme.bold(question.prompt))} ${typeTag}`);
102
- if (question.required) add(` ${input.theme.fg("warning", "*required")}`);
102
+ if (question.required) add(` ${input.theme.fg("warning", "*필수")}`);
103
103
  add("");
104
104
 
105
105
  if (question.type === "radio") {
@@ -119,7 +119,7 @@ export function renderQuestion(input: RenderFormInput, add: (text: string) => vo
119
119
  const isSelected = selected?.wasCustom === true;
120
120
  const bullet = isSelected ? input.theme.fg("accent", SYM.radioOn) : input.theme.fg("dim", SYM.radioOff);
121
121
  const pointer = isCursor ? input.theme.fg("accent", SYM.pointer) : " ";
122
- const label = isSelected ? `Other: ${selected.label}` : "Other...";
122
+ const label = isSelected ? `기타: ${selected.label}` : "기타...";
123
123
  add(` ${pointer} ${bullet} ${input.theme.fg(isCursor ? "accent" : "muted", label)}`);
124
124
  }
125
125
  }
@@ -141,14 +141,14 @@ export function renderQuestion(input: RenderFormInput, add: (text: string) => vo
141
141
  const custom = input.answerState.checkCustom.get(question.id)?.trim();
142
142
  const box = custom ? input.theme.fg("accent", SYM.checkOn) : input.theme.fg("dim", SYM.checkOff);
143
143
  const pointer = isCursor ? input.theme.fg("accent", SYM.pointer) : " ";
144
- const label = custom ? `Other: ${custom}` : "Other...";
144
+ const label = custom ? `기타: ${custom}` : "기타...";
145
145
  add(` ${pointer} ${box} ${input.theme.fg(isCursor ? "accent" : "muted", label)}`);
146
146
  }
147
147
  }
148
148
 
149
149
  if (input.otherMode) {
150
150
  add("");
151
- add(` ${input.theme.fg("muted", " Your answer:")}`);
151
+ add(` ${input.theme.fg("muted", " 직접 입력:")}`);
152
152
  for (const line of input.editorLines) add(` ${line}`);
153
153
  return;
154
154
  }
@@ -172,22 +172,22 @@ export function renderFooter(input: RenderFormInput, add: (text: string) => void
172
172
  if (!question) return;
173
173
  add("");
174
174
  if (input.otherMode) {
175
- add(input.theme.fg("dim", " Enter submit • Esc go back"));
175
+ add(input.theme.fg("dim", " Enter 제출 • Esc 돌아가기"));
176
176
  return;
177
177
  }
178
178
  if (question.type === "text") {
179
- const nav = input.questions.length > 1 ? "Tab/←→ navigate • " : "";
180
- add(input.theme.fg("dim", ` ${nav}Enter submit • Esc cancel`));
179
+ const nav = input.questions.length > 1 ? "Tab/←→ 이동 • " : "";
180
+ add(input.theme.fg("dim", ` ${nav}Enter 제출 • Esc 취소`));
181
181
  return;
182
182
  }
183
183
  if (question.type === "checkbox") {
184
- const nav = input.questions.length > 1 ? "Tab/←→ navigate • " : "";
185
- const enterAction = input.questions.length > 1 ? "next" : "submit";
186
- add(input.theme.fg("dim", ` ↑↓ navigate • Space toggle • ${nav}Enter ${enterAction} • Esc cancel`));
184
+ const nav = input.questions.length > 1 ? "Tab/←→ 이동 • " : "";
185
+ const enterAction = input.questions.length > 1 ? "다음" : "제출";
186
+ add(input.theme.fg("dim", ` ↑↓ 이동 • Space 토글 • ${nav}Enter ${enterAction} • Esc 취소`));
187
187
  return;
188
188
  }
189
- const nav = input.questions.length > 1 ? "Tab/←→ navigate • " : "";
190
- add(input.theme.fg("dim", ` ↑↓ navigate • ${nav}Enter select • Esc cancel`));
189
+ const nav = input.questions.length > 1 ? "Tab/←→ 이동 • " : "";
190
+ add(input.theme.fg("dim", ` ↑↓ 이동 • ${nav}Enter 선택 • Esc 취소`));
191
191
  }
192
192
 
193
193
  export function renderForm(input: RenderFormInput): string[] {