@seed-ship/mcp-ui-solid 4.3.9 → 5.1.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.
@@ -6,6 +6,7 @@
6
6
  * See CHANGELOG for breaking changes on experimental types.
7
7
  */
8
8
 
9
+ import type { JSX } from 'solid-js'
9
10
  import type { UIComponent, UILayout } from './index'
10
11
 
11
12
  // ─── Event Base ──────────────────────────────────────────────
@@ -97,15 +98,44 @@ export interface ChatCommands {
97
98
  /**
98
99
  * Show a ChatPrompt (choice, confirm, form) above the input (C4).
99
100
  *
100
- * **Known limitation (v4.3.9):** Not re-entrant. If called while another
101
- * prompt is already active, the previous prompt's Promise will never resolve
102
- * (memory leak). Host apps must queue prompts or dismiss the previous one
103
- * manually before showing a new one. Fix planned for v4.4.0 (auto-reject
104
- * previous prompt or FIFO queue).
101
+ * **No default handler in v5.0.0 / v5.1.0.** `showChatPrompt` is a command
102
+ * *name*, not a default implementation mcp-ui ships `ChatPrompt` (the
103
+ * presentation component) and the bus (event/command plumbing), but the
104
+ * handler that threads a Promise resolver through the SolidJS lifecycle is
105
+ * the consumer's responsibility. Every host app calls
106
+ * `bus.commands.handle('showChatPrompt', (config, signal?) => { ... })`.
105
107
  *
106
- * **AbortSignal limitation (v4.3.9):** The `signal` argument is currently
107
- * unused — `ChatPrompt` does not listen to aborts. Host apps must wire
108
- * abort Promise rejection themselves. Fix planned for v4.4.0.
108
+ * ### Implementer contract
109
+ *
110
+ * A conforming handler MUST:
111
+ *
112
+ * 1. Return a `Promise<ChatPromptResponse>`.
113
+ * 2. Resolve the Promise from the `ChatPrompt` component's `onSubmit`
114
+ * (explicit answer) or from `onDismiss` (dismissed flag true).
115
+ * 3. If a `signal` is provided:
116
+ * - If `signal.aborted` is already `true`, reject with
117
+ * `new DOMException('Prompt aborted', 'AbortError')` synchronously
118
+ * (or via `Promise.reject`) and do NOT show the UI.
119
+ * - Otherwise, register `signal.addEventListener('abort', () =>
120
+ * reject(new DOMException('Prompt aborted', 'AbortError')))` and
121
+ * clean up the listener on resolve/dismiss.
122
+ * 4. Enforce re-entrance policy — if a previous prompt is still active
123
+ * when a new one arrives, the recommended behavior is auto-reject the
124
+ * previous Promise with a custom error (e.g. `PromptReplacedError`).
125
+ * Alternatives: FIFO queue, or throw synchronously.
126
+ *
127
+ * The `DOMException('AbortError')` shape is the Web Platform convention
128
+ * (matches `fetch()`, `Response.body.cancel()`, `WritableStream.abort()`).
129
+ * Consumers branching on the error can do
130
+ * `catch (err) { if (err.name === 'AbortError') return; throw err }`.
131
+ *
132
+ * ### Planned primitive (v5.2.0)
133
+ *
134
+ * A `createChatPromptController(setActivePrompt)` helper will centralise
135
+ * the resolver lifecycle + abort + re-entrance logic once, so consumers
136
+ * can write `bus.commands.handle('showChatPrompt', ctrl.handle)` instead
137
+ * of threading a `let chatPromptResolver` closure by hand. Design doc:
138
+ * `docs/2026/r&d/mcpui-v5.1.0-consensus.md`.
109
139
  */
110
140
  showChatPrompt: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>
111
141
  /** Dismiss the active ChatPrompt */
@@ -208,21 +238,95 @@ export interface ChatPromptConfig {
208
238
  config: ChoicePromptConfig | ConfirmPromptConfig | FormPromptConfig
209
239
  }
210
240
 
211
- export interface ChoicePromptConfig {
212
- options: Array<{
213
- value: string
214
- label: string
215
- icon?: string
216
- description?: string
217
- /**
218
- * Free-form metadata (confidence, source, tags, ...).
219
- * Opaque to default renderer — use a custom ChoiceBody wrapper to display it.
220
- * Preserved through showChatPrompt → ChatPromptResponse roundtrip.
221
- * @since v4.3.9
222
- */
223
- metadata?: Record<string, unknown>
224
- }>
241
+ /**
242
+ * A single choice option. The generic `TMeta` parameter flows through the
243
+ * whole `ChoicePromptConfig<TMeta>` shape so consumers can strongly-type
244
+ * their metadata in `optionRenderer` without casting.
245
+ *
246
+ * @since v4.3.9 (metadata), v5.1.0 (generic TMeta + optionRenderer typing)
247
+ */
248
+ export interface ChoiceOption<TMeta = Record<string, unknown>> {
249
+ value: string
250
+ label: string
251
+ icon?: string
252
+ description?: string
253
+ /**
254
+ * Free-form metadata (confidence, source, tags, ...).
255
+ * Opaque to the default renderer — use `optionRenderer` to display it.
256
+ * Preserved through `showChatPrompt → ChatPromptResponse` roundtrip.
257
+ * @since v4.3.9
258
+ */
259
+ metadata?: TMeta
260
+ }
261
+
262
+ export interface ChoicePromptConfig<TMeta = Record<string, unknown>> {
263
+ options: Array<ChoiceOption<TMeta>>
225
264
  layout?: 'horizontal' | 'vertical' | 'grid'
265
+ /**
266
+ * Optional render prop for custom option bodies (badges, confidence
267
+ * indicators, rich layouts). Replaces the default `label + icon +
268
+ * description` body. mcp-ui still wraps the returned JSX in a `<button>`
269
+ * with the `onClick` handler, keyboard support, and focus styles — only
270
+ * the *content* of the button is yours.
271
+ *
272
+ * @param option The full `ChoiceOption` including strongly-typed `metadata`.
273
+ * @param index Zero-based position in the `options` array.
274
+ *
275
+ * @example
276
+ * ```tsx
277
+ * interface ConfBadgeMeta { confidence: number; source: string }
278
+ *
279
+ * bus.commands.exec('showChatPrompt', {
280
+ * type: 'choice',
281
+ * title: 'Pick an intent',
282
+ * config: {
283
+ * layout: 'vertical',
284
+ * options: [
285
+ * { value: 'a', label: 'Immobilier', metadata: { confidence: 0.9, source: 'llm' } },
286
+ * { value: 'b', label: 'Santé', metadata: { confidence: 0.4, source: 'llm' } },
287
+ * ],
288
+ * optionRenderer: (opt: ChoiceOption<ConfBadgeMeta>) => (
289
+ * <div>
290
+ * {opt.label}
291
+ * <span class="ml-2 text-xs">
292
+ * ({Math.round((opt.metadata?.confidence ?? 0) * 100)}%)
293
+ * </span>
294
+ * </div>
295
+ * ),
296
+ * },
297
+ * } as ChatPromptConfig)
298
+ * ```
299
+ *
300
+ * ### ⚠️ Accessibility
301
+ * Do NOT return `<button>`, `<a href>`, or other interactive elements from
302
+ * `optionRenderer`. mcp-ui already wraps the content in a `<button>`, and
303
+ * nested interactive elements break screen-reader semantics, keyboard
304
+ * focus order, and click-through behaviour.
305
+ *
306
+ * ### ⚠️ Stale closures
307
+ * `optionRenderer` is called once per option per render. If you capture
308
+ * SolidJS signals inside the closure, wrap the access in a thunk so the
309
+ * framework tracks the dependency correctly. Don't destructure signal
310
+ * values into locals outside reactive scopes.
311
+ *
312
+ * @since v5.1.0
313
+ */
314
+ optionRenderer?: (option: ChoiceOption<TMeta>, index: number) => JSX.Element
315
+ /**
316
+ * Custom Tailwind classes appended to each option button (after mcp-ui's
317
+ * defaults). Escape hatch for colour/border/radius tweaks that don't
318
+ * warrant a full `optionRenderer`.
319
+ *
320
+ * @since v5.1.0
321
+ */
322
+ buttonClass?: string
323
+ /**
324
+ * Custom Tailwind classes appended to the options container (the
325
+ * flex/grid wrapper that lays out the buttons).
326
+ *
327
+ * @since v5.1.0
328
+ */
329
+ containerClass?: string
226
330
  }
227
331
 
228
332
  export interface ConfirmPromptConfig {
@@ -471,8 +575,6 @@ export interface ClarificationEvent {
471
575
  options: Array<{
472
576
  value: string
473
577
  label: string
474
- /** @deprecated Use metadata.file_id instead. Will be removed in v5.0.0. */
475
- file_id?: number
476
578
  /**
477
579
  * Free-form metadata (confidence, source, tags, ...).
478
580
  * Opaque to mcp-ui — host apps pass it through as-is.