@hachej/boring-ask-user 0.1.13
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 +266 -0
- package/dist/front/index.d.ts +17 -0
- package/dist/front/index.js +546 -0
- package/dist/server/index.d.ts +205 -0
- package/dist/server/index.js +1011 -0
- package/dist/shared/index.d.ts +3443 -0
- package/dist/shared/index.js +305 -0
- package/dist/types-CF72YmK-.d.ts +193 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# @hachej/boring-ask-user
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
Lets the coding agent ask the user a structured question and stream the answer back. Surfaces the question as a workbench panel; the agent's `ask_user` tool blocks until the user responds.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/hachej/boring-ui.git && cd boring-ui && pnpm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> **Note:** This plugin is workspace-private (`"private": true`) — install from source within the monorepo.
|
|
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.
|
|
24
|
+
|
|
25
|
+
### Why Use @hachej/boring-ask-user?
|
|
26
|
+
|
|
27
|
+
| Feature | What It Does |
|
|
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` |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Example
|
|
39
|
+
|
|
40
|
+
**Frontend (workbench):**
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { askUserPlugin } from "@hachej/boring-ask-user/front"
|
|
44
|
+
// const already — no factory. Add directly to WorkspaceProvider plugins.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pass `askUserPlugin` to your `WorkspaceProvider`'s `plugins` array. This front plugin includes a provider/binding, so compose it statically in the app shell rather than relying on dynamic package hot-load.
|
|
48
|
+
|
|
49
|
+
**Server (agent runtime):**
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createAskUserServerPlugin } from "@hachej/boring-ask-user/server"
|
|
53
|
+
|
|
54
|
+
const askUserPlugin = createAskUserServerPlugin({ workspaceRoot, bridge })
|
|
55
|
+
// Add the returned plugin object to createWorkspaceAgentServer({ plugins: [...] })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The agent now has an `ask_user` tool. The agent calls it with:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
{
|
|
62
|
+
title: "Deploy target?",
|
|
63
|
+
context: "Choose the environment.",
|
|
64
|
+
schema: {
|
|
65
|
+
wireVersion: 1,
|
|
66
|
+
fields: [
|
|
67
|
+
{ type: "select", name: "env", label: "Environment", options: [
|
|
68
|
+
{ value: "staging", label: "Staging" },
|
|
69
|
+
{ value: "production", label: "Production" },
|
|
70
|
+
]},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A panel opens in the workbench. The user picks an option and clicks "Send answers." The agent receives `{ status: "answered", answer: { values: { env: "production" } } }` and continues.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Field Types
|
|
81
|
+
|
|
82
|
+
| Type | Values | Key Props |
|
|
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` |
|
|
91
|
+
|
|
92
|
+
Each field requires `name: string` (keys into the answer) and `label: string`. Optional: `required`, `helpText`, `defaultValue`.
|
|
93
|
+
|
|
94
|
+
`AskUserOption = { value: string; label: string; description?: string }`.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Answer Types
|
|
99
|
+
|
|
100
|
+
```ts
|
|
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
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Installation
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# From source (workspace-only — not published to npm)
|
|
117
|
+
cd boring-ui/plugins/ask-user
|
|
118
|
+
pnpm install && pnpm build
|
|
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
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
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
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BoringFrontFactoryWithId } from '@hachej/boring-workspace/plugin';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `BoringFrontFactoryWithId` for the ask-user plugin. Registers
|
|
5
|
+
* (1) a provider that owns the per-app questions runtime (apiBaseUrl,
|
|
6
|
+
* auth headers, in-memory pending-question store), (2) a "Questions"
|
|
7
|
+
* panel rendering the pending question form, and (3) a surface
|
|
8
|
+
* resolver mapping ASK_USER_SURFACE_KIND requests into the panel.
|
|
9
|
+
*
|
|
10
|
+
* Pass directly to `WorkspaceProvider.plugins`.
|
|
11
|
+
*
|
|
12
|
+
* The panel is opened via the surface resolver (kind: ASK_USER_SURFACE_KIND),
|
|
13
|
+
* which is how the server-side agent tool triggers it.
|
|
14
|
+
*/
|
|
15
|
+
declare const askUserPlugin: BoringFrontFactoryWithId;
|
|
16
|
+
|
|
17
|
+
export { askUserPlugin, askUserPlugin as default };
|