@oh-my-pi/user-prompt 0.3.0 → 0.4.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/README.md CHANGED
@@ -19,62 +19,18 @@ Asks the user questions during execution and returns their response. Useful for:
19
19
  - Getting decisions on implementation choices
20
20
  - Offering choices about what direction to take
21
21
 
22
- ## Features
23
-
24
- ### Enhanced UI (when available)
25
-
26
- The plugin provides custom TUI components that integrate directly into pi's interface:
27
-
28
- **Single-select with inline "Other" input:**
29
- ```
30
- ─────────────────────────────────────────────
31
- Which database would you like to use?
32
-
33
- → PostgreSQL (Recommended)
34
- MySQL
35
- SQLite
36
- MongoDB
37
- Other (type your own)
38
-
39
- ↑↓ navigate · enter select · esc cancel
40
- ─────────────────────────────────────────────
41
- ```
42
-
43
- When "Other" is selected, an inline text input appears - no separate dialog needed.
44
-
45
- **Multi-select with checkboxes:**
46
- ```
47
- ─────────────────────────────────────────────
48
- Which features should I implement?
49
-
50
- → [X] Authentication
51
- [X] API endpoints
52
- [ ] Database models
53
- [ ] Unit tests
54
- [ ] Documentation
55
-
56
- ↑↓ navigate · space toggle · enter confirm · esc cancel
57
- ─────────────────────────────────────────────
58
- ```
59
-
60
- Space toggles selection, Enter confirms. Selected items show `[X]` in green with white text.
61
-
62
- ### Fallback Mode
63
-
64
- If the enhanced UI cannot be loaded, the plugin gracefully falls back to using pi's built-in `select()` and `input()` methods.
65
-
66
22
  ## Parameters
67
23
 
68
- | Parameter | Type | Required | Description |
69
- |-----------|------|----------|-------------|
70
- | `question` | string | Yes | The question to ask the user |
71
- | `options` | array | Yes | Array of `{label: string}` options to present |
72
- | `multiSelect` | boolean | No | Allow multiple selections (default: false) |
24
+ | Parameter | Type | Required | Description |
25
+ | ---------- | ------- | -------- | --------------------------------------------- |
26
+ | `question` | string | Yes | The question to ask the user |
27
+ | `options` | array | Yes | Array of `{label: string}` options to present |
28
+ | `multi` | boolean | No | Allow multiple selections (default: false) |
73
29
 
74
30
  ## Usage Notes
75
31
 
76
32
  - Users can always select "Other" to provide custom text input
77
- - Use `multiSelect: true` to allow multiple answers to be selected
33
+ - Use `multi: true` to allow multiple answers to be selected
78
34
  - If you recommend a specific option, make that the first option and add "(Recommended)" at the end of the label
79
35
 
80
36
  ## Examples
@@ -83,13 +39,13 @@ If the enhanced UI cannot be loaded, the plugin gracefully falls back to using p
83
39
 
84
40
  ```json
85
41
  {
86
- "question": "Which database would you like to use?",
87
- "options": [
88
- {"label": "PostgreSQL (Recommended)"},
89
- {"label": "MySQL"},
90
- {"label": "SQLite"},
91
- {"label": "MongoDB"}
92
- ]
42
+ "question": "Which database would you like to use?",
43
+ "options": [
44
+ { "label": "PostgreSQL (Recommended)" },
45
+ { "label": "MySQL" },
46
+ { "label": "SQLite" },
47
+ { "label": "MongoDB" }
48
+ ]
93
49
  }
94
50
  ```
95
51
 
@@ -97,15 +53,15 @@ If the enhanced UI cannot be loaded, the plugin gracefully falls back to using p
97
53
 
98
54
  ```json
99
55
  {
100
- "question": "Which features should I implement?",
101
- "options": [
102
- {"label": "Authentication"},
103
- {"label": "API endpoints"},
104
- {"label": "Database models"},
105
- {"label": "Unit tests"},
106
- {"label": "Documentation"}
107
- ],
108
- "multiSelect": true
56
+ "question": "Which features should I implement?",
57
+ "options": [
58
+ { "label": "Authentication" },
59
+ { "label": "API endpoints" },
60
+ { "label": "Database models" },
61
+ { "label": "Unit tests" },
62
+ { "label": "Documentation" }
63
+ ],
64
+ "multi": true
109
65
  }
110
66
  ```
111
67
 
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@oh-my-pi/user-prompt",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Interactive user prompting tool for gathering user input during execution",
5
- "keywords": ["omp-plugin", "user-prompt", "interactive", "questions", "input"],
5
+ "keywords": [
6
+ "omp-plugin",
7
+ "user-prompt",
8
+ "interactive",
9
+ "questions",
10
+ "input"
11
+ ],
6
12
  "author": "Can Bölük <me@can.ac>",
7
13
  "license": "MIT",
8
14
  "repository": {
@@ -12,8 +18,13 @@
12
18
  },
13
19
  "omp": {
14
20
  "install": [
15
- { "src": "tools/user-prompt/index.ts", "dest": "agent/tools/user-prompt/index.ts" }
21
+ {
22
+ "src": "tools/user-prompt/index.ts",
23
+ "dest": "agent/tools/user-prompt/index.ts"
24
+ }
16
25
  ]
17
26
  },
18
- "files": ["tools"]
27
+ "files": [
28
+ "tools"
29
+ ]
19
30
  }
@@ -17,7 +17,11 @@
17
17
 
18
18
  import { Type } from "@sinclair/typebox";
19
19
  import { Text } from "@mariozechner/pi-tui";
20
- import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
20
+ import type {
21
+ CustomAgentTool,
22
+ CustomToolFactory,
23
+ ToolAPI,
24
+ } from "@mariozechner/pi-coding-agent";
21
25
 
22
26
  // =============================================================================
23
27
  // Tool Definition
@@ -26,27 +30,29 @@ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/
26
30
  const OTHER_OPTION = "Other (type your own)";
27
31
 
28
32
  const OptionItem = Type.Object({
29
- label: Type.String({ description: "Display label for this option" }),
33
+ label: Type.String({ description: "Display label for this option" }),
30
34
  });
31
35
 
32
36
  const UserPromptParams = Type.Object({
33
- question: Type.String({ description: "The question to ask the user" }),
34
- options: Type.Array(OptionItem, {
35
- description: "Available options for the user to choose from.",
36
- minItems: 1,
37
- }),
38
- multi: Type.Optional(Type.Boolean({
37
+ question: Type.String({ description: "The question to ask the user" }),
38
+ options: Type.Array(OptionItem, {
39
+ description: "Available options for the user to choose from.",
40
+ minItems: 1,
41
+ }),
42
+ multi: Type.Optional(
43
+ Type.Boolean({
39
44
  description: "Allow multiple options to be selected (default: false)",
40
45
  default: false,
41
- })),
46
+ }),
47
+ ),
42
48
  });
43
49
 
44
50
  interface UserPromptDetails {
45
- question: string;
46
- options: string[];
47
- multi: boolean;
48
- selectedOptions: string[];
49
- customInput?: string;
51
+ question: string;
52
+ options: string[];
53
+ multi: boolean;
54
+ selectedOptions: string[];
55
+ customInput?: string;
50
56
  }
51
57
 
52
58
  const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
@@ -79,157 +85,179 @@ assistant: Uses the user_prompt tool:
79
85
  </example>`;
80
86
 
81
87
  const factory: CustomToolFactory = (pi: ToolAPI) => {
82
- const tool: CustomAgentTool<typeof UserPromptParams, UserPromptDetails> = {
83
- name: "user_prompt",
84
- label: "User Prompt",
85
- description: DESCRIPTION,
86
- parameters: UserPromptParams,
87
-
88
- async execute(_toolCallId, params, _signal, _onUpdate) {
89
- const { question, options, multi = false } = params;
90
- const optionLabels = options.map((o) => o.label);
91
-
92
- if (!pi.hasUI) {
93
- return {
94
- content: [{ type: "text", text: "Error: User prompt requires interactive mode" }],
95
- details: { question, options: optionLabels, multi, selectedOptions: [] },
96
- };
97
- }
98
-
99
- let selectedOptions: string[] = [];
100
- let customInput: string | undefined;
101
-
102
- if (multi) {
103
- // Multi-select: show checkboxes in the label to indicate selection state
104
- const DONE = "✓ Done selecting";
105
- const selected = new Set<string>();
106
-
107
- while (true) {
108
- // Build options with checkbox indicators
109
- const opts: string[] = [];
110
-
111
- // Add "Done" option if any selected
112
- if (selected.size > 0) {
113
- opts.push(DONE);
114
- }
115
-
116
- // Add all options with [X] or [ ] prefix
117
- for (const opt of optionLabels) {
118
- const checkbox = selected.has(opt) ? "[X]" : "[ ]";
119
- opts.push(`${checkbox} ${opt}`);
120
- }
121
-
122
- // Add "Other" option
123
- opts.push(OTHER_OPTION);
124
-
125
- const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
126
- const choice = await pi.ui.select(`${prefix}${question}`, opts);
127
-
128
- if (choice === null || choice === DONE) break;
129
-
130
- if (choice === OTHER_OPTION) {
131
- const input = await pi.ui.input("Enter your response:");
132
- if (input) customInput = input;
133
- break;
134
- }
135
-
136
- // Toggle selection - extract the actual option name
137
- const optMatch = choice.match(/^\[.\] (.+)$/);
138
- if (optMatch) {
139
- const opt = optMatch[1];
140
- if (selected.has(opt)) {
141
- selected.delete(opt);
142
- } else {
143
- selected.add(opt);
144
- }
145
- }
146
- }
147
- selectedOptions = Array.from(selected);
148
- } else {
149
- // Single select with "Other" option
150
- const choice = await pi.ui.select(question, [...optionLabels, OTHER_OPTION]);
151
- if (choice === OTHER_OPTION) {
152
- const input = await pi.ui.input("Enter your response:");
153
- if (input) customInput = input;
154
- } else if (choice) {
155
- selectedOptions = [choice];
156
- }
157
- }
88
+ const tool: CustomAgentTool<typeof UserPromptParams, UserPromptDetails> = {
89
+ name: "user_prompt",
90
+ label: "User Prompt",
91
+ description: DESCRIPTION,
92
+ parameters: UserPromptParams,
93
+
94
+ async execute(_toolCallId, params, _signal, _onUpdate) {
95
+ const { question, options, multi = false } = params;
96
+ const optionLabels = options.map((o) => o.label);
158
97
 
159
- const details: UserPromptDetails = {
98
+ if (!pi.hasUI) {
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: "Error: User prompt requires interactive mode",
104
+ },
105
+ ],
106
+ details: {
160
107
  question,
161
108
  options: optionLabels,
162
109
  multi,
163
- selectedOptions,
164
- customInput,
165
- };
166
-
167
- let responseText: string;
168
- if (customInput) {
169
- responseText = `User provided custom input: ${customInput}`;
170
- } else if (selectedOptions.length > 0) {
171
- responseText = multi
172
- ? `User selected: ${selectedOptions.join(", ")}`
173
- : `User selected: ${selectedOptions[0]}`;
174
- } else {
175
- responseText = "User cancelled the selection";
176
- }
177
-
178
- return { content: [{ type: "text", text: responseText }], details };
179
- },
180
-
181
- renderCall(args, t) {
182
- if (!args.question) {
183
- return new Text(t.fg("error", "user_prompt: no question provided"), 0, 0);
184
- }
185
-
186
- const multiTag = args.multi ? t.fg("muted", " [multi-select]") : "";
187
- let text = t.fg("toolTitle", "? ") + t.fg("accent", args.question) + multiTag;
188
-
189
- if (args.options?.length) {
190
- for (const opt of args.options) {
191
- text += "\n" + t.fg("dim", " ○ ") + t.fg("muted", opt.label);
192
- }
193
- text += "\n" + t.fg("dim", " ○ ") + t.fg("muted", "Other (custom input)");
194
- }
195
-
196
- return new Text(text, 0, 0);
197
- },
198
-
199
- renderResult(result, { expanded }, t) {
200
- const { details } = result;
201
- if (!details) {
202
- const txt = result.content[0];
203
- return new Text(txt?.type === "text" ? txt.text : "", 0, 0);
204
- }
205
-
206
- let text = t.fg("toolTitle", "? ") + t.fg("accent", details.question);
207
-
208
- if (details.customInput) {
209
- // Custom input provided
210
- text += "\n" + t.fg("dim", " ⎿ ") + t.fg("success", details.customInput);
211
- } else if (details.selectedOptions.length > 0) {
212
- // Show only selected options
213
- const selected = details.selectedOptions;
214
- if (selected.length === 1) {
215
- text += "\n" + t.fg("dim", " ⎿ ") + t.fg("success", selected[0]);
110
+ selectedOptions: [],
111
+ },
112
+ };
113
+ }
114
+
115
+ let selectedOptions: string[] = [];
116
+ let customInput: string | undefined;
117
+
118
+ if (multi) {
119
+ // Multi-select: show checkboxes in the label to indicate selection state
120
+ const DONE = "✓ Done selecting";
121
+ const selected = new Set<string>();
122
+
123
+ while (true) {
124
+ // Build options with checkbox indicators
125
+ const opts: string[] = [];
126
+
127
+ // Add "Done" option if any selected
128
+ if (selected.size > 0) {
129
+ opts.push(DONE);
130
+ }
131
+
132
+ // Add all options with [X] or [ ] prefix
133
+ for (const opt of optionLabels) {
134
+ const checkbox = selected.has(opt) ? "[X]" : "[ ]";
135
+ opts.push(`${checkbox} ${opt}`);
136
+ }
137
+
138
+ // Add "Other" option
139
+ opts.push(OTHER_OPTION);
140
+
141
+ const prefix =
142
+ selected.size > 0 ? `(${selected.size} selected) ` : "";
143
+ const choice = await pi.ui.select(`${prefix}${question}`, opts);
144
+
145
+ if (choice === null || choice === DONE) break;
146
+
147
+ if (choice === OTHER_OPTION) {
148
+ const input = await pi.ui.input("Enter your response:");
149
+ if (input) customInput = input;
150
+ break;
151
+ }
152
+
153
+ // Toggle selection - extract the actual option name
154
+ const optMatch = choice.match(/^\[.\] (.+)$/);
155
+ if (optMatch) {
156
+ const opt = optMatch[1];
157
+ if (selected.has(opt)) {
158
+ selected.delete(opt);
216
159
  } else {
217
- // Multiple selections
218
- for (let i = 0; i < selected.length; i++) {
219
- const isLast = i === selected.length - 1;
220
- const branch = isLast ? "└─" : "├─";
221
- text += "\n" + t.fg("dim", ` ${branch} `) + t.fg("success", selected[i]);
222
- }
160
+ selected.add(opt);
223
161
  }
224
- } else {
225
- text += "\n" + t.fg("dim", " ⎿ ") + t.fg("warning", "Cancelled");
226
- }
162
+ }
163
+ }
164
+ selectedOptions = Array.from(selected);
165
+ } else {
166
+ // Single select with "Other" option
167
+ const choice = await pi.ui.select(question, [
168
+ ...optionLabels,
169
+ OTHER_OPTION,
170
+ ]);
171
+ if (choice === OTHER_OPTION) {
172
+ const input = await pi.ui.input("Enter your response:");
173
+ if (input) customInput = input;
174
+ } else if (choice) {
175
+ selectedOptions = [choice];
176
+ }
177
+ }
178
+
179
+ const details: UserPromptDetails = {
180
+ question,
181
+ options: optionLabels,
182
+ multi,
183
+ selectedOptions,
184
+ customInput,
185
+ };
186
+
187
+ let responseText: string;
188
+ if (customInput) {
189
+ responseText = `User provided custom input: ${customInput}`;
190
+ } else if (selectedOptions.length > 0) {
191
+ responseText = multi
192
+ ? `User selected: ${selectedOptions.join(", ")}`
193
+ : `User selected: ${selectedOptions[0]}`;
194
+ } else {
195
+ responseText = "User cancelled the selection";
196
+ }
197
+
198
+ return { content: [{ type: "text", text: responseText }], details };
199
+ },
200
+
201
+ renderCall(args, t) {
202
+ if (!args.question) {
203
+ return new Text(
204
+ t.fg("error", "user_prompt: no question provided"),
205
+ 0,
206
+ 0,
207
+ );
208
+ }
209
+
210
+ const multiTag = args.multi ? t.fg("muted", " [multi-select]") : "";
211
+ let text =
212
+ t.fg("toolTitle", "? ") + t.fg("accent", args.question) + multiTag;
213
+
214
+ if (args.options?.length) {
215
+ for (const opt of args.options) {
216
+ text += "\n" + t.fg("dim", " ○ ") + t.fg("muted", opt.label);
217
+ }
218
+ text +=
219
+ "\n" + t.fg("dim", " ○ ") + t.fg("muted", "Other (custom input)");
220
+ }
221
+
222
+ return new Text(text, 0, 0);
223
+ },
224
+
225
+ renderResult(result, { expanded }, t) {
226
+ const { details } = result;
227
+ if (!details) {
228
+ const txt = result.content[0];
229
+ return new Text(txt?.type === "text" ? txt.text : "", 0, 0);
230
+ }
231
+
232
+ let text = t.fg("toolTitle", "? ") + t.fg("accent", details.question);
233
+
234
+ if (details.customInput) {
235
+ // Custom input provided
236
+ text +=
237
+ "\n" + t.fg("dim", " ⎿ ") + t.fg("success", details.customInput);
238
+ } else if (details.selectedOptions.length > 0) {
239
+ // Show only selected options
240
+ const selected = details.selectedOptions;
241
+ if (selected.length === 1) {
242
+ text += "\n" + t.fg("dim", " ⎿ ") + t.fg("success", selected[0]);
243
+ } else {
244
+ // Multiple selections
245
+ for (let i = 0; i < selected.length; i++) {
246
+ const isLast = i === selected.length - 1;
247
+ const branch = isLast ? "└─" : "├─";
248
+ text +=
249
+ "\n" + t.fg("dim", ` ${branch} `) + t.fg("success", selected[i]);
250
+ }
251
+ }
252
+ } else {
253
+ text += "\n" + t.fg("dim", " ⎿ ") + t.fg("warning", "Cancelled");
254
+ }
227
255
 
228
- return new Text(text, 0, 0);
229
- },
230
- };
256
+ return new Text(text, 0, 0);
257
+ },
258
+ };
231
259
 
232
- return tool;
260
+ return tool;
233
261
  };
234
262
 
235
263
  export default factory;