@oh-my-pi/user-prompt 0.3.0 → 0.5.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 +23 -67
- package/package.json +15 -4
- package/tools/user-prompt/index.ts +185 -157
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Interactive user prompting tool for gathering user input during agent execution.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
omp install oh-my-pi/
|
|
8
|
+
omp install @oh-my-pi/user-prompt
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Tool
|
|
@@ -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
|
|
69
|
-
|
|
70
|
-
| `question` | string
|
|
71
|
-
| `options`
|
|
72
|
-
| `
|
|
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 `
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Interactive user prompting tool for gathering user input during execution",
|
|
5
|
-
"keywords": [
|
|
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
|
-
{
|
|
21
|
+
{
|
|
22
|
+
"src": "tools/user-prompt/index.ts",
|
|
23
|
+
"dest": "agent/tools/user-prompt/index.ts"
|
|
24
|
+
}
|
|
16
25
|
]
|
|
17
26
|
},
|
|
18
|
-
"files": [
|
|
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 {
|
|
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
|
-
|
|
33
|
+
label: Type.String({ description: "Display label for this option" }),
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
const UserPromptParams = Type.Object({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
return new Text(text, 0, 0);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
231
259
|
|
|
232
|
-
|
|
260
|
+
return tool;
|
|
233
261
|
};
|
|
234
262
|
|
|
235
263
|
export default factory;
|