@hachej/boring-ask-user 0.1.41 → 0.1.42
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 +67 -213
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,266 +1,120 @@
|
|
|
1
1
|
# @hachej/boring-ask-user
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lets the coding agent ask the user a structured, typed question and block until
|
|
4
|
+
the answer comes back. The question renders as a form in the **Questions**
|
|
5
|
+
workbench pane; the agent's `ask_user` tool resolves once the user submits or
|
|
6
|
+
cancels.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
## What it does
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
- Adds an `ask_user` agent tool that emits a typed form schema (text, textarea,
|
|
11
|
+
select, multiselect, checkbox, radio, number) and waits for a validated answer.
|
|
12
|
+
- Contributes a **Questions** center pane that renders the pending question,
|
|
13
|
+
validates input (Zod), and posts the answer back.
|
|
14
|
+
- Registers a workspace **blocker** while a question is pending, so the
|
|
15
|
+
composer surfaces "Answer the question to continue" with open/cancel actions.
|
|
16
|
+
- Persists pending questions to a file store that survives agent restarts.
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
## What it contributes to the workspace
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## TL;DR
|
|
20
|
-
|
|
21
|
-
**The Problem**: Your agent wants to confirm destructive actions, collect missing parameters, or branch on user choice — but it has no way to pause and wait for a structured, typed answer. Chat messages don't provide validated form input.
|
|
22
|
-
|
|
23
|
-
**The Solution**: The `ask_user` tool lets the agent emit a typed schema (text, textarea, select, multiselect, checkbox, radio, number fields). It opens a form panel in the workbench. The user fills it in. The agent unblocks with a typed `Record<string, AskUserAnswerValue>` response.
|
|
20
|
+
| Surface | Detail |
|
|
21
|
+
|---------|--------|
|
|
22
|
+
| Provider | `ask-user.provider` — owns the per-app questions runtime + pending store |
|
|
23
|
+
| Panel | `ask-user.questions` ("Questions"), `placement: "center"`, chromeless |
|
|
24
|
+
| Surface resolver | kind `questions` (`ASK_USER_SURFACE_KIND`) → opens the panel |
|
|
25
|
+
| Agent tool | `ask_user` (blocking; resolves `answered` / `cancelled`) |
|
|
26
|
+
| HTTP route | `POST /api/v1/questions/commands` (submit + cancel commands) |
|
|
27
|
+
| Pi prompt | `pi.systemPrompt` nudges the agent to use `ask_user` over chat roleplay |
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
## How it's wired
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
| **Typed form fields** | `text`, `textarea`, `select`, `multiselect`, `checkbox`, `radio`, `number` — validated with Zod |
|
|
30
|
-
| **Blocking tool** | Agent calls `ask_user` and waits — resolves with `answered` or `cancelled` status |
|
|
31
|
-
| **Workbench panel** | Questions pane with submit/cancel buttons, form validation, and empty state |
|
|
32
|
-
| **Bridge pubsub** | SSE-based communication between agent backend and frontend panel (HTTP fallback) |
|
|
33
|
-
| **File-backed store** | Persistence via `FileAskUserStore`; swap in your own `AskUserStore` for DB-backed storage |
|
|
34
|
-
| **Surface resolver** | Agent opens the questions panel via `openSurface` with `ASK_USER_SURFACE_KIND` |
|
|
31
|
+
Both entrypoints have a default export, so the package works as a
|
|
32
|
+
`defaultPluginPackages` entry as well as via the named factories.
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## Quick Example
|
|
39
|
-
|
|
40
|
-
**Frontend (workbench):**
|
|
34
|
+
**Front** — pass the `askUserPlugin` const directly to `WorkspaceProvider`:
|
|
41
35
|
|
|
42
36
|
```ts
|
|
43
37
|
import { askUserPlugin } from "@hachej/boring-ask-user/front"
|
|
44
|
-
//
|
|
38
|
+
// <WorkspaceProvider plugins={[askUserPlugin, ...]}>
|
|
45
39
|
```
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
It bundles a provider, so compose it statically in the app shell rather than
|
|
42
|
+
relying on dynamic hot-load.
|
|
48
43
|
|
|
49
|
-
**Server
|
|
44
|
+
**Server** — register the server plugin with the agent runtime:
|
|
50
45
|
|
|
51
46
|
```ts
|
|
52
47
|
import { createAskUserServerPlugin } from "@hachej/boring-ask-user/server"
|
|
53
48
|
|
|
54
|
-
const
|
|
55
|
-
//
|
|
49
|
+
const plugin = createAskUserServerPlugin({
|
|
50
|
+
workspaceRoot, // required unless you pass your own `store`
|
|
51
|
+
bridge, // UiBridge — needed for live SSE state publishing
|
|
52
|
+
store, // optional; defaults to FileAskUserStore
|
|
53
|
+
sessionId, // optional string | () => string
|
|
54
|
+
})
|
|
56
55
|
```
|
|
57
56
|
|
|
58
|
-
The agent
|
|
57
|
+
The agent then calls `ask_user` with a `{ title, context?, schema }` payload:
|
|
59
58
|
|
|
60
59
|
```ts
|
|
61
60
|
{
|
|
62
61
|
title: "Deploy target?",
|
|
63
|
-
context: "Choose the environment.",
|
|
64
62
|
schema: {
|
|
65
63
|
wireVersion: 1,
|
|
66
64
|
fields: [
|
|
67
65
|
{ type: "select", name: "env", label: "Environment", options: [
|
|
68
66
|
{ value: "staging", label: "Staging" },
|
|
69
67
|
{ value: "production", label: "Production" },
|
|
70
|
-
]},
|
|
68
|
+
] },
|
|
71
69
|
],
|
|
72
70
|
},
|
|
73
71
|
}
|
|
74
72
|
```
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
The pane opens, the user submits, and the tool resolves with
|
|
75
|
+
`{ status: "answered", answer: { values: { env: "production" } } }`.
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
## Field types
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
`text`, `textarea`, `select`, `multiselect`, `checkbox`, `radio`, `number`.
|
|
80
|
+
Every field needs `name` (keys into the answer) and `label`; common optionals
|
|
81
|
+
are `required`, `helpText`, `defaultValue`. `select`/`multiselect`/`radio` take
|
|
82
|
+
`options: { value, label, description? }[]`. Schema limits (max 8 fields, 50
|
|
83
|
+
options/field, etc.) live in `ASK_USER_SCHEMA_LIMITS`.
|
|
81
84
|
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
| `text` | `string` | `placeholder`, `defaultValue`, `minLength`, `maxLength`, `pattern` |
|
|
85
|
-
| `textarea` | `string` (multi-line) | `placeholder`, `defaultValue`, `minLength`, `maxLength` |
|
|
86
|
-
| `select` | `string` (single) | `options: AskUserOption[]`, `defaultValue` |
|
|
87
|
-
| `multiselect` | `string[]` | `options`, `defaultValue[]`, `minSelections`, `maxSelections` |
|
|
88
|
-
| `checkbox` | `boolean` | `defaultValue` |
|
|
89
|
-
| `radio` | `string` (single) | `options`, `defaultValue` |
|
|
90
|
-
| `number` | `number` | `min`, `max`, `step`, `integer`, `defaultValue` |
|
|
85
|
+
Answer values are `string | string[] | boolean | number | null`, keyed by field
|
|
86
|
+
name under `answer.values`.
|
|
91
87
|
|
|
92
|
-
|
|
88
|
+
## Config & storage
|
|
93
89
|
|
|
94
|
-
|
|
90
|
+
The default `FileAskUserStore` persists to
|
|
91
|
+
`${workspaceRoot}/.boring/ask-user.json`. Implement the `AskUserStore`
|
|
92
|
+
interface and pass it as `store` for DB-backed persistence. The store enforces
|
|
93
|
+
one pending question per session (`PENDING_EXISTS` on a duplicate).
|
|
95
94
|
|
|
96
|
-
|
|
95
|
+
## Package surfaces
|
|
97
96
|
|
|
98
|
-
|
|
97
|
+
| Import | Env | Exports |
|
|
98
|
+
|--------|-----|---------|
|
|
99
|
+
| `@hachej/boring-ask-user/front` | Browser | `askUserPlugin` (default + named) |
|
|
100
|
+
| `@hachej/boring-ask-user/server` | Node | `createAskUserServerPlugin`, `AskUserStore`, `FileAskUserStore`, runtime/bridge/route helpers; default export = `defaultPluginPackages` adapter |
|
|
101
|
+
| `@hachej/boring-ask-user/shared` | Any | schema/types/constants/error codes |
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
type AskUserAnswerValue = string | string[] | boolean | number | null
|
|
102
|
-
|
|
103
|
-
type AskUserAnswer = {
|
|
104
|
-
questionId: string
|
|
105
|
-
sessionId: string
|
|
106
|
-
values: Record<string, AskUserAnswerValue> // keyed by field name
|
|
107
|
-
submittedAt: string
|
|
108
|
-
}
|
|
109
|
-
```
|
|
103
|
+
## Notes
|
|
110
104
|
|
|
111
|
-
|
|
105
|
+
- No file-upload, rich-text, or date-picker fields.
|
|
106
|
+
- If the agent process restarts mid-question, the question stays pending in the
|
|
107
|
+
file store; the front pane re-reads pending state on focus / agent stream
|
|
108
|
+
activity.
|
|
112
109
|
|
|
113
|
-
##
|
|
110
|
+
## Validation
|
|
114
111
|
|
|
115
112
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pnpm
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## Architecture
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
Agent calls ask_user tool
|
|
127
|
-
│
|
|
128
|
-
▼
|
|
129
|
-
┌─────────────────────────┐
|
|
130
|
-
│ createAskUserServer │
|
|
131
|
-
│ Plugin │
|
|
132
|
-
│ ├── Creates question │
|
|
133
|
-
│ ├── Stores in Store │
|
|
134
|
-
│ └── Posts to UiBridge │
|
|
135
|
-
└───────────┬─────────────┘
|
|
136
|
-
│ SSE / HTTP
|
|
137
|
-
▼
|
|
138
|
-
┌─────────────────────────┐
|
|
139
|
-
│ askUserPlugin (front) │
|
|
140
|
-
│ ├── Receives question │
|
|
141
|
-
│ ├── Opens panel │
|
|
142
|
-
│ ├── Renders form │
|
|
143
|
-
│ │ (schema-driven) │
|
|
144
|
-
│ └── Posts answer │
|
|
145
|
-
└───────────┬─────────────┘
|
|
146
|
-
│ POST /api/v1/questions/answer
|
|
147
|
-
▼
|
|
148
|
-
┌─────────────────────────┐
|
|
149
|
-
│ Questions Bridge │
|
|
150
|
-
│ ├── Validates answer │
|
|
151
|
-
│ ├── Resolves promise │
|
|
152
|
-
│ └── Agent continues │
|
|
153
|
-
└─────────────────────────┘
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Package Surfaces
|
|
157
|
-
|
|
158
|
-
| Import | Environment | What You Get |
|
|
159
|
-
|--------|-------------|--------------|
|
|
160
|
-
| `@hachej/boring-ask-user/front` | Browser | `askUserPlugin` const — workbench provider + panel + surface resolver |
|
|
161
|
-
| `@hachej/boring-ask-user/server` | Node | `createAskUserServerPlugin()` — agent tool + HTTP routes + file store |
|
|
162
|
-
| `@hachej/boring-ask-user/shared` | Any | `AskUserField`, `AskUserFormSchema`, `AskUserToolInput`, error codes, constants |
|
|
163
|
-
|
|
164
|
-
### AskUserStore Interface
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
interface AskUserStore {
|
|
168
|
-
getPending(sessionId: string): Promise<AskUserQuestion | null>
|
|
169
|
-
getByQuestionId(questionId: string): Promise<AskUserQuestion | null>
|
|
170
|
-
createPending(question: AskUserQuestion): Promise<void>
|
|
171
|
-
answer(questionId: string, answer: AskUserAnswer): Promise<void>
|
|
172
|
-
cancel(questionId: string): Promise<void>
|
|
173
|
-
markAbandoned(questionId: string): Promise<void>
|
|
174
|
-
clearPending(sessionId: string): Promise<void>
|
|
175
|
-
appendTranscriptEvent(event: AskUserTranscriptEvent): Promise<void>
|
|
176
|
-
listTranscriptEvents(sessionId: string): Promise<AskUserTranscriptEvent[]>
|
|
177
|
-
getTranscriptEventsForQuestion(questionId: string): Promise<AskUserTranscriptEvent[]>
|
|
178
|
-
subscribe(listener: (change: AskUserStoreChange) => void): () => void
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
The default `FileAskUserStore` persists to `${workspaceRoot}/.boring/ask-user.json`. Provide your own for DB-backed storage:
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
import { createAskUserServerPlugin } from "@hachej/boring-ask-user/server"
|
|
186
|
-
|
|
187
|
-
const plugin = createAskUserServerPlugin({
|
|
188
|
-
workspaceRoot: "/path/to/workspace",
|
|
189
|
-
bridge, // UiBridge for SSE pubsub
|
|
190
|
-
store: myDBStore, // optional — default is FileAskUserStore
|
|
191
|
-
})
|
|
113
|
+
pnpm --filter @hachej/boring-ask-user typecheck
|
|
114
|
+
pnpm --filter @hachej/boring-ask-user test
|
|
115
|
+
pnpm --filter @hachej/boring-ask-user build
|
|
192
116
|
```
|
|
193
117
|
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## How @hachej/boring-ask-user Compares
|
|
197
|
-
|
|
198
|
-
| Feature | @hachej/boring-ask-user | Chat-based answers | MCP human-in-loop |
|
|
199
|
-
|---------|-------------------------|--------------------|--------------------|
|
|
200
|
-
| Structured input | ✅ 7 typed fields with Zod validation | ❌ Free text only | ⚠️ Varies |
|
|
201
|
-
| Blocking | ✅ Tool waits for answer | ❌ Agent parses chat | ⚠️ Stdin only |
|
|
202
|
-
| Workbench UI | ✅ Form panel with validation | ✅ Chat bubble | ❌ Terminal prompt |
|
|
203
|
-
| Cancellation | ✅ User can cancel | ⚠️ Just type something else | ⚠️ Ctrl+C |
|
|
204
|
-
| Multi-field | ✅ Multiple fields in one question | ❌ One-at-a-time | ❌ |
|
|
205
|
-
| Transcript | ✅ Full event log per question | ❌ None | ❌ |
|
|
206
|
-
|
|
207
|
-
**When to use @hachej/boring-ask-user:**
|
|
208
|
-
- Your agent needs structured, validated answers (not free-text chat)
|
|
209
|
-
- You want a proper form UI in the workbench
|
|
210
|
-
- You're building approval gates or environment selectors
|
|
211
|
-
|
|
212
|
-
**When it might not fit:**
|
|
213
|
-
- Free-text chat answers are sufficient (just ask in the chat)
|
|
214
|
-
- You need real-time collaborative editing (not supported)
|
|
215
|
-
- You need terminal-based stdin/stdout (use direct prompt input)
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## Troubleshooting
|
|
220
|
-
|
|
221
|
-
| Error | Cause | Fix |
|
|
222
|
-
|-------|-------|-----|
|
|
223
|
-
| `ask_user tool not found` | Server plugin not registered | Add `createAskUserServerPlugin({ ... })` to `createWorkspaceAgentServer({ plugins: [...] })` or another static server composition point |
|
|
224
|
-
| Panel doesn't open | Front plugin not in workspace | Add `askUserPlugin` to `WorkspaceProvider` plugins array |
|
|
225
|
-
| Answer not reaching agent | Bridge connection broken | Check SSE endpoint; ensure `bridge` is passed to server plugin |
|
|
226
|
-
| Validation fails | User input doesn't match field schema | Check `required` fields, `options` for select fields, and `name` field keys |
|
|
227
|
-
| `PENDING_EXISTS` error | Another question is pending | Cancel or answer the existing question first |
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## Limitations
|
|
232
|
-
|
|
233
|
-
- **Workspace-private** — `"private": true` in package.json. Not published to npm. Install from source within the monorepo.
|
|
234
|
-
- **No file upload fields** — The form supports text, textarea, select, multiselect, checkbox, radio, and number only.
|
|
235
|
-
- **Single question per session** — The store enforces one pending question per session (`PENDING_EXISTS` error on duplicate).
|
|
236
|
-
- **No rich text or rich media** — Fields are plain text / numbers / selections. No markdown editors, image pickers, or date pickers.
|
|
237
|
-
- **Agent process restart clears pending** — If the agent restarts mid-question, the question becomes `abandoned` (the file store persists but no listener is active).
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## FAQ
|
|
242
|
-
|
|
243
|
-
**Q: What happens if the user closes the panel without answering?**
|
|
244
|
-
A: The question remains pending. The agent tool is still blocked. Use the cancel button in the panel to reject it.
|
|
245
|
-
|
|
246
|
-
**Q: Can the agent ask follow-up questions based on the answer?**
|
|
247
|
-
A: Yes — the agent receives the typed answer values and can use them in its next reasoning step, including asking another `ask_user` question.
|
|
248
|
-
|
|
249
|
-
**Q: How does this differ from just asking in chat?**
|
|
250
|
-
A: Chat responses are unstructured text. `ask_user` returns typed, validated data — `{ env: "production" }` — which the agent can use programmatically without parsing free text.
|
|
251
|
-
|
|
252
|
-
**Q: Is the question store persistent?**
|
|
253
|
-
A: The default `FileAskUserStore` persists to a JSON file and survives restarts. Swap in your own `AskUserStore` for database-backed persistence.
|
|
254
|
-
|
|
255
|
-
**Q: What happens if the agent process restarts mid-question?**
|
|
256
|
-
A: The question stays pending in the file store. On the next agent call, the tool will find a pending question and can either resume or mark it abandoned. The front panel also refreshes on page focus to pick up any pending state.
|
|
257
|
-
|
|
258
|
-
---
|
|
259
|
-
|
|
260
|
-
*About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
118
|
## License
|
|
265
119
|
|
|
266
120
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-ask-user",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"fastify": "^5.3.3",
|
|
47
47
|
"react": "^18.0.0 || ^19.0.0",
|
|
48
48
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
49
|
-
"@hachej/boring-workspace": "0.1.
|
|
49
|
+
"@hachej/boring-workspace": "0.1.42"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"lucide-react": "^1.8.0",
|
|
53
53
|
"zod": "^3.23.0",
|
|
54
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
54
|
+
"@hachej/boring-ui-kit": "0.1.42"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"tsup": "^8.4.0",
|
|
68
68
|
"typescript": "~5.9.3",
|
|
69
69
|
"vitest": "^3.2.6",
|
|
70
|
-
"@hachej/boring-workspace": "0.1.
|
|
70
|
+
"@hachej/boring-workspace": "0.1.42"
|
|
71
71
|
},
|
|
72
72
|
"peerDependenciesMeta": {
|
|
73
73
|
"fastify": {
|