@hachej/boring-ask-user 0.1.41 → 0.1.43

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.
Files changed (2) hide show
  1. package/README.md +67 -213
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,266 +1,120 @@
1
1
  # @hachej/boring-ask-user
2
2
 
3
- <div align="center">
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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ ## What it does
6
9
 
7
- </div>
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
- 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.
18
+ ## What it contributes to the workspace
10
19
 
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.
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
- ### Why Use @hachej/boring-ask-user?
29
+ ## How it's wired
26
30
 
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` |
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
- // const already — no factory. Add directly to WorkspaceProvider plugins.
38
+ // <WorkspaceProvider plugins={[askUserPlugin, ...]}>
45
39
  ```
46
40
 
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.
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 (agent runtime):**
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 askUserPlugin = createAskUserServerPlugin({ workspaceRoot, bridge })
55
- // Add the returned plugin object to createWorkspaceAgentServer({ plugins: [...] })
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 now has an `ask_user` tool. The agent calls it with:
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
- 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.
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
- ## Field Types
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
- | 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` |
85
+ Answer values are `string | string[] | boolean | number | null`, keyed by field
86
+ name under `answer.values`.
91
87
 
92
- Each field requires `name: string` (keys into the answer) and `label: string`. Optional: `required`, `helpText`, `defaultValue`.
88
+ ## Config & storage
93
89
 
94
- `AskUserOption = { value: string; label: string; description?: string }`.
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
- ## Answer Types
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
- ```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
- ```
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
- ## Installation
110
+ ## Validation
114
111
 
115
112
  ```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
- })
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.41",
3
+ "version": "0.1.43",
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.41"
49
+ "@hachej/boring-workspace": "0.1.43"
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.41"
54
+ "@hachej/boring-ui-kit": "0.1.43"
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.41"
70
+ "@hachej/boring-workspace": "0.1.43"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "fastify": {