@hachej/boring-workspace 0.1.10 → 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.
@@ -0,0 +1,322 @@
1
+ # Ask User Questions Plugin Spec
2
+
3
+ Last updated: 2026-05-08
4
+
5
+ ## Summary
6
+
7
+ Add a blocking `ask_user` capability for agent sessions that opens a Workspace
8
+ page named **Questions**, renders an agent-generated form, lets the user submit
9
+ answers, and returns those answers to the waiting agent tool call.
10
+
11
+ The Workspace implementation owns the page/plugin UI, form primitives, and UI
12
+ bridge command handling. The Agent implementation owns the blocking tool runtime
13
+ and session-level waiter. The browser must submit answers through the Workspace
14
+ UI bridge command path, not through ad-hoc form POST semantics.
15
+
16
+ ## Goals
17
+
18
+ - Provide an agent tool that asks the user for structured input and blocks until
19
+ the user answers or cancels.
20
+ - Open or focus a Workspace page/panel named `Questions` whenever a question is
21
+ pending.
22
+ - Render forms from a portable, JSON-serializable schema generated by the agent.
23
+ - Provide primitive React UI components and hooks so app shells can render these
24
+ forms in a standard way.
25
+ - Persist the single pending question per agent session across browser reloads.
26
+ - Send submitted answers back to the waiting agent through `UiBridge.postCommand`
27
+ as the single user-dispatch source.
28
+ - Keep Workspace base front/shared code package-neutral; no value imports from
29
+ `@boring/agent` outside allowed app composition boundaries.
30
+
31
+ ## Non-goals
32
+
33
+ - Multiple simultaneous pending questions per agent session.
34
+ - File upload fields in the first version.
35
+ - Full process-crash restoration of an already-blocked in-memory tool call,
36
+ unless the current agent session runtime already supports rehydrating waiters.
37
+ - Using chat-stream `data-ui-command` parts as the source of truth for answers.
38
+ - Making `pi-ask-user` the Workspace UX implementation dependency.
39
+
40
+ ## User flow
41
+
42
+ 1. Agent calls `ask_user` with title, optional context, and form schema.
43
+ 2. Server creates a pending question record for the current agent session.
44
+ 3. Agent tool blocks on an `AskUserCoordinator` waiter.
45
+ 4. Workspace receives/loads the pending question and opens or focuses the
46
+ `Questions` page.
47
+ 5. User fills the form and clicks Submit.
48
+ 6. Questions page calls `uiBridge.postCommand({ type: "questions.submit", ... })`.
49
+ 7. Workspace bridge server handler validates the command and records the answer.
50
+ 8. `AskUserCoordinator` resolves the waiter.
51
+ 9. Tool returns the structured answer to the agent and the agent continues.
52
+
53
+ Cancel follows the same path with `type: "questions.cancel"` and returns a
54
+ stable cancelled result to the tool caller.
55
+
56
+ ## Package placement
57
+
58
+ ### `@boring/workspace/shared`
59
+
60
+ Define browser-safe contracts only:
61
+
62
+ - `AskUserQuestion`
63
+ - `AskUserFormSchema`
64
+ - `AskUserField`
65
+ - `AskUserAnswer`
66
+ - `QuestionsSubmitCommand`
67
+ - `QuestionsCancelCommand`
68
+ - stable error codes imported from the canonical error-code enum
69
+
70
+ No `node:*`, no `Buffer`, no `@boring/agent` value imports.
71
+
72
+ ### `@boring/workspace/plugins/questionsPlugin`
73
+
74
+ New default Workspace plugin that owns:
75
+
76
+ - `front/QuestionsPage.tsx`
77
+ - `front/QuestionsPanel.tsx`
78
+ - `front/usePendingQuestion.ts`
79
+ - `front/primitives/QuestionForm.tsx`
80
+ - `front/primitives/QuestionField.tsx`
81
+ - field primitive components
82
+ - `server/questionsBridge.ts`
83
+ - `shared/types.ts`
84
+
85
+ Plugin outputs should include at minimum:
86
+
87
+ - a panel/page output for `Questions`
88
+ - a command output or bridge binding for `questions.submit`
89
+ - a command output or bridge binding for `questions.cancel`
90
+ - a surface resolver if the workspace opens the page through `openSurface`
91
+
92
+ ### `@boring/workspace/app/*`
93
+
94
+ App composition may wire the questions plugin to `@boring/agent/server` tool
95
+ registration APIs. This is the allowed boundary for Workspace + Agent product
96
+ composition.
97
+
98
+ ### `@boring/agent/server`
99
+
100
+ Agent package should expose the blocking `ask_user` tool factory and an adapter
101
+ interface that Workspace can satisfy:
102
+
103
+ ```ts
104
+ interface AskUserRuntime {
105
+ askUser(request: AskUserRequest): Promise<AskUserToolResult>
106
+ }
107
+ ```
108
+
109
+ Workspace app/server composition injects a runtime that stores the pending
110
+ question and resolves through UI bridge answers.
111
+
112
+ ## Form schema
113
+
114
+ Use a small JSON-schema-lite contract. It is serializable, easy to render in
115
+ React, and avoids forcing Zod or another runtime validator across the shared
116
+ browser/server boundary. A later helper may compile from Zod or JSON Schema into
117
+ this format.
118
+
119
+ ```ts
120
+ type AskUserFormSchema = {
121
+ fields: AskUserField[]
122
+ submitLabel?: string
123
+ }
124
+
125
+ type AskUserField =
126
+ | {
127
+ type: "text"
128
+ name: string
129
+ label: string
130
+ required?: boolean
131
+ placeholder?: string
132
+ defaultValue?: string
133
+ helpText?: string
134
+ }
135
+ | {
136
+ type: "textarea"
137
+ name: string
138
+ label: string
139
+ required?: boolean
140
+ placeholder?: string
141
+ defaultValue?: string
142
+ helpText?: string
143
+ }
144
+ | {
145
+ type: "select"
146
+ name: string
147
+ label: string
148
+ required?: boolean
149
+ options: AskUserOption[]
150
+ defaultValue?: string
151
+ helpText?: string
152
+ }
153
+ | {
154
+ type: "multiselect"
155
+ name: string
156
+ label: string
157
+ required?: boolean
158
+ options: AskUserOption[]
159
+ defaultValue?: string[]
160
+ helpText?: string
161
+ }
162
+ | {
163
+ type: "checkbox"
164
+ name: string
165
+ label: string
166
+ defaultValue?: boolean
167
+ helpText?: string
168
+ }
169
+ | {
170
+ type: "radio"
171
+ name: string
172
+ label: string
173
+ required?: boolean
174
+ options: AskUserOption[]
175
+ defaultValue?: string
176
+ helpText?: string
177
+ }
178
+
179
+ type AskUserOption = {
180
+ value: string
181
+ label: string
182
+ description?: string
183
+ }
184
+ ```
185
+
186
+ Answer shape:
187
+
188
+ ```ts
189
+ type AskUserAnswer = {
190
+ questionId: string
191
+ sessionId: string
192
+ values: Record<string, string | string[] | boolean | null>
193
+ submittedAt: string
194
+ }
195
+ ```
196
+
197
+ Tool result shape:
198
+
199
+ ```ts
200
+ type AskUserToolResult =
201
+ | { status: "answered"; answer: AskUserAnswer }
202
+ | { status: "cancelled"; questionId: string; sessionId: string }
203
+ ```
204
+
205
+ ## Persistence
206
+
207
+ Only one pending question is allowed per agent session. Creating a new pending
208
+ question for a session that already has one should fail with a stable error code
209
+ or replace only if an explicit future `replacePending` option is added.
210
+
211
+ Store interface:
212
+
213
+ ```ts
214
+ interface AskUserStore {
215
+ getPending(sessionId: string): Promise<AskUserQuestion | null>
216
+ createPending(question: AskUserQuestion): Promise<void>
217
+ answer(questionId: string, answer: AskUserAnswer): Promise<void>
218
+ cancel(questionId: string): Promise<void>
219
+ clearPending(sessionId: string): Promise<void>
220
+ }
221
+ ```
222
+
223
+ Initial implementation should provide a file-backed or existing workspace-store
224
+ adapter. Core/cloud can later inject a DB-backed implementation.
225
+
226
+ Persistence requirement for v1: pending question survives browser reload and the
227
+ Questions page can re-render it. Full server-process crash resumption of the
228
+ blocked tool waiter is a later concern unless already provided by the current
229
+ agent session runtime.
230
+
231
+ ## UI bridge commands
232
+
233
+ The browser submits through `UiBridge.postCommand`:
234
+
235
+ ```ts
236
+ uiBridge.postCommand({
237
+ type: "questions.submit",
238
+ questionId,
239
+ sessionId,
240
+ values,
241
+ })
242
+ ```
243
+
244
+ Cancel:
245
+
246
+ ```ts
247
+ uiBridge.postCommand({
248
+ type: "questions.cancel",
249
+ questionId,
250
+ sessionId,
251
+ })
252
+ ```
253
+
254
+ Server bridge handlers must:
255
+
256
+ - validate `sessionId` and `questionId`
257
+ - validate submitted values against the form schema
258
+ - persist answer/cancel state
259
+ - resolve the matching `AskUserCoordinator` waiter
260
+ - emit/update Workspace state so the Questions page leaves pending mode
261
+
262
+ No direct browser form route should be treated as the conceptual API. If an HTTP
263
+ route exists underneath, it is only transport for the UI bridge.
264
+
265
+ ## Questions page behavior
266
+
267
+ - Register/open a page or panel titled `Questions`.
268
+ - Auto-open or focus when a new pending question appears.
269
+ - Show title, context, form fields, submit button, and cancel affordance.
270
+ - Disable submit while validation fails or command is in flight.
271
+ - After submit, show a short answered state and return focus behavior to the
272
+ workspace shell if appropriate.
273
+ - If no pending question exists, show an empty state: "No questions right now."
274
+
275
+ ## Primitive UI package
276
+
277
+ Expose primitives from Workspace front code so apps can customize the page while
278
+ keeping standard schema behavior:
279
+
280
+ - `<QuestionForm schema values onValuesChange onSubmit onCancel />`
281
+ - `<QuestionField field value onChange error />`
282
+ - `<QuestionSubmitButton />`
283
+ - `<QuestionCancelButton />`
284
+ - `useQuestionForm(schema, initialValues?)`
285
+ - `validateQuestionValues(schema, values)`
286
+
287
+ Primitives should be headless-enough to style, but the default Questions page
288
+ should provide a polished complete UI.
289
+
290
+ ## `pi-ask-user` evaluation
291
+
292
+ `pi-ask-user` provides a native Pi `ask_user` tool with TUI overlay/inline UI,
293
+ selection lists, freeform input, multi-select, timeout, and a useful bundled
294
+ skill. It is good prior art and may be useful as a standalone CLI fallback.
295
+
296
+ Do not make it the primary Workspace implementation because it does not provide:
297
+
298
+ - Workspace page/panel integration
299
+ - generated rich React form rendering
300
+ - Workspace persistence semantics
301
+ - answer dispatch through `UiBridge.postCommand`
302
+ - app-shell primitive components
303
+
304
+ Recommended stance:
305
+
306
+ - Use `pi-ask-user` as reference for prompt/tool ergonomics.
307
+ - Consider optional CLI/TUI fallback for standalone `@boring/agent` mode.
308
+ - Keep Workspace Questions plugin implementation first-party.
309
+
310
+ ## Acceptance criteria for implementation bead
311
+
312
+ - Shared schema/types exist and are browser-safe.
313
+ - Questions plugin registers a `Questions` page/panel.
314
+ - Agent/session can create one pending question and block for the answer.
315
+ - Pending question persists across browser reload.
316
+ - Default page renders text, textarea, select, multiselect, checkbox, and radio.
317
+ - Submit uses `UiBridge.postCommand`, not direct conceptual route dispatch.
318
+ - Submitted values resolve the blocking tool call.
319
+ - Cancel resolves the tool call with `status: "cancelled"`.
320
+ - Primitive form components/hooks are exported for app-shell reuse.
321
+ - Unit tests cover schema validation and one-pending-question enforcement.
322
+ - Integration test or harness test covers ask -> render -> submit -> tool result.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-workspace",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Workspace UI, plugin, and bridge package for composing chat, files, catalogs, editors, and app-specific panes.",
@@ -125,8 +125,8 @@
125
125
  "tailwind-merge": "^2.0.0",
126
126
  "zod": "^3.23.0",
127
127
  "zustand": "^5.0.0",
128
- "@hachej/boring-agent": "0.1.10",
129
- "@hachej/boring-ui-kit": "0.1.10"
128
+ "@hachej/boring-agent": "0.1.13",
129
+ "@hachej/boring-ui-kit": "0.1.13"
130
130
  },
131
131
  "devDependencies": {
132
132
  "@tailwindcss/postcss": "^4.0.0",