@rol1n/ask-u-questions 0.1.0 → 0.1.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 CHANGED
@@ -8,12 +8,10 @@ Interactive single-choice, multiple-choice, and free-text questions for Pi. Regi
8
8
  pi install npm:@rol1n/ask-u-questions
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`. Enable one copy of the extension.
11
+ Also included in `@rol1n/pi-kit`.
12
12
 
13
13
  ## Use
14
14
 
15
- The agent calls the tool with a question and optional choices:
16
-
17
15
  ```json
18
16
  {
19
17
  "question": "Which output format should I use?",
@@ -25,8 +23,4 @@ The agent calls the tool with a question and optional choices:
25
23
  }
26
24
  ```
27
25
 
28
- Omit `options` or supply an empty array for free-text input. Set `multiSelect` only when several choices are expected. `details` adds supporting context; an Other choice is supplied automatically for choice questions.
29
-
30
- ## Requirements and behavior
31
-
32
- Use an interactive Pi terminal session for dialogs. Results distinguish answered, cancelled, and unavailable states, so a cancelled dialog is not an approval. No API credentials or package-specific settings are required.
26
+ Omit `options` or pass `[]` for free-text input. Set `multiSelect: true` only when multiple choices are expected. `details` adds supporting context, and an `Other` option is provided automatically for choice questions.
@@ -55,3 +55,26 @@ export function addWrapped(
55
55
  }
56
56
  }
57
57
 
58
+ const SHARED_UI_LOCK_KEY = "__piSharedUiLock";
59
+ function getSharedUiLock(): { withLock<T>(fn: () => T | Promise<T>): Promise<T> } {
60
+ const g = globalThis as any;
61
+ if (!g[SHARED_UI_LOCK_KEY]) {
62
+ let chain: Promise<void> = Promise.resolve();
63
+ g[SHARED_UI_LOCK_KEY] = {
64
+ withLock<T>(fn: () => T | Promise<T>): Promise<T> {
65
+ const prev = chain;
66
+ let release: () => void;
67
+ chain = new Promise<void>((r) => {
68
+ release = r;
69
+ });
70
+ return prev.then(fn).finally(() => release!());
71
+ },
72
+ };
73
+ }
74
+ return g[SHARED_UI_LOCK_KEY];
75
+ }
76
+ const sharedUiLock = getSharedUiLock();
77
+
78
+ export function withUILock<T>(fn: () => Promise<T>): Promise<T> {
79
+ return sharedUiLock.withLock(fn);
80
+ }
@@ -1,12 +1,16 @@
1
1
  /** Interactive questions, serialized through the shared dialog lock. */
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import type { AskUserQuestionMode } from "./types.ts";
4
- import { AskUserQuestionParams } from "./schema.ts";
5
- import { normalizeOptions } from "./answers.ts";
6
- import { buildResult, cancelledResult, unavailableResult } from "./results.ts";
3
+ import {
4
+ AskUserQuestionParams,
5
+ buildResult,
6
+ cancelledResult,
7
+ normalizeOptions,
8
+ unavailableResult,
9
+ type AskUserQuestionMode,
10
+ } from "./types.ts";
11
+ import { withUILock } from "./dialog-ui.ts";
7
12
  import { askSingleChoice } from "./single-choice.ts";
8
13
  import { askMultiChoice } from "./multi-choice.ts";
9
- import { withUILock } from "./ui-lock.ts";
10
14
  import { questionRenderers } from "./tool-renderers.ts";
11
15
 
12
16
  export type { AskOption, TextAnswer, OptionAnswer, OtherAnswer, AskAnswer, AskUserQuestionStatus, AskUserQuestionMode, AskUserQuestionResultDetails } from "./types.ts";
@@ -1,7 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Editor, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import type { AskAnswer, AskOption, DisplayOption } from "./types.ts";
4
- import { getOtherLabel, isChineseContext, sortAnswers } from "./answers.ts";
3
+ import { getOtherLabel, isChineseContext, sortAnswers, type AskAnswer, type AskOption, type DisplayOption } from "./types.ts";
5
4
  import { addWrapped, createEditorTheme } from "./dialog-ui.ts";
6
5
 
7
6
  export async function askMultiChoice(
@@ -257,4 +256,3 @@ export async function askMultiChoice(
257
256
  },
258
257
  );
259
258
  }
260
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rol1n/ask-u-questions",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "ask-u-questions extension from pi-kit",
6
6
  "keywords": [
@@ -8,8 +8,8 @@
8
8
  ],
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+ssh://git@github.com/RolinShmily/pi-kit.git",
12
- "directory": "extensions/ask-u-questions"
11
+ "url": "git+https://github.com/RolinShmily/pi-kit.git",
12
+ "directory": "ask-u-questions"
13
13
  },
14
14
  "homepage": "https://github.com/RolinShmily/pi-kit#readme",
15
15
  "bugs": {
@@ -21,10 +21,6 @@
21
21
  "publishConfig": {
22
22
  "access": "public"
23
23
  },
24
- "files": [
25
- "extensions/ask-u-questions",
26
- "extensions/ask-u-questions/README.md"
27
- ],
28
24
  "peerDependencies": {
29
25
  "@earendil-works/pi-ai": "*",
30
26
  "@earendil-works/pi-agent-core": "*",
@@ -34,7 +30,7 @@
34
30
  },
35
31
  "pi": {
36
32
  "extensions": [
37
- "./extensions/ask-u-questions/index.ts"
33
+ "./index.ts"
38
34
  ],
39
35
  "skills": [],
40
36
  "prompts": [],
@@ -1,7 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Editor, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import type { AskAnswer, AskOption, DisplayOption } from "./types.ts";
4
- import { getOtherLabel, isChineseContext } from "./answers.ts";
3
+ import { getOtherLabel, isChineseContext, type AskAnswer, type AskOption, type DisplayOption } from "./types.ts";
5
4
  import { addWrapped, createEditorTheme } from "./dialog-ui.ts";
6
5
 
7
6
  export async function askSingleChoice(
@@ -179,4 +178,3 @@ export async function askSingleChoice(
179
178
  },
180
179
  );
181
180
  }
182
-
@@ -1,8 +1,6 @@
1
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
- import type { AskUserQuestionParams } from "./schema.ts";
4
- import type { AskUserQuestionResultDetails } from "./types.ts";
5
- import { getOtherLabel, isChineseContext, normalizeOptions } from "./answers.ts";
3
+ import { AskUserQuestionParams, getOtherLabel, isChineseContext, normalizeOptions, type AskUserQuestionResultDetails } from "./types.ts";
6
4
 
7
5
  export const questionRenderers: Pick<ToolDefinition<typeof AskUserQuestionParams, AskUserQuestionResultDetails>, "renderCall" | "renderResult"> = {
8
6
  renderCall(args, theme, context) {
package/types.ts ADDED
@@ -0,0 +1,192 @@
1
+ import { Type } from "typebox";
2
+
3
+ const OptionSchema = Type.Object({
4
+ label: Type.String({ description: 'Option label. Put a recommended option first and append "(Recommended)".' }),
5
+ value: Type.Optional(Type.String({ description: "Machine-readable value; defaults to the label." })),
6
+ description: Type.Optional(Type.String({ description: "Additional context shown below the option." })),
7
+ });
8
+
9
+ export const AskUserQuestionParams = Type.Object({
10
+ question: Type.String({ description: "The question to ask. Ask only one question per call." }),
11
+ details: Type.Optional(Type.String({ description: "Background or instructions shown below the question." })),
12
+ options: Type.Optional(Type.Array(OptionSchema, {
13
+ description: "Choices for selection. Omit or pass an empty array for free-text input. An Other choice is always available.",
14
+ })),
15
+ multiSelect: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options when true." })),
16
+ });
17
+
18
+ export interface AskOption {
19
+ label: string;
20
+ value: string;
21
+ description?: string;
22
+ }
23
+
24
+ export interface DisplayOption extends AskOption {
25
+ id: string;
26
+ index?: number;
27
+ isOther?: boolean;
28
+ isSubmit?: boolean;
29
+ }
30
+
31
+ export interface TextAnswer {
32
+ type: "text";
33
+ label: string;
34
+ value: string;
35
+ }
36
+
37
+ export interface OptionAnswer {
38
+ type: "option";
39
+ label: string;
40
+ value: string;
41
+ index: number;
42
+ }
43
+
44
+ export interface OtherAnswer {
45
+ type: "other";
46
+ label: string;
47
+ value: string;
48
+ }
49
+
50
+ export type AskAnswer = TextAnswer | OptionAnswer | OtherAnswer;
51
+ export type AskUserQuestionStatus = "answered" | "cancelled" | "unavailable";
52
+ export type AskUserQuestionMode = "text" | "single-select" | "multi-select";
53
+
54
+ export interface AskUserQuestionResultDetails {
55
+ status: AskUserQuestionStatus;
56
+ question: string;
57
+ context?: string;
58
+ mode: AskUserQuestionMode;
59
+ answers: AskAnswer[];
60
+ message?: string;
61
+ }
62
+
63
+ export function normalizeOptions(
64
+ options: Array<{ label: string; value?: string; description?: string }> | undefined,
65
+ ): AskOption[] {
66
+ return (options || [])
67
+ .map((option) => ({
68
+ label: option.label.trim(),
69
+ value: option.value?.trim() || option.label.trim(),
70
+ description: option.description?.trim() || undefined,
71
+ }))
72
+ .filter((option) => option.label.length > 0);
73
+ }
74
+
75
+ export function isChineseContext(
76
+ question: string,
77
+ options?: Array<{ label?: string; description?: string }>,
78
+ context?: string,
79
+ ): boolean {
80
+ if (/[\u4e00-\u9fa5]/.test(question)) return true;
81
+ if (context && /[\u4e00-\u9fa5]/.test(context)) return true;
82
+ if (options && options.some((o) => (o.label && /[\u4e00-\u9fa5]/.test(o.label)) || (o.description && /[\u4e00-\u9fa5]/.test(o.description)))) {
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ export function getOtherLabel(options: AskOption[], isZh = false): string {
89
+ if (isZh) {
90
+ return options.some((option) => option.label === "其他(自定义输入)")
91
+ ? "自定义输入"
92
+ : "其他(自定义输入)";
93
+ }
94
+ return options.some((option) => option.label === "Other (custom input)")
95
+ ? "Custom input"
96
+ : "Other (custom input)";
97
+ }
98
+
99
+ function answerSortRank(answer: AskAnswer): number {
100
+ switch (answer.type) {
101
+ case "option":
102
+ return answer.index;
103
+ case "other":
104
+ return Number.MAX_SAFE_INTEGER - 1;
105
+ case "text":
106
+ return Number.MAX_SAFE_INTEGER;
107
+ }
108
+ }
109
+
110
+ export function sortAnswers(answers: AskAnswer[]): AskAnswer[] {
111
+ return [...answers].sort((a, b) => answerSortRank(a) - answerSortRank(b));
112
+ }
113
+
114
+ function formatAnswerForModel(answer: AskAnswer): string {
115
+ switch (answer.type) {
116
+ case "text":
117
+ return answer.label;
118
+ case "other":
119
+ return `Other: ${answer.label}`;
120
+ case "option":
121
+ return `${answer.index}. ${answer.label}`;
122
+ }
123
+ }
124
+
125
+ function buildStructuredResult(
126
+ status: AskUserQuestionStatus,
127
+ question: string,
128
+ mode: AskUserQuestionMode,
129
+ answers: AskAnswer[],
130
+ context?: string,
131
+ message?: string,
132
+ ): AskUserQuestionResultDetails {
133
+ return {
134
+ status,
135
+ question,
136
+ context,
137
+ mode,
138
+ answers,
139
+ message,
140
+ };
141
+ }
142
+
143
+ export function cancelledResult(question: string, mode: AskUserQuestionMode, context?: string) {
144
+ const isZh = isChineseContext(question, undefined, context);
145
+ const message = isZh ? "用户取消了本次提问" : "User cancelled the question.";
146
+ return {
147
+ content: [{ type: "text" as const, text: message }],
148
+ details: buildStructuredResult("cancelled", question, mode, [], context, message),
149
+ };
150
+ }
151
+
152
+ export function unavailableResult(question: string, mode: AskUserQuestionMode, message: string, context?: string) {
153
+ return {
154
+ content: [{ type: "text" as const, text: message }],
155
+ details: buildStructuredResult("unavailable", question, mode, [], context, message),
156
+ };
157
+ }
158
+
159
+ export function buildResult(
160
+ question: string,
161
+ context: string | undefined,
162
+ mode: AskUserQuestionMode,
163
+ answers: AskAnswer[],
164
+ ) {
165
+ const isZh = isChineseContext(question, answers, context);
166
+ let text: string;
167
+ if (mode === "text") {
168
+ const answer = answers[0];
169
+ if (isZh) {
170
+ text = answer.label.trim().length > 0
171
+ ? `用户输入回答: ${answer.label}`
172
+ : "用户提交了空回答";
173
+ } else {
174
+ text = answer.label.trim().length > 0
175
+ ? `User answered: ${answer.label}`
176
+ : "User submitted an empty answer";
177
+ }
178
+ } else if (mode === "single-select") {
179
+ text = isZh
180
+ ? `用户选择项: ${formatAnswerForModel(answers[0])}`
181
+ : `User selected: ${formatAnswerForModel(answers[0])}`;
182
+ } else {
183
+ text = isZh
184
+ ? `用户选择多项:\n${answers.map((answer) => `- ${formatAnswerForModel(answer)}`).join("\n")}`
185
+ : `User selected multiple:\n${answers.map((answer) => `- ${formatAnswerForModel(answer)}`).join("\n")}`;
186
+ }
187
+
188
+ return {
189
+ content: [{ type: "text" as const, text }],
190
+ details: buildStructuredResult("answered", question, mode, answers, context),
191
+ };
192
+ }
@@ -1,32 +0,0 @@
1
- # ask-u-questions
2
-
3
- Interactive single-choice, multiple-choice, and free-text questions for Pi. Registers the `ask_user_question` tool and serializes dialogs through a shared UI lock.
4
-
5
- ## Install
6
-
7
- ```sh
8
- pi install npm:@rol1n/ask-u-questions
9
- ```
10
-
11
- Also included in `@rol1n/pi-kit`. Enable one copy of the extension.
12
-
13
- ## Use
14
-
15
- The agent calls the tool with a question and optional choices:
16
-
17
- ```json
18
- {
19
- "question": "Which output format should I use?",
20
- "options": [
21
- { "label": "Markdown (Recommended)", "value": "markdown" },
22
- { "label": "JSON", "value": "json" }
23
- ],
24
- "multiSelect": false
25
- }
26
- ```
27
-
28
- Omit `options` or supply an empty array for free-text input. Set `multiSelect` only when several choices are expected. `details` adds supporting context; an Other choice is supplied automatically for choice questions.
29
-
30
- ## Requirements and behavior
31
-
32
- Use an interactive Pi terminal session for dialogs. Results distinguish answered, cancelled, and unavailable states, so a cancelled dialog is not an approval. No API credentials or package-specific settings are required.
@@ -1,53 +0,0 @@
1
- import type { AskAnswer, AskOption } from "./types.ts";
2
-
3
- export function normalizeOptions(
4
- options: Array<{ label: string; value?: string; description?: string }> | undefined,
5
- ): AskOption[] {
6
- return (options || [])
7
- .map((option) => ({
8
- label: option.label.trim(),
9
- value: option.value?.trim() || option.label.trim(),
10
- description: option.description?.trim() || undefined,
11
- }))
12
- .filter((option) => option.label.length > 0);
13
- }
14
-
15
- export function isChineseContext(
16
- question: string,
17
- options?: Array<{ label?: string; description?: string }>,
18
- context?: string,
19
- ): boolean {
20
- if (/[\u4e00-\u9fa5]/.test(question)) return true;
21
- if (context && /[\u4e00-\u9fa5]/.test(context)) return true;
22
- if (options && options.some((o) => (o.label && /[\u4e00-\u9fa5]/.test(o.label)) || (o.description && /[\u4e00-\u9fa5]/.test(o.description)))) {
23
- return true;
24
- }
25
- return false;
26
- }
27
-
28
- export function getOtherLabel(options: AskOption[], isZh = false): string {
29
- if (isZh) {
30
- return options.some((option) => option.label === "其他(自定义输入)")
31
- ? "自定义输入"
32
- : "其他(自定义输入)";
33
- }
34
- return options.some((option) => option.label === "Other (custom input)")
35
- ? "Custom input"
36
- : "Other (custom input)";
37
- }
38
-
39
- function answerSortRank(answer: AskAnswer): number {
40
- switch (answer.type) {
41
- case "option":
42
- return answer.index;
43
- case "other":
44
- return Number.MAX_SAFE_INTEGER - 1;
45
- case "text":
46
- return Number.MAX_SAFE_INTEGER;
47
- }
48
- }
49
-
50
- export function sortAnswers(answers: AskAnswer[]): AskAnswer[] {
51
- return [...answers].sort((a, b) => answerSortRank(a) - answerSortRank(b));
52
- }
53
-
@@ -1,83 +0,0 @@
1
- import type { AskAnswer, AskUserQuestionMode, AskUserQuestionResultDetails, AskUserQuestionStatus } from "./types.ts";
2
- import { isChineseContext } from "./answers.ts";
3
-
4
- function formatAnswerForModel(answer: AskAnswer): string {
5
- switch (answer.type) {
6
- case "text":
7
- return answer.label;
8
- case "other":
9
- return `Other: ${answer.label}`;
10
- case "option":
11
- return `${answer.index}. ${answer.label}`;
12
- }
13
- }
14
-
15
- function buildStructuredResult(
16
- status: AskUserQuestionStatus,
17
- question: string,
18
- mode: AskUserQuestionMode,
19
- answers: AskAnswer[],
20
- context?: string,
21
- message?: string,
22
- ): AskUserQuestionResultDetails {
23
- return {
24
- status,
25
- question,
26
- context,
27
- mode,
28
- answers,
29
- message,
30
- };
31
- }
32
-
33
- export function cancelledResult(question: string, mode: AskUserQuestionMode, context?: string) {
34
- const isZh = isChineseContext(question, undefined, context);
35
- const message = isZh ? "用户取消了本次提问" : "User cancelled the question.";
36
- return {
37
- content: [{ type: "text" as const, text: message }],
38
- details: buildStructuredResult("cancelled", question, mode, [], context, message),
39
- };
40
- }
41
-
42
- export function unavailableResult(question: string, mode: AskUserQuestionMode, message: string, context?: string) {
43
- return {
44
- content: [{ type: "text" as const, text: message }],
45
- details: buildStructuredResult("unavailable", question, mode, [], context, message),
46
- };
47
- }
48
-
49
- export function buildResult(
50
- question: string,
51
- context: string | undefined,
52
- mode: AskUserQuestionMode,
53
- answers: AskAnswer[],
54
- ) {
55
- const isZh = isChineseContext(question, answers, context);
56
- let text: string;
57
- if (mode === "text") {
58
- const answer = answers[0];
59
- if (isZh) {
60
- text = answer.label.trim().length > 0
61
- ? `用户输入回答: ${answer.label}`
62
- : "用户提交了空回答";
63
- } else {
64
- text = answer.label.trim().length > 0
65
- ? `User answered: ${answer.label}`
66
- : "User submitted an empty answer";
67
- }
68
- } else if (mode === "single-select") {
69
- text = isZh
70
- ? `用户选择项: ${formatAnswerForModel(answers[0])}`
71
- : `User selected: ${formatAnswerForModel(answers[0])}`;
72
- } else {
73
- text = isZh
74
- ? `用户选择多项:\n${answers.map((answer) => `- ${formatAnswerForModel(answer)}`).join("\n")}`
75
- : `User selected multiple:\n${answers.map((answer) => `- ${formatAnswerForModel(answer)}`).join("\n")}`;
76
- }
77
-
78
- return {
79
- content: [{ type: "text" as const, text }],
80
- details: buildStructuredResult("answered", question, mode, answers, context),
81
- };
82
- }
83
-
@@ -1,17 +0,0 @@
1
- import { Type } from "typebox";
2
-
3
- const OptionSchema = Type.Object({
4
- label: Type.String({ description: 'Option label. Put a recommended option first and append "(Recommended)".' }),
5
- value: Type.Optional(Type.String({ description: "Machine-readable value; defaults to the label." })),
6
- description: Type.Optional(Type.String({ description: "Additional context shown below the option." })),
7
- });
8
-
9
- export const AskUserQuestionParams = Type.Object({
10
- question: Type.String({ description: "The question to ask. Ask only one question per call." }),
11
- details: Type.Optional(Type.String({ description: "Background or instructions shown below the question." })),
12
- options: Type.Optional(Type.Array(OptionSchema, {
13
- description: "Choices for selection. Omit or pass an empty array for free-text input. An Other choice is always available.",
14
- })),
15
- multiSelect: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options when true." })),
16
- });
17
-
@@ -1,45 +0,0 @@
1
- export interface AskOption {
2
- label: string;
3
- value: string;
4
- description?: string;
5
- }
6
-
7
- export interface DisplayOption extends AskOption {
8
- id: string;
9
- index?: number;
10
- isOther?: boolean;
11
- isSubmit?: boolean;
12
- }
13
-
14
- export interface TextAnswer {
15
- type: "text";
16
- label: string;
17
- value: string;
18
- }
19
-
20
- export interface OptionAnswer {
21
- type: "option";
22
- label: string;
23
- value: string;
24
- index: number;
25
- }
26
-
27
- export interface OtherAnswer {
28
- type: "other";
29
- label: string;
30
- value: string;
31
- }
32
-
33
- export type AskAnswer = TextAnswer | OptionAnswer | OtherAnswer;
34
- export type AskUserQuestionStatus = "answered" | "cancelled" | "unavailable";
35
- export type AskUserQuestionMode = "text" | "single-select" | "multi-select";
36
-
37
- export interface AskUserQuestionResultDetails {
38
- status: AskUserQuestionStatus;
39
- question: string;
40
- context?: string;
41
- mode: AskUserQuestionMode;
42
- answers: AskAnswer[];
43
- message?: string;
44
- }
45
-
@@ -1,24 +0,0 @@
1
- const SHARED_UI_LOCK_KEY = "__piSharedUiLock";
2
- function getSharedUiLock(): { withLock<T>(fn: () => T | Promise<T>): Promise<T> } {
3
- const g = globalThis as any;
4
- if (!g[SHARED_UI_LOCK_KEY]) {
5
- let chain: Promise<void> = Promise.resolve();
6
- g[SHARED_UI_LOCK_KEY] = {
7
- withLock<T>(fn: () => T | Promise<T>): Promise<T> {
8
- const prev = chain;
9
- let release: () => void;
10
- chain = new Promise<void>((r) => {
11
- release = r;
12
- });
13
- return prev.then(fn).finally(() => release!());
14
- },
15
- };
16
- }
17
- return g[SHARED_UI_LOCK_KEY];
18
- }
19
- const sharedUiLock = getSharedUiLock();
20
-
21
- export function withUILock<T>(fn: () => Promise<T>): Promise<T> {
22
- return sharedUiLock.withLock(fn);
23
- }
24
-