@seed-ship/mcp-ui-solid 5.1.0 → 5.3.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +64 -13
  3. package/dist/components/ElicitationForm.cjs +51 -0
  4. package/dist/components/ElicitationForm.cjs.map +1 -0
  5. package/dist/components/ElicitationForm.d.ts +68 -0
  6. package/dist/components/ElicitationForm.d.ts.map +1 -0
  7. package/dist/components/ElicitationForm.js +51 -0
  8. package/dist/components/ElicitationForm.js.map +1 -0
  9. package/dist/components/FeedbackInline.cjs +57 -0
  10. package/dist/components/FeedbackInline.cjs.map +1 -0
  11. package/dist/components/FeedbackInline.d.ts +71 -0
  12. package/dist/components/FeedbackInline.d.ts.map +1 -0
  13. package/dist/components/FeedbackInline.js +57 -0
  14. package/dist/components/FeedbackInline.js.map +1 -0
  15. package/dist/components/index.d.ts +2 -0
  16. package/dist/components/index.d.ts.map +1 -1
  17. package/dist/components.cjs +2 -0
  18. package/dist/components.cjs.map +1 -1
  19. package/dist/components.d.cts +2 -0
  20. package/dist/components.d.ts +2 -0
  21. package/dist/components.js +2 -0
  22. package/dist/components.js.map +1 -1
  23. package/dist/index.cjs +17 -0
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +12 -2
  26. package/dist/index.d.ts +12 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +19 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/services/chat-bus.cjs +71 -0
  31. package/dist/services/chat-bus.cjs.map +1 -1
  32. package/dist/services/chat-bus.d.ts +31 -1
  33. package/dist/services/chat-bus.d.ts.map +1 -1
  34. package/dist/services/chat-bus.js +71 -0
  35. package/dist/services/chat-bus.js.map +1 -1
  36. package/dist/services/chat-prompt-controller.cjs +83 -0
  37. package/dist/services/chat-prompt-controller.cjs.map +1 -0
  38. package/dist/services/chat-prompt-controller.d.ts +93 -0
  39. package/dist/services/chat-prompt-controller.d.ts.map +1 -0
  40. package/dist/services/chat-prompt-controller.js +83 -0
  41. package/dist/services/chat-prompt-controller.js.map +1 -0
  42. package/dist/stores/scratchpad-store.cjs +105 -77
  43. package/dist/stores/scratchpad-store.cjs.map +1 -1
  44. package/dist/stores/scratchpad-store.d.ts +88 -19
  45. package/dist/stores/scratchpad-store.d.ts.map +1 -1
  46. package/dist/stores/scratchpad-store.js +105 -77
  47. package/dist/stores/scratchpad-store.js.map +1 -1
  48. package/dist/stores/server-capabilities-store.cjs +61 -0
  49. package/dist/stores/server-capabilities-store.cjs.map +1 -0
  50. package/dist/stores/server-capabilities-store.d.ts +172 -0
  51. package/dist/stores/server-capabilities-store.d.ts.map +1 -0
  52. package/dist/stores/server-capabilities-store.js +61 -0
  53. package/dist/stores/server-capabilities-store.js.map +1 -0
  54. package/dist/types/chat-bus.d.ts +39 -0
  55. package/dist/types/chat-bus.d.ts.map +1 -1
  56. package/docs/recipes/elicitation-pseudo-spec-adapter.md +171 -0
  57. package/docs/recipes/feedback-inline-wiring.md +142 -0
  58. package/package.json +1 -1
  59. package/src/components/ElicitationForm.test.tsx +197 -0
  60. package/src/components/ElicitationForm.tsx +126 -0
  61. package/src/components/FeedbackInline.test.tsx +117 -0
  62. package/src/components/FeedbackInline.tsx +143 -0
  63. package/src/components/index.ts +4 -0
  64. package/src/index.ts +39 -1
  65. package/src/services/chat-bus.test.ts +154 -2
  66. package/src/services/chat-bus.ts +115 -0
  67. package/src/services/chat-prompt-controller.test.ts +144 -0
  68. package/src/services/chat-prompt-controller.ts +214 -0
  69. package/src/stores/scratchpad-store.test.tsx +140 -0
  70. package/src/stores/scratchpad-store.tsx +244 -0
  71. package/src/stores/server-capabilities-store.test.tsx +206 -0
  72. package/src/stores/server-capabilities-store.tsx +215 -0
  73. package/src/types/chat-bus.ts +40 -0
  74. package/tsconfig.tsbuildinfo +1 -1
  75. package/src/stores/scratchpad-store.ts +0 -126
@@ -132,6 +132,76 @@ function mergeScratchpadSections(existing, incoming, mode = "replace") {
132
132
  return incoming;
133
133
  }
134
134
  }
135
+ function elicitationToPromptConfig(event) {
136
+ const propEntries = Object.entries(event.requestedSchema.properties);
137
+ if (propEntries.length === 1 && propEntries[0][1].type === "boolean") {
138
+ return {
139
+ type: "confirm",
140
+ title: event.message,
141
+ config: {
142
+ message: propEntries[0][1].description
143
+ }
144
+ };
145
+ }
146
+ if (propEntries.length === 1) {
147
+ const [, schema] = propEntries[0];
148
+ if (schema.enum && schema.enum.length > 0 && schema.enum.length <= 4) {
149
+ return {
150
+ type: "choice",
151
+ title: event.message,
152
+ config: {
153
+ options: schema.enum.map((val, idx) => {
154
+ var _a;
155
+ return {
156
+ value: String(val),
157
+ label: ((_a = schema.enumNames) == null ? void 0 : _a[idx]) ?? String(val)
158
+ };
159
+ }),
160
+ layout: "vertical"
161
+ }
162
+ };
163
+ }
164
+ }
165
+ const required = new Set(event.requestedSchema.required ?? []);
166
+ const fields = propEntries.map(([name, schema]) => ({
167
+ name,
168
+ label: schema.title ?? name,
169
+ ...schemaToFieldType(schema),
170
+ required: required.has(name),
171
+ helpText: schema.description,
172
+ ...schema.default !== void 0 ? { placeholder: String(schema.default) } : {}
173
+ }));
174
+ return {
175
+ type: "form",
176
+ title: event.message,
177
+ config: { fields }
178
+ };
179
+ }
180
+ function schemaToFieldType(schema) {
181
+ if (schema.enum && schema.enum.length > 0) {
182
+ return {
183
+ type: "select",
184
+ options: schema.enum.map((val, idx) => {
185
+ var _a;
186
+ return {
187
+ label: ((_a = schema.enumNames) == null ? void 0 : _a[idx]) ?? String(val),
188
+ value: String(val)
189
+ };
190
+ })
191
+ };
192
+ }
193
+ if (schema.type === "boolean") return { type: "checkbox" };
194
+ if (schema.type === "number" || schema.type === "integer") return { type: "number" };
195
+ if (schema.type === "string") {
196
+ if (schema.format === "email") return { type: "email" };
197
+ if (schema.format === "date" || schema.format === "date-time") return { type: "date" };
198
+ return { type: "text" };
199
+ }
200
+ console.warn(
201
+ `[MCP-UI] elicitationToPromptConfig: unsupported schema type "${schema.type}", falling back to text.`
202
+ );
203
+ return { type: "text" };
204
+ }
135
205
  function clarificationToPromptConfig(event) {
136
206
  return {
137
207
  type: "choice",
@@ -159,6 +229,7 @@ export {
159
229
  createChatBus,
160
230
  createCommandHandler,
161
231
  createEventEmitter,
232
+ elicitationToPromptConfig,
162
233
  mergeScratchpadSections
163
234
  };
164
235
  //# sourceMappingURL=chat-bus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat-bus.js","sources":["../../src/services/chat-bus.ts"],"sourcesContent":["/**\n * Chat Bus — Event Emitter + Command Handler\n * v2.4.0: Core primitives for the chat event/command bus\n *\n * @experimental — This API may change without major bump until v2.5.0.\n */\n\nimport type {\n ChatEvents,\n ChatCommands,\n ChatEventEmitter,\n ChatCommandHandler,\n ChatBus,\n EventSubscribeOptions,\n ScratchpadSection,\n ClarificationEvent,\n ChatPromptConfig,\n} from '../types/chat-bus'\n\n// ─── Event Emitter ───────────────────────────────────────────\n\ninterface Listener<F extends (...args: any[]) => any> {\n handler: F\n options?: EventSubscribeOptions\n throttledHandler?: F\n}\n\n/**\n * Create a typed event emitter with throttle and streamKey filtering support.\n *\n * @experimental\n *\n * @example\n * const emitter = createEventEmitter<ChatEvents>()\n * const unsub = emitter.on('onToken', (event) => console.log(event.token), { throttle: 100 })\n * emitter.emit('onToken', { streamKey: 'abc', token: 'hello' })\n * unsub()\n */\nexport function createEventEmitter(): ChatEventEmitter {\n const listeners = new Map<string, Set<Listener<any>>>()\n\n interface ThrottledFn<F> {\n fn: F\n cancel: () => void\n }\n\n function createThrottled<F extends (...args: any[]) => void>(fn: F, ms: number): ThrottledFn<F> {\n let lastCall = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let lastArgs: any[] | null = null\n let cancelled = false\n\n const throttled = ((...args: any[]) => {\n if (cancelled) return\n lastArgs = args\n const now = Date.now()\n const remaining = ms - (now - lastCall)\n\n if (remaining <= 0) {\n if (timer) { clearTimeout(timer); timer = null }\n lastCall = now\n fn(...args)\n } else if (!timer) {\n timer = setTimeout(() => {\n lastCall = Date.now()\n timer = null\n if (lastArgs && !cancelled) {\n try { fn(...lastArgs) } catch (err) { console.error('[ChatBus] Error in throttled handler:', err) }\n }\n }, remaining)\n }\n }) as F\n\n return {\n fn: throttled,\n cancel: () => { cancelled = true; if (timer) { clearTimeout(timer); timer = null } },\n }\n }\n\n return {\n on(event, handler, options) {\n if (!listeners.has(event as string)) {\n listeners.set(event as string, new Set())\n }\n\n const listener: Listener<typeof handler> = { handler, options }\n\n // Apply throttle if requested\n let throttleHandle: ThrottledFn<typeof handler> | null = null\n if (options?.throttle && options.throttle > 0) {\n throttleHandle = createThrottled(handler, options.throttle)\n listener.throttledHandler = throttleHandle.fn\n }\n\n listeners.get(event as string)!.add(listener)\n\n // Return unsubscribe function — cancels pending throttle timers\n return () => {\n throttleHandle?.cancel()\n listeners.get(event as string)?.delete(listener)\n }\n },\n\n emit(event, ...args) {\n const set = listeners.get(event as string)\n if (!set) return\n\n for (const listener of set) {\n // StreamKey filtering: skip if listener wants a specific streamKey\n // For most events args[0] has streamKey; for onCustomEvent args[1] has it\n if (listener.options?.streamKey) {\n let streamKeyArg: unknown\n for (const arg of args) {\n if (arg && typeof arg === 'object' && 'streamKey' in (arg as any)) {\n streamKeyArg = (arg as any).streamKey\n break\n }\n }\n if (streamKeyArg !== undefined && streamKeyArg !== listener.options.streamKey) continue\n }\n\n const fn = listener.throttledHandler || listener.handler\n try {\n fn(...args)\n } catch (err) {\n console.error(`[ChatBus] Error in ${event as string} handler:`, err)\n }\n }\n },\n\n clear() {\n listeners.clear()\n },\n } as ChatEventEmitter\n}\n\n// ─── Command Handler ─────────────────────────────────────────\n\n/**\n * Create a typed command handler. The host app registers handlers,\n * agents execute commands.\n *\n * @experimental\n *\n * @example\n * const commands = createCommandHandler<ChatCommands>()\n * commands.handle('injectPrompt', (text) => setInputValue(text))\n * commands.exec('injectPrompt', 'Hello world')\n */\nexport function createCommandHandler(): ChatCommandHandler {\n const handlers = new Map<string, (...args: any[]) => any>()\n\n return {\n handle(command, handler) {\n handlers.set(command as string, handler)\n },\n\n exec(command, ...args) {\n const handler = handlers.get(command as string)\n if (!handler) {\n console.warn(`[ChatBus] No handler registered for command: ${command as string}`)\n return undefined as any\n }\n return handler(...args)\n },\n } as ChatCommandHandler\n}\n\n// ─── Chat Bus Factory ────────────────────────────────────────\n\n/**\n * Create a complete ChatBus with events + commands.\n *\n * @experimental\n *\n * @example\n * const bus = createChatBus()\n * bus.events.on('onStreamEnd', (event) => { ... })\n * bus.commands.handle('sendPrompt', (text) => { ... })\n */\nexport function createChatBus(): ChatBus {\n return {\n events: createEventEmitter(),\n commands: createCommandHandler(),\n }\n}\n\n// ─── Scratchpad Section Merge Helper ─────────────────────────\n\n/**\n * Merge sections from a ScratchpadEvent into existing state sections.\n * Handles replace/append/upsert modes.\n *\n * @example\n * const newSections = mergeScratchpadSections(\n * currentState.sections,\n * event.sections,\n * event.sectionMode\n * )\n */\nexport function mergeScratchpadSections(\n existing: ScratchpadSection[],\n incoming: ScratchpadSection[] | undefined,\n mode: 'replace' | 'append' | 'upsert' = 'replace'\n): ScratchpadSection[] {\n if (!incoming) return existing\n\n switch (mode) {\n case 'append':\n return [...existing, ...incoming]\n\n case 'upsert': {\n const result = [...existing]\n for (const section of incoming) {\n const idx = result.findIndex((s) => s.id === section.id)\n if (idx >= 0) {\n result[idx] = section\n } else {\n result.push(section)\n }\n }\n return result\n }\n\n case 'replace':\n default:\n return incoming\n }\n}\n\n// ─── Clarification → Prompt Helper (v4.3.9) ──────────────────\n\n/**\n * Convert a ClarificationEvent into a ChatPromptConfig.\n * Universal bridge for apps receiving clarification events via SSE.\n *\n * Legacy runtime `file_id` (removed from the type in v5.0.0) is still\n * transparently migrated into `metadata.file_id` when present, so payloads\n * from older servers continue to work without upgrade pressure.\n *\n * @experimental\n * @since v4.3.9\n * @example\n * bus.events.on('onClarificationNeeded', ({ clarification }) => {\n * bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))\n * })\n */\nexport function clarificationToPromptConfig(\n event: ClarificationEvent\n): ChatPromptConfig {\n return {\n type: 'choice',\n title: event.question,\n config: {\n options: event.options.map((opt) => {\n const merged: Record<string, unknown> = { ...(opt.metadata ?? {}) }\n // Runtime fallback for legacy payloads that still carry file_id at the top level.\n const legacyFileId = (opt as { file_id?: number }).file_id\n if (legacyFileId !== undefined && merged.file_id === undefined) {\n merged.file_id = legacyFileId\n }\n return {\n value: opt.value,\n label: opt.label,\n // Only include metadata if non-empty (keeps payloads clean)\n ...(Object.keys(merged).length > 0 ? { metadata: merged } : {}),\n }\n }),\n layout: 'vertical',\n },\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat-bus.js","sources":["../../src/services/chat-bus.ts"],"sourcesContent":["/**\n * Chat Bus — Event Emitter + Command Handler\n * v2.4.0: Core primitives for the chat event/command bus\n *\n * @experimental — This API may change without major bump until v2.5.0.\n */\n\nimport type {\n ChatEvents,\n ChatCommands,\n ChatEventEmitter,\n ChatCommandHandler,\n ChatBus,\n EventSubscribeOptions,\n ScratchpadSection,\n ClarificationEvent,\n ElicitationEvent,\n ElicitationPropertySchema,\n ChatPromptConfig,\n FormPromptConfig,\n} from '../types/chat-bus'\n\n// ─── Event Emitter ───────────────────────────────────────────\n\ninterface Listener<F extends (...args: any[]) => any> {\n handler: F\n options?: EventSubscribeOptions\n throttledHandler?: F\n}\n\n/**\n * Create a typed event emitter with throttle and streamKey filtering support.\n *\n * @experimental\n *\n * @example\n * const emitter = createEventEmitter<ChatEvents>()\n * const unsub = emitter.on('onToken', (event) => console.log(event.token), { throttle: 100 })\n * emitter.emit('onToken', { streamKey: 'abc', token: 'hello' })\n * unsub()\n */\nexport function createEventEmitter(): ChatEventEmitter {\n const listeners = new Map<string, Set<Listener<any>>>()\n\n interface ThrottledFn<F> {\n fn: F\n cancel: () => void\n }\n\n function createThrottled<F extends (...args: any[]) => void>(fn: F, ms: number): ThrottledFn<F> {\n let lastCall = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let lastArgs: any[] | null = null\n let cancelled = false\n\n const throttled = ((...args: any[]) => {\n if (cancelled) return\n lastArgs = args\n const now = Date.now()\n const remaining = ms - (now - lastCall)\n\n if (remaining <= 0) {\n if (timer) { clearTimeout(timer); timer = null }\n lastCall = now\n fn(...args)\n } else if (!timer) {\n timer = setTimeout(() => {\n lastCall = Date.now()\n timer = null\n if (lastArgs && !cancelled) {\n try { fn(...lastArgs) } catch (err) { console.error('[ChatBus] Error in throttled handler:', err) }\n }\n }, remaining)\n }\n }) as F\n\n return {\n fn: throttled,\n cancel: () => { cancelled = true; if (timer) { clearTimeout(timer); timer = null } },\n }\n }\n\n return {\n on(event, handler, options) {\n if (!listeners.has(event as string)) {\n listeners.set(event as string, new Set())\n }\n\n const listener: Listener<typeof handler> = { handler, options }\n\n // Apply throttle if requested\n let throttleHandle: ThrottledFn<typeof handler> | null = null\n if (options?.throttle && options.throttle > 0) {\n throttleHandle = createThrottled(handler, options.throttle)\n listener.throttledHandler = throttleHandle.fn\n }\n\n listeners.get(event as string)!.add(listener)\n\n // Return unsubscribe function — cancels pending throttle timers\n return () => {\n throttleHandle?.cancel()\n listeners.get(event as string)?.delete(listener)\n }\n },\n\n emit(event, ...args) {\n const set = listeners.get(event as string)\n if (!set) return\n\n for (const listener of set) {\n // StreamKey filtering: skip if listener wants a specific streamKey\n // For most events args[0] has streamKey; for onCustomEvent args[1] has it\n if (listener.options?.streamKey) {\n let streamKeyArg: unknown\n for (const arg of args) {\n if (arg && typeof arg === 'object' && 'streamKey' in (arg as any)) {\n streamKeyArg = (arg as any).streamKey\n break\n }\n }\n if (streamKeyArg !== undefined && streamKeyArg !== listener.options.streamKey) continue\n }\n\n const fn = listener.throttledHandler || listener.handler\n try {\n fn(...args)\n } catch (err) {\n console.error(`[ChatBus] Error in ${event as string} handler:`, err)\n }\n }\n },\n\n clear() {\n listeners.clear()\n },\n } as ChatEventEmitter\n}\n\n// ─── Command Handler ─────────────────────────────────────────\n\n/**\n * Create a typed command handler. The host app registers handlers,\n * agents execute commands.\n *\n * @experimental\n *\n * @example\n * const commands = createCommandHandler<ChatCommands>()\n * commands.handle('injectPrompt', (text) => setInputValue(text))\n * commands.exec('injectPrompt', 'Hello world')\n */\nexport function createCommandHandler(): ChatCommandHandler {\n const handlers = new Map<string, (...args: any[]) => any>()\n\n return {\n handle(command, handler) {\n handlers.set(command as string, handler)\n },\n\n exec(command, ...args) {\n const handler = handlers.get(command as string)\n if (!handler) {\n console.warn(`[ChatBus] No handler registered for command: ${command as string}`)\n return undefined as any\n }\n return handler(...args)\n },\n } as ChatCommandHandler\n}\n\n// ─── Chat Bus Factory ────────────────────────────────────────\n\n/**\n * Create a complete ChatBus with events + commands.\n *\n * @experimental\n *\n * @example\n * const bus = createChatBus()\n * bus.events.on('onStreamEnd', (event) => { ... })\n * bus.commands.handle('sendPrompt', (text) => { ... })\n */\nexport function createChatBus(): ChatBus {\n return {\n events: createEventEmitter(),\n commands: createCommandHandler(),\n }\n}\n\n// ─── Scratchpad Section Merge Helper ─────────────────────────\n\n/**\n * Merge sections from a ScratchpadEvent into existing state sections.\n * Handles replace/append/upsert modes.\n *\n * @example\n * const newSections = mergeScratchpadSections(\n * currentState.sections,\n * event.sections,\n * event.sectionMode\n * )\n */\nexport function mergeScratchpadSections(\n existing: ScratchpadSection[],\n incoming: ScratchpadSection[] | undefined,\n mode: 'replace' | 'append' | 'upsert' = 'replace'\n): ScratchpadSection[] {\n if (!incoming) return existing\n\n switch (mode) {\n case 'append':\n return [...existing, ...incoming]\n\n case 'upsert': {\n const result = [...existing]\n for (const section of incoming) {\n const idx = result.findIndex((s) => s.id === section.id)\n if (idx >= 0) {\n result[idx] = section\n } else {\n result.push(section)\n }\n }\n return result\n }\n\n case 'replace':\n default:\n return incoming\n }\n}\n\n// ─── Clarification → Prompt Helper (v4.3.9) ──────────────────\n\n/**\n * Convert a ClarificationEvent into a ChatPromptConfig.\n * Universal bridge for apps receiving clarification events via SSE.\n *\n * Legacy runtime `file_id` (removed from the type in v5.0.0) is still\n * transparently migrated into `metadata.file_id` when present, so payloads\n * from older servers continue to work without upgrade pressure.\n *\n * @experimental\n * @since v4.3.9\n * @example\n * bus.events.on('onClarificationNeeded', ({ clarification }) => {\n * bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))\n * })\n */\n// ─── Elicitation → Prompt Helper (v5.2.0) ───────────────────\n\n/**\n * Convert an MCP `elicitation/create` payload into a `ChatPromptConfig`.\n *\n * Mapping rules :\n * - Single `boolean` property → `type: 'confirm'`\n * - Single property with `enum` of ≤4 values → `type: 'choice'` (one option per enum value)\n * - Anything else → `type: 'form'` with one field per schema property\n *\n * JSON Schema primitive types map to mcp-ui form field types :\n *\n * | JSON Schema | mcp-ui FormFieldType |\n * |---|---|\n * | `type: 'string'` | `'text'` |\n * | `type: 'string', format: 'email'` | `'email'` |\n * | `type: 'string', format: 'date'` or `'date-time'` | `'date'` |\n * | `type: 'string', enum: [...]` | `'select'` |\n * | `type: 'number' \\| 'integer'` | `'number'` |\n * | `type: 'boolean'` | `'checkbox'` |\n *\n * Unknown shapes fall through to plain text with a `helpText` warning.\n *\n * @experimental\n * @since v5.2.0\n *\n * @example\n * bus.events.on('onElicitation', ({ elicitation }) => {\n * bus.commands.exec('showChatPrompt', elicitationToPromptConfig(elicitation))\n * })\n */\nexport function elicitationToPromptConfig(event: ElicitationEvent): ChatPromptConfig {\n const propEntries = Object.entries(event.requestedSchema.properties)\n\n // Shortcut 1 : single boolean → confirm\n if (propEntries.length === 1 && propEntries[0][1].type === 'boolean') {\n return {\n type: 'confirm',\n title: event.message,\n config: {\n message: propEntries[0][1].description,\n },\n }\n }\n\n // Shortcut 2 : single enum property with ≤4 values → choice\n if (propEntries.length === 1) {\n const [, schema] = propEntries[0]\n if (schema.enum && schema.enum.length > 0 && schema.enum.length <= 4) {\n return {\n type: 'choice',\n title: event.message,\n config: {\n options: schema.enum.map((val, idx) => ({\n value: String(val),\n label: schema.enumNames?.[idx] ?? String(val),\n })),\n layout: 'vertical',\n },\n }\n }\n }\n\n // Default : full form\n const required = new Set(event.requestedSchema.required ?? [])\n const fields: FormPromptConfig['fields'] = propEntries.map(([name, schema]) => ({\n name,\n label: schema.title ?? name,\n ...schemaToFieldType(schema),\n required: required.has(name),\n helpText: schema.description,\n ...(schema.default !== undefined ? { placeholder: String(schema.default) } : {}),\n }))\n\n return {\n type: 'form',\n title: event.message,\n config: { fields },\n }\n}\n\nfunction schemaToFieldType(\n schema: ElicitationPropertySchema\n):\n | { type: FormPromptConfig['fields'][number]['type']; options?: Array<{ label: string; value: string }> }\n | { type: FormPromptConfig['fields'][number]['type']; helpText?: string } {\n // Enum → select\n if (schema.enum && schema.enum.length > 0) {\n return {\n type: 'select',\n options: schema.enum.map((val, idx) => ({\n label: schema.enumNames?.[idx] ?? String(val),\n value: String(val),\n })),\n }\n }\n\n if (schema.type === 'boolean') return { type: 'checkbox' }\n if (schema.type === 'number' || schema.type === 'integer') return { type: 'number' }\n if (schema.type === 'string') {\n if (schema.format === 'email') return { type: 'email' }\n if (schema.format === 'date' || schema.format === 'date-time') return { type: 'date' }\n return { type: 'text' }\n }\n\n // Unknown primitive — fall back to text with a warning\n console.warn(\n `[MCP-UI] elicitationToPromptConfig: unsupported schema type \"${(schema as { type?: string }).type}\", falling back to text.`\n )\n return { type: 'text' }\n}\n\nexport function clarificationToPromptConfig(\n event: ClarificationEvent\n): ChatPromptConfig {\n return {\n type: 'choice',\n title: event.question,\n config: {\n options: event.options.map((opt) => {\n const merged: Record<string, unknown> = { ...(opt.metadata ?? {}) }\n // Runtime fallback for legacy payloads that still carry file_id at the top level.\n const legacyFileId = (opt as { file_id?: number }).file_id\n if (legacyFileId !== undefined && merged.file_id === undefined) {\n merged.file_id = legacyFileId\n }\n return {\n value: opt.value,\n label: opt.label,\n // Only include metadata if non-empty (keeps payloads clean)\n ...(Object.keys(merged).length > 0 ? { metadata: merged } : {}),\n }\n }),\n layout: 'vertical',\n },\n }\n}\n"],"names":[],"mappings":"AAyCO,SAAS,qBAAuC;AACrD,QAAM,gCAAgB,IAAA;AAOtB,WAAS,gBAAoD,IAAO,IAA4B;AAC9F,QAAI,WAAW;AACf,QAAI,QAA8C;AAClD,QAAI,WAAyB;AAC7B,QAAI,YAAY;AAEhB,UAAM,aAAa,IAAI,SAAgB;AACrC,UAAI,UAAW;AACf,iBAAW;AACX,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,YAAY,MAAM,MAAM;AAE9B,UAAI,aAAa,GAAG;AAClB,YAAI,OAAO;AAAE,uBAAa,KAAK;AAAG,kBAAQ;AAAA,QAAK;AAC/C,mBAAW;AACX,WAAG,GAAG,IAAI;AAAA,MACZ,WAAW,CAAC,OAAO;AACjB,gBAAQ,WAAW,MAAM;AACvB,qBAAW,KAAK,IAAA;AAChB,kBAAQ;AACR,cAAI,YAAY,CAAC,WAAW;AAC1B,gBAAI;AAAE,iBAAG,GAAG,QAAQ;AAAA,YAAE,SAAS,KAAK;AAAE,sBAAQ,MAAM,yCAAyC,GAAG;AAAA,YAAE;AAAA,UACpG;AAAA,QACF,GAAG,SAAS;AAAA,MACd;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,MAAM;AAAE,oBAAY;AAAM,YAAI,OAAO;AAAE,uBAAa,KAAK;AAAG,kBAAQ;AAAA,QAAK;AAAA,MAAE;AAAA,IAAA;AAAA,EAEvF;AAEA,SAAO;AAAA,IACL,GAAG,OAAO,SAAS,SAAS;AAC1B,UAAI,CAAC,UAAU,IAAI,KAAe,GAAG;AACnC,kBAAU,IAAI,OAAiB,oBAAI,IAAA,CAAK;AAAA,MAC1C;AAEA,YAAM,WAAqC,EAAE,SAAS,QAAA;AAGtD,UAAI,iBAAqD;AACzD,WAAI,mCAAS,aAAY,QAAQ,WAAW,GAAG;AAC7C,yBAAiB,gBAAgB,SAAS,QAAQ,QAAQ;AAC1D,iBAAS,mBAAmB,eAAe;AAAA,MAC7C;AAEA,gBAAU,IAAI,KAAe,EAAG,IAAI,QAAQ;AAG5C,aAAO,MAAM;AA3DZ;AA4DC,yDAAgB;AAChB,wBAAU,IAAI,KAAe,MAA7B,mBAAgC,OAAO;AAAA,MACzC;AAAA,IACF;AAAA,IAEA,KAAK,UAAU,MAAM;AAjElB;AAkED,YAAM,MAAM,UAAU,IAAI,KAAe;AACzC,UAAI,CAAC,IAAK;AAEV,iBAAW,YAAY,KAAK;AAG1B,aAAI,cAAS,YAAT,mBAAkB,WAAW;AAC/B,cAAI;AACJ,qBAAW,OAAO,MAAM;AACtB,gBAAI,OAAO,OAAO,QAAQ,YAAY,eAAgB,KAAa;AACjE,6BAAgB,IAAY;AAC5B;AAAA,YACF;AAAA,UACF;AACA,cAAI,iBAAiB,UAAa,iBAAiB,SAAS,QAAQ,UAAW;AAAA,QACjF;AAEA,cAAM,KAAK,SAAS,oBAAoB,SAAS;AACjD,YAAI;AACF,aAAG,GAAG,IAAI;AAAA,QACZ,SAAS,KAAK;AACZ,kBAAQ,MAAM,sBAAsB,KAAe,aAAa,GAAG;AAAA,QACrE;AAAA,MACF;AAAA,IACF;AAAA,IAEA,QAAQ;AACN,gBAAU,MAAA;AAAA,IACZ;AAAA,EAAA;AAEJ;AAeO,SAAS,uBAA2C;AACzD,QAAM,+BAAe,IAAA;AAErB,SAAO;AAAA,IACL,OAAO,SAAS,SAAS;AACvB,eAAS,IAAI,SAAmB,OAAO;AAAA,IACzC;AAAA,IAEA,KAAK,YAAY,MAAM;AACrB,YAAM,UAAU,SAAS,IAAI,OAAiB;AAC9C,UAAI,CAAC,SAAS;AACZ,gBAAQ,KAAK,gDAAgD,OAAiB,EAAE;AAChF,eAAO;AAAA,MACT;AACA,aAAO,QAAQ,GAAG,IAAI;AAAA,IACxB;AAAA,EAAA;AAEJ;AAcO,SAAS,gBAAyB;AACvC,SAAO;AAAA,IACL,QAAQ,mBAAA;AAAA,IACR,UAAU,qBAAA;AAAA,EAAqB;AAEnC;AAeO,SAAS,wBACd,UACA,UACA,OAAwC,WACnB;AACrB,MAAI,CAAC,SAAU,QAAO;AAEtB,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO,CAAC,GAAG,UAAU,GAAG,QAAQ;AAAA,IAElC,KAAK,UAAU;AACb,YAAM,SAAS,CAAC,GAAG,QAAQ;AAC3B,iBAAW,WAAW,UAAU;AAC9B,cAAM,MAAM,OAAO,UAAU,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE;AACvD,YAAI,OAAO,GAAG;AACZ,iBAAO,GAAG,IAAI;AAAA,QAChB,OAAO;AACL,iBAAO,KAAK,OAAO;AAAA,QACrB;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IAEA,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EAAA;AAEb;AAkDO,SAAS,0BAA0B,OAA2C;AACnF,QAAM,cAAc,OAAO,QAAQ,MAAM,gBAAgB,UAAU;AAGnE,MAAI,YAAY,WAAW,KAAK,YAAY,CAAC,EAAE,CAAC,EAAE,SAAS,WAAW;AACpE,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO,MAAM;AAAA,MACb,QAAQ;AAAA,QACN,SAAS,YAAY,CAAC,EAAE,CAAC,EAAE;AAAA,MAAA;AAAA,IAC7B;AAAA,EAEJ;AAGA,MAAI,YAAY,WAAW,GAAG;AAC5B,UAAM,GAAG,MAAM,IAAI,YAAY,CAAC;AAChC,QAAI,OAAO,QAAQ,OAAO,KAAK,SAAS,KAAK,OAAO,KAAK,UAAU,GAAG;AACpE,aAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO,MAAM;AAAA,QACb,QAAQ;AAAA,UACN,SAAS,OAAO,KAAK,IAAI,CAAC,KAAK,QAAA;AAtQlC;AAsQ2C;AAAA,cACtC,OAAO,OAAO,GAAG;AAAA,cACjB,SAAO,YAAO,cAAP,mBAAmB,SAAQ,OAAO,GAAG;AAAA,YAAA;AAAA,WAC5C;AAAA,UACF,QAAQ;AAAA,QAAA;AAAA,MACV;AAAA,IAEJ;AAAA,EACF;AAGA,QAAM,WAAW,IAAI,IAAI,MAAM,gBAAgB,YAAY,EAAE;AAC7D,QAAM,SAAqC,YAAY,IAAI,CAAC,CAAC,MAAM,MAAM,OAAO;AAAA,IAC9E;AAAA,IACA,OAAO,OAAO,SAAS;AAAA,IACvB,GAAG,kBAAkB,MAAM;AAAA,IAC3B,UAAU,SAAS,IAAI,IAAI;AAAA,IAC3B,UAAU,OAAO;AAAA,IACjB,GAAI,OAAO,YAAY,SAAY,EAAE,aAAa,OAAO,OAAO,OAAO,MAAM,CAAA;AAAA,EAAC,EAC9E;AAEF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,MAAM;AAAA,IACb,QAAQ,EAAE,OAAA;AAAA,EAAO;AAErB;AAEA,SAAS,kBACP,QAG0E;AAE1E,MAAI,OAAO,QAAQ,OAAO,KAAK,SAAS,GAAG;AACzC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,OAAO,KAAK,IAAI,CAAC,KAAK,QAAA;AA3S9B;AA2SuC;AAAA,UACtC,SAAO,YAAO,cAAP,mBAAmB,SAAQ,OAAO,GAAG;AAAA,UAC5C,OAAO,OAAO,GAAG;AAAA,QAAA;AAAA,OACjB;AAAA,IAAA;AAAA,EAEN;AAEA,MAAI,OAAO,SAAS,UAAW,QAAO,EAAE,MAAM,WAAA;AAC9C,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAW,QAAO,EAAE,MAAM,SAAA;AAC1E,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI,OAAO,WAAW,QAAS,QAAO,EAAE,MAAM,QAAA;AAC9C,QAAI,OAAO,WAAW,UAAU,OAAO,WAAW,YAAa,QAAO,EAAE,MAAM,OAAA;AAC9E,WAAO,EAAE,MAAM,OAAA;AAAA,EACjB;AAGA,UAAQ;AAAA,IACN,gEAAiE,OAA6B,IAAI;AAAA,EAAA;AAEpG,SAAO,EAAE,MAAM,OAAA;AACjB;AAEO,SAAS,4BACd,OACkB;AAClB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,MAAM;AAAA,IACb,QAAQ;AAAA,MACN,SAAS,MAAM,QAAQ,IAAI,CAAC,QAAQ;AAClC,cAAM,SAAkC,EAAE,GAAI,IAAI,YAAY,CAAA,EAAC;AAE/D,cAAM,eAAgB,IAA6B;AACnD,YAAI,iBAAiB,UAAa,OAAO,YAAY,QAAW;AAC9D,iBAAO,UAAU;AAAA,QACnB;AACA,eAAO;AAAA,UACL,OAAO,IAAI;AAAA,UACX,OAAO,IAAI;AAAA;AAAA,UAEX,GAAI,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,EAAE,UAAU,WAAW,CAAA;AAAA,QAAC;AAAA,MAEjE,CAAC;AAAA,MACD,QAAQ;AAAA,IAAA;AAAA,EACV;AAEJ;"}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
+ const solidJs = require("solid-js");
7
+ class PromptReplacedError extends Error {
8
+ constructor(message = "Prompt replaced by a newer one") {
9
+ super(message);
10
+ __publicField(this, "name", "PromptReplacedError");
11
+ }
12
+ }
13
+ function createChatPromptController() {
14
+ const [activePrompt, setActivePrompt] = solidJs.createSignal(null);
15
+ let pending = null;
16
+ function cleanupAbort(entry) {
17
+ if (entry.signal && entry.onAbort) {
18
+ entry.signal.removeEventListener("abort", entry.onAbort);
19
+ }
20
+ }
21
+ function handle(config, signal) {
22
+ if (pending) {
23
+ const previous = pending;
24
+ pending = null;
25
+ cleanupAbort(previous);
26
+ previous.reject(new PromptReplacedError());
27
+ }
28
+ if (signal == null ? void 0 : signal.aborted) {
29
+ setActivePrompt(null);
30
+ return Promise.reject(new DOMException("Prompt aborted", "AbortError"));
31
+ }
32
+ return new Promise((resolve, reject) => {
33
+ const entry = { type: config.type, resolve, reject, signal };
34
+ if (signal) {
35
+ entry.onAbort = () => {
36
+ if (pending === entry) {
37
+ pending = null;
38
+ cleanupAbort(entry);
39
+ setActivePrompt(null);
40
+ reject(new DOMException("Prompt aborted", "AbortError"));
41
+ }
42
+ };
43
+ signal.addEventListener("abort", entry.onAbort, { once: true });
44
+ }
45
+ pending = entry;
46
+ setActivePrompt(config);
47
+ });
48
+ }
49
+ function resolveActive(response) {
50
+ if (!pending) return;
51
+ const entry = pending;
52
+ pending = null;
53
+ cleanupAbort(entry);
54
+ setActivePrompt(null);
55
+ entry.resolve(response);
56
+ }
57
+ function dismissActive() {
58
+ if (!pending) return;
59
+ const entry = pending;
60
+ pending = null;
61
+ cleanupAbort(entry);
62
+ setActivePrompt(null);
63
+ entry.resolve({ type: entry.type, value: "", label: "", dismissed: true });
64
+ }
65
+ function abort(reason = "Prompt aborted") {
66
+ if (!pending) return;
67
+ const entry = pending;
68
+ pending = null;
69
+ cleanupAbort(entry);
70
+ setActivePrompt(null);
71
+ entry.reject(new DOMException(reason, "AbortError"));
72
+ }
73
+ return {
74
+ handle,
75
+ activePrompt,
76
+ resolveActive,
77
+ dismissActive,
78
+ abort
79
+ };
80
+ }
81
+ exports.PromptReplacedError = PromptReplacedError;
82
+ exports.createChatPromptController = createChatPromptController;
83
+ //# sourceMappingURL=chat-prompt-controller.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-prompt-controller.cjs","sources":["../../src/services/chat-prompt-controller.ts"],"sourcesContent":["/**\n * createChatPromptController — centralised lifecycle for `showChatPrompt`\n *\n * @experimental\n * @since v5.2.0\n *\n * The controller owns the resolver closure, AbortSignal wiring, and\n * re-entrance policy in one primitive. Consumers go from ~20 LOC of manual\n * wiring per app to :\n *\n * ```ts\n * const ctrl = createChatPromptController()\n * bus.commands.handle('showChatPrompt', ctrl.handle)\n * // ...\n * <Show when={ctrl.activePrompt()}>\n * {(cfg) => (\n * <ChatPrompt\n * config={cfg()}\n * onSubmit={ctrl.resolveActive}\n * onDismiss={ctrl.dismissActive}\n * />\n * )}\n * </Show>\n * ```\n *\n * ## Re-entrance policy\n *\n * If a new `showChatPrompt` arrives while a previous Promise is still\n * pending, the previous Promise rejects **synchronously** with a\n * `PromptReplacedError` before the new prompt is installed. Callers that\n * care can branch on `err instanceof PromptReplacedError` or `err.name ===\n * 'PromptReplacedError'`.\n *\n * ## Abort semantics\n *\n * `handle(config, signal?)` honours `AbortSignal` :\n *\n * - If `signal.aborted === true` on entry → returns a rejected Promise with\n * `new DOMException('Prompt aborted', 'AbortError')`, does NOT set\n * `activePrompt`.\n * - Otherwise registers a once-only listener that rejects with the same\n * `DOMException` on abort, clearing the active state.\n *\n * `AbortError` is the Web Platform convention (matches `fetch()`,\n * `Response.body.cancel()`, etc.) — callers can branch on `err.name ===\n * 'AbortError'` without importing any mcp-ui type.\n */\n\nimport { createSignal, type Accessor } from 'solid-js'\nimport type { ChatPromptConfig, ChatPromptResponse } from '../types/chat-bus'\n\n// ─── Error class ─────────────────────────────────────────────\n\n/**\n * Thrown when an active `showChatPrompt` Promise is rejected because a new\n * prompt arrived before the previous one resolved. Consumers can use\n * `instanceof PromptReplacedError` or `err.name === 'PromptReplacedError'` to\n * branch (retry, bail, log).\n *\n * @experimental\n * @since v5.2.0\n */\nexport class PromptReplacedError extends Error {\n readonly name = 'PromptReplacedError' as const\n constructor(message = 'Prompt replaced by a newer one') {\n super(message)\n }\n}\n\n// ─── Controller shape ────────────────────────────────────────\n\nexport interface ChatPromptController {\n /**\n * Register as the bus handler :\n * `bus.commands.handle('showChatPrompt', ctrl.handle)`\n */\n handle: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>\n\n /**\n * Reactive accessor for the currently active prompt config (null when no\n * prompt is pending). Use in JSX to drive `<ChatPrompt>` rendering.\n */\n activePrompt: Accessor<ChatPromptConfig | null>\n\n /** Call this from `<ChatPrompt>`'s `onSubmit` prop. */\n resolveActive: (response: ChatPromptResponse) => void\n\n /** Call this from `<ChatPrompt>`'s `onDismiss` prop. */\n dismissActive: () => void\n\n /**\n * Cancel the active prompt programmatically (e.g. on route change). Rejects\n * the pending Promise with the supplied reason or an `AbortError`.\n */\n abort: (reason?: string) => void\n}\n\n// ─── Factory ─────────────────────────────────────────────────\n\n/**\n * Create a stateful controller that owns the active prompt Promise, the\n * AbortSignal listener, and the re-entrance policy. See module JSDoc for\n * full usage.\n *\n * @experimental\n * @since v5.2.0\n */\nexport function createChatPromptController(): ChatPromptController {\n const [activePrompt, setActivePrompt] = createSignal<ChatPromptConfig | null>(null)\n\n interface PendingEntry {\n type: ChatPromptConfig['type']\n resolve: (r: ChatPromptResponse) => void\n reject: (err: unknown) => void\n signal?: AbortSignal\n onAbort?: () => void\n }\n\n let pending: PendingEntry | null = null\n\n function cleanupAbort(entry: PendingEntry): void {\n if (entry.signal && entry.onAbort) {\n entry.signal.removeEventListener('abort', entry.onAbort)\n }\n }\n\n function clearPending(): void {\n if (pending) {\n cleanupAbort(pending)\n pending = null\n }\n setActivePrompt(null)\n }\n\n function handle(\n config: ChatPromptConfig,\n signal?: AbortSignal\n ): Promise<ChatPromptResponse> {\n // Re-entrance : synchronously reject the previous Promise before\n // installing the new prompt. The caller's .catch sees the rejection\n // on the microtask boundary regardless.\n if (pending) {\n const previous = pending\n pending = null\n cleanupAbort(previous)\n previous.reject(new PromptReplacedError())\n }\n\n // Abort already tripped on entry : return a rejected Promise without\n // ever showing the UI.\n if (signal?.aborted) {\n setActivePrompt(null)\n return Promise.reject(new DOMException('Prompt aborted', 'AbortError'))\n }\n\n return new Promise<ChatPromptResponse>((resolve, reject) => {\n const entry: PendingEntry = { type: config.type, resolve, reject, signal }\n\n if (signal) {\n entry.onAbort = () => {\n // If this entry is still active, reject + clear. If a newer prompt\n // has since replaced it, the cleanup already ran — no-op.\n if (pending === entry) {\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n reject(new DOMException('Prompt aborted', 'AbortError'))\n }\n }\n signal.addEventListener('abort', entry.onAbort, { once: true })\n }\n\n pending = entry\n setActivePrompt(config)\n })\n }\n\n function resolveActive(response: ChatPromptResponse): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n entry.resolve(response)\n }\n\n function dismissActive(): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n // Surface as a resolved Promise with dismissed: true — matches existing\n // ChatPrompt onDismiss contract from v4.x.\n entry.resolve({ type: entry.type, value: '', label: '', dismissed: true })\n }\n\n function abort(reason = 'Prompt aborted'): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n entry.reject(new DOMException(reason, 'AbortError'))\n }\n\n return {\n handle,\n activePrompt,\n resolveActive,\n dismissActive,\n abort,\n }\n}\n"],"names":["createSignal"],"mappings":";;;;;;AA8DO,MAAM,4BAA4B,MAAM;AAAA,EAE7C,YAAY,UAAU,kCAAkC;AACtD,UAAM,OAAO;AAFN,gCAAO;AAAA,EAGhB;AACF;AAwCO,SAAS,6BAAmD;AACjE,QAAM,CAAC,cAAc,eAAe,IAAIA,QAAAA,aAAsC,IAAI;AAUlF,MAAI,UAA+B;AAEnC,WAAS,aAAa,OAA2B;AAC/C,QAAI,MAAM,UAAU,MAAM,SAAS;AACjC,YAAM,OAAO,oBAAoB,SAAS,MAAM,OAAO;AAAA,IACzD;AAAA,EACF;AAUA,WAAS,OACP,QACA,QAC6B;AAI7B,QAAI,SAAS;AACX,YAAM,WAAW;AACjB,gBAAU;AACV,mBAAa,QAAQ;AACrB,eAAS,OAAO,IAAI,qBAAqB;AAAA,IAC3C;AAIA,QAAI,iCAAQ,SAAS;AACnB,sBAAgB,IAAI;AACpB,aAAO,QAAQ,OAAO,IAAI,aAAa,kBAAkB,YAAY,CAAC;AAAA,IACxE;AAEA,WAAO,IAAI,QAA4B,CAAC,SAAS,WAAW;AAC1D,YAAM,QAAsB,EAAE,MAAM,OAAO,MAAM,SAAS,QAAQ,OAAA;AAElE,UAAI,QAAQ;AACV,cAAM,UAAU,MAAM;AAGpB,cAAI,YAAY,OAAO;AACrB,sBAAU;AACV,yBAAa,KAAK;AAClB,4BAAgB,IAAI;AACpB,mBAAO,IAAI,aAAa,kBAAkB,YAAY,CAAC;AAAA,UACzD;AAAA,QACF;AACA,eAAO,iBAAiB,SAAS,MAAM,SAAS,EAAE,MAAM,MAAM;AAAA,MAChE;AAEA,gBAAU;AACV,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAAA,EACH;AAEA,WAAS,cAAc,UAAoC;AACzD,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AACpB,UAAM,QAAQ,QAAQ;AAAA,EACxB;AAEA,WAAS,gBAAsB;AAC7B,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AAGpB,UAAM,QAAQ,EAAE,MAAM,MAAM,MAAM,OAAO,IAAI,OAAO,IAAI,WAAW,KAAA,CAAM;AAAA,EAC3E;AAEA,WAAS,MAAM,SAAS,kBAAwB;AAC9C,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AACpB,UAAM,OAAO,IAAI,aAAa,QAAQ,YAAY,CAAC;AAAA,EACrD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;;"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * createChatPromptController — centralised lifecycle for `showChatPrompt`
3
+ *
4
+ * @experimental
5
+ * @since v5.2.0
6
+ *
7
+ * The controller owns the resolver closure, AbortSignal wiring, and
8
+ * re-entrance policy in one primitive. Consumers go from ~20 LOC of manual
9
+ * wiring per app to :
10
+ *
11
+ * ```ts
12
+ * const ctrl = createChatPromptController()
13
+ * bus.commands.handle('showChatPrompt', ctrl.handle)
14
+ * // ...
15
+ * <Show when={ctrl.activePrompt()}>
16
+ * {(cfg) => (
17
+ * <ChatPrompt
18
+ * config={cfg()}
19
+ * onSubmit={ctrl.resolveActive}
20
+ * onDismiss={ctrl.dismissActive}
21
+ * />
22
+ * )}
23
+ * </Show>
24
+ * ```
25
+ *
26
+ * ## Re-entrance policy
27
+ *
28
+ * If a new `showChatPrompt` arrives while a previous Promise is still
29
+ * pending, the previous Promise rejects **synchronously** with a
30
+ * `PromptReplacedError` before the new prompt is installed. Callers that
31
+ * care can branch on `err instanceof PromptReplacedError` or `err.name ===
32
+ * 'PromptReplacedError'`.
33
+ *
34
+ * ## Abort semantics
35
+ *
36
+ * `handle(config, signal?)` honours `AbortSignal` :
37
+ *
38
+ * - If `signal.aborted === true` on entry → returns a rejected Promise with
39
+ * `new DOMException('Prompt aborted', 'AbortError')`, does NOT set
40
+ * `activePrompt`.
41
+ * - Otherwise registers a once-only listener that rejects with the same
42
+ * `DOMException` on abort, clearing the active state.
43
+ *
44
+ * `AbortError` is the Web Platform convention (matches `fetch()`,
45
+ * `Response.body.cancel()`, etc.) — callers can branch on `err.name ===
46
+ * 'AbortError'` without importing any mcp-ui type.
47
+ */
48
+ import { type Accessor } from 'solid-js';
49
+ import type { ChatPromptConfig, ChatPromptResponse } from '../types/chat-bus';
50
+ /**
51
+ * Thrown when an active `showChatPrompt` Promise is rejected because a new
52
+ * prompt arrived before the previous one resolved. Consumers can use
53
+ * `instanceof PromptReplacedError` or `err.name === 'PromptReplacedError'` to
54
+ * branch (retry, bail, log).
55
+ *
56
+ * @experimental
57
+ * @since v5.2.0
58
+ */
59
+ export declare class PromptReplacedError extends Error {
60
+ readonly name: "PromptReplacedError";
61
+ constructor(message?: string);
62
+ }
63
+ export interface ChatPromptController {
64
+ /**
65
+ * Register as the bus handler :
66
+ * `bus.commands.handle('showChatPrompt', ctrl.handle)`
67
+ */
68
+ handle: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>;
69
+ /**
70
+ * Reactive accessor for the currently active prompt config (null when no
71
+ * prompt is pending). Use in JSX to drive `<ChatPrompt>` rendering.
72
+ */
73
+ activePrompt: Accessor<ChatPromptConfig | null>;
74
+ /** Call this from `<ChatPrompt>`'s `onSubmit` prop. */
75
+ resolveActive: (response: ChatPromptResponse) => void;
76
+ /** Call this from `<ChatPrompt>`'s `onDismiss` prop. */
77
+ dismissActive: () => void;
78
+ /**
79
+ * Cancel the active prompt programmatically (e.g. on route change). Rejects
80
+ * the pending Promise with the supplied reason or an `AbortError`.
81
+ */
82
+ abort: (reason?: string) => void;
83
+ }
84
+ /**
85
+ * Create a stateful controller that owns the active prompt Promise, the
86
+ * AbortSignal listener, and the re-entrance policy. See module JSDoc for
87
+ * full usage.
88
+ *
89
+ * @experimental
90
+ * @since v5.2.0
91
+ */
92
+ export declare function createChatPromptController(): ChatPromptController;
93
+ //# sourceMappingURL=chat-prompt-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-prompt-controller.d.ts","sourceRoot":"","sources":["../../src/services/chat-prompt-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAI7E;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAG,qBAAqB,CAAS;gBAClC,OAAO,SAAmC;CAGvD;AAID,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAEvF;;;OAGG;IACH,YAAY,EAAE,QAAQ,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAE/C,uDAAuD;IACvD,aAAa,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAErD,wDAAwD;IACxD,aAAa,EAAE,MAAM,IAAI,CAAA;IAEzB;;;OAGG;IACH,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAID;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,IAAI,oBAAoB,CA0GjE"}
@@ -0,0 +1,83 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { createSignal } from "solid-js";
5
+ class PromptReplacedError extends Error {
6
+ constructor(message = "Prompt replaced by a newer one") {
7
+ super(message);
8
+ __publicField(this, "name", "PromptReplacedError");
9
+ }
10
+ }
11
+ function createChatPromptController() {
12
+ const [activePrompt, setActivePrompt] = createSignal(null);
13
+ let pending = null;
14
+ function cleanupAbort(entry) {
15
+ if (entry.signal && entry.onAbort) {
16
+ entry.signal.removeEventListener("abort", entry.onAbort);
17
+ }
18
+ }
19
+ function handle(config, signal) {
20
+ if (pending) {
21
+ const previous = pending;
22
+ pending = null;
23
+ cleanupAbort(previous);
24
+ previous.reject(new PromptReplacedError());
25
+ }
26
+ if (signal == null ? void 0 : signal.aborted) {
27
+ setActivePrompt(null);
28
+ return Promise.reject(new DOMException("Prompt aborted", "AbortError"));
29
+ }
30
+ return new Promise((resolve, reject) => {
31
+ const entry = { type: config.type, resolve, reject, signal };
32
+ if (signal) {
33
+ entry.onAbort = () => {
34
+ if (pending === entry) {
35
+ pending = null;
36
+ cleanupAbort(entry);
37
+ setActivePrompt(null);
38
+ reject(new DOMException("Prompt aborted", "AbortError"));
39
+ }
40
+ };
41
+ signal.addEventListener("abort", entry.onAbort, { once: true });
42
+ }
43
+ pending = entry;
44
+ setActivePrompt(config);
45
+ });
46
+ }
47
+ function resolveActive(response) {
48
+ if (!pending) return;
49
+ const entry = pending;
50
+ pending = null;
51
+ cleanupAbort(entry);
52
+ setActivePrompt(null);
53
+ entry.resolve(response);
54
+ }
55
+ function dismissActive() {
56
+ if (!pending) return;
57
+ const entry = pending;
58
+ pending = null;
59
+ cleanupAbort(entry);
60
+ setActivePrompt(null);
61
+ entry.resolve({ type: entry.type, value: "", label: "", dismissed: true });
62
+ }
63
+ function abort(reason = "Prompt aborted") {
64
+ if (!pending) return;
65
+ const entry = pending;
66
+ pending = null;
67
+ cleanupAbort(entry);
68
+ setActivePrompt(null);
69
+ entry.reject(new DOMException(reason, "AbortError"));
70
+ }
71
+ return {
72
+ handle,
73
+ activePrompt,
74
+ resolveActive,
75
+ dismissActive,
76
+ abort
77
+ };
78
+ }
79
+ export {
80
+ PromptReplacedError,
81
+ createChatPromptController
82
+ };
83
+ //# sourceMappingURL=chat-prompt-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-prompt-controller.js","sources":["../../src/services/chat-prompt-controller.ts"],"sourcesContent":["/**\n * createChatPromptController — centralised lifecycle for `showChatPrompt`\n *\n * @experimental\n * @since v5.2.0\n *\n * The controller owns the resolver closure, AbortSignal wiring, and\n * re-entrance policy in one primitive. Consumers go from ~20 LOC of manual\n * wiring per app to :\n *\n * ```ts\n * const ctrl = createChatPromptController()\n * bus.commands.handle('showChatPrompt', ctrl.handle)\n * // ...\n * <Show when={ctrl.activePrompt()}>\n * {(cfg) => (\n * <ChatPrompt\n * config={cfg()}\n * onSubmit={ctrl.resolveActive}\n * onDismiss={ctrl.dismissActive}\n * />\n * )}\n * </Show>\n * ```\n *\n * ## Re-entrance policy\n *\n * If a new `showChatPrompt` arrives while a previous Promise is still\n * pending, the previous Promise rejects **synchronously** with a\n * `PromptReplacedError` before the new prompt is installed. Callers that\n * care can branch on `err instanceof PromptReplacedError` or `err.name ===\n * 'PromptReplacedError'`.\n *\n * ## Abort semantics\n *\n * `handle(config, signal?)` honours `AbortSignal` :\n *\n * - If `signal.aborted === true` on entry → returns a rejected Promise with\n * `new DOMException('Prompt aborted', 'AbortError')`, does NOT set\n * `activePrompt`.\n * - Otherwise registers a once-only listener that rejects with the same\n * `DOMException` on abort, clearing the active state.\n *\n * `AbortError` is the Web Platform convention (matches `fetch()`,\n * `Response.body.cancel()`, etc.) — callers can branch on `err.name ===\n * 'AbortError'` without importing any mcp-ui type.\n */\n\nimport { createSignal, type Accessor } from 'solid-js'\nimport type { ChatPromptConfig, ChatPromptResponse } from '../types/chat-bus'\n\n// ─── Error class ─────────────────────────────────────────────\n\n/**\n * Thrown when an active `showChatPrompt` Promise is rejected because a new\n * prompt arrived before the previous one resolved. Consumers can use\n * `instanceof PromptReplacedError` or `err.name === 'PromptReplacedError'` to\n * branch (retry, bail, log).\n *\n * @experimental\n * @since v5.2.0\n */\nexport class PromptReplacedError extends Error {\n readonly name = 'PromptReplacedError' as const\n constructor(message = 'Prompt replaced by a newer one') {\n super(message)\n }\n}\n\n// ─── Controller shape ────────────────────────────────────────\n\nexport interface ChatPromptController {\n /**\n * Register as the bus handler :\n * `bus.commands.handle('showChatPrompt', ctrl.handle)`\n */\n handle: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>\n\n /**\n * Reactive accessor for the currently active prompt config (null when no\n * prompt is pending). Use in JSX to drive `<ChatPrompt>` rendering.\n */\n activePrompt: Accessor<ChatPromptConfig | null>\n\n /** Call this from `<ChatPrompt>`'s `onSubmit` prop. */\n resolveActive: (response: ChatPromptResponse) => void\n\n /** Call this from `<ChatPrompt>`'s `onDismiss` prop. */\n dismissActive: () => void\n\n /**\n * Cancel the active prompt programmatically (e.g. on route change). Rejects\n * the pending Promise with the supplied reason or an `AbortError`.\n */\n abort: (reason?: string) => void\n}\n\n// ─── Factory ─────────────────────────────────────────────────\n\n/**\n * Create a stateful controller that owns the active prompt Promise, the\n * AbortSignal listener, and the re-entrance policy. See module JSDoc for\n * full usage.\n *\n * @experimental\n * @since v5.2.0\n */\nexport function createChatPromptController(): ChatPromptController {\n const [activePrompt, setActivePrompt] = createSignal<ChatPromptConfig | null>(null)\n\n interface PendingEntry {\n type: ChatPromptConfig['type']\n resolve: (r: ChatPromptResponse) => void\n reject: (err: unknown) => void\n signal?: AbortSignal\n onAbort?: () => void\n }\n\n let pending: PendingEntry | null = null\n\n function cleanupAbort(entry: PendingEntry): void {\n if (entry.signal && entry.onAbort) {\n entry.signal.removeEventListener('abort', entry.onAbort)\n }\n }\n\n function clearPending(): void {\n if (pending) {\n cleanupAbort(pending)\n pending = null\n }\n setActivePrompt(null)\n }\n\n function handle(\n config: ChatPromptConfig,\n signal?: AbortSignal\n ): Promise<ChatPromptResponse> {\n // Re-entrance : synchronously reject the previous Promise before\n // installing the new prompt. The caller's .catch sees the rejection\n // on the microtask boundary regardless.\n if (pending) {\n const previous = pending\n pending = null\n cleanupAbort(previous)\n previous.reject(new PromptReplacedError())\n }\n\n // Abort already tripped on entry : return a rejected Promise without\n // ever showing the UI.\n if (signal?.aborted) {\n setActivePrompt(null)\n return Promise.reject(new DOMException('Prompt aborted', 'AbortError'))\n }\n\n return new Promise<ChatPromptResponse>((resolve, reject) => {\n const entry: PendingEntry = { type: config.type, resolve, reject, signal }\n\n if (signal) {\n entry.onAbort = () => {\n // If this entry is still active, reject + clear. If a newer prompt\n // has since replaced it, the cleanup already ran — no-op.\n if (pending === entry) {\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n reject(new DOMException('Prompt aborted', 'AbortError'))\n }\n }\n signal.addEventListener('abort', entry.onAbort, { once: true })\n }\n\n pending = entry\n setActivePrompt(config)\n })\n }\n\n function resolveActive(response: ChatPromptResponse): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n entry.resolve(response)\n }\n\n function dismissActive(): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n // Surface as a resolved Promise with dismissed: true — matches existing\n // ChatPrompt onDismiss contract from v4.x.\n entry.resolve({ type: entry.type, value: '', label: '', dismissed: true })\n }\n\n function abort(reason = 'Prompt aborted'): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n entry.reject(new DOMException(reason, 'AbortError'))\n }\n\n return {\n handle,\n activePrompt,\n resolveActive,\n dismissActive,\n abort,\n }\n}\n"],"names":[],"mappings":";;;;AA8DO,MAAM,4BAA4B,MAAM;AAAA,EAE7C,YAAY,UAAU,kCAAkC;AACtD,UAAM,OAAO;AAFN,gCAAO;AAAA,EAGhB;AACF;AAwCO,SAAS,6BAAmD;AACjE,QAAM,CAAC,cAAc,eAAe,IAAI,aAAsC,IAAI;AAUlF,MAAI,UAA+B;AAEnC,WAAS,aAAa,OAA2B;AAC/C,QAAI,MAAM,UAAU,MAAM,SAAS;AACjC,YAAM,OAAO,oBAAoB,SAAS,MAAM,OAAO;AAAA,IACzD;AAAA,EACF;AAUA,WAAS,OACP,QACA,QAC6B;AAI7B,QAAI,SAAS;AACX,YAAM,WAAW;AACjB,gBAAU;AACV,mBAAa,QAAQ;AACrB,eAAS,OAAO,IAAI,qBAAqB;AAAA,IAC3C;AAIA,QAAI,iCAAQ,SAAS;AACnB,sBAAgB,IAAI;AACpB,aAAO,QAAQ,OAAO,IAAI,aAAa,kBAAkB,YAAY,CAAC;AAAA,IACxE;AAEA,WAAO,IAAI,QAA4B,CAAC,SAAS,WAAW;AAC1D,YAAM,QAAsB,EAAE,MAAM,OAAO,MAAM,SAAS,QAAQ,OAAA;AAElE,UAAI,QAAQ;AACV,cAAM,UAAU,MAAM;AAGpB,cAAI,YAAY,OAAO;AACrB,sBAAU;AACV,yBAAa,KAAK;AAClB,4BAAgB,IAAI;AACpB,mBAAO,IAAI,aAAa,kBAAkB,YAAY,CAAC;AAAA,UACzD;AAAA,QACF;AACA,eAAO,iBAAiB,SAAS,MAAM,SAAS,EAAE,MAAM,MAAM;AAAA,MAChE;AAEA,gBAAU;AACV,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAAA,EACH;AAEA,WAAS,cAAc,UAAoC;AACzD,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AACpB,UAAM,QAAQ,QAAQ;AAAA,EACxB;AAEA,WAAS,gBAAsB;AAC7B,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AAGpB,UAAM,QAAQ,EAAE,MAAM,MAAM,MAAM,OAAO,IAAI,OAAO,IAAI,WAAW,KAAA,CAAM;AAAA,EAC3E;AAEA,WAAS,MAAM,SAAS,kBAAwB;AAC9C,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AACpB,UAAM,OAAO,IAAI,aAAa,QAAQ,YAAY,CAAC;AAAA,EACrD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -1,90 +1,118 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const web = require("solid-js/web");
4
+ const solidJs = require("solid-js");
3
5
  const store = require("solid-js/store");
4
- const [scratchpadStore, setScratchpadStore] = store.createStore({ current: null, pinned: false });
5
- function useScratchpadState() {
6
+ function createScratchpadStore() {
7
+ const [scratchpadStore, setScratchpadStore] = store.createStore({
8
+ current: null,
9
+ pinned: false
10
+ });
11
+ const dispatch = (event) => {
12
+ var _a, _b;
13
+ if (event.action === "create") {
14
+ console.info(`%c[MCP-UI] dispatchScratchpad%c create id=${event.id} sections=${((_a = event.sections) == null ? void 0 : _a.length) || 0} status=${event.status || "loading"}${event.pinned ? " pinned" : ""}`, "color: #10b981; font-weight: bold", "color: inherit");
15
+ setScratchpadStore({
16
+ current: {
17
+ id: event.id,
18
+ title: event.title || "",
19
+ sections: event.sections || [],
20
+ filters: event.filters || {},
21
+ preview: event.preview,
22
+ agentMessages: event.agentMessages || [],
23
+ status: event.status || "loading",
24
+ previewEndpoint: event.previewEndpoint,
25
+ previewDebounce: event.previewDebounce,
26
+ previewMethod: event.previewMethod,
27
+ previewHeaders: event.previewHeaders,
28
+ turn: event.turn,
29
+ totalTurns: event.totalTurns,
30
+ turnHistory: event.turnHistory
31
+ },
32
+ pinned: event.pinned || false
33
+ });
34
+ } else if (event.action === "update") {
35
+ console.info(`%c[MCP-UI] dispatchScratchpad%c update id=${event.id} sectionMode=${event.sectionMode || "replace"} sections=${((_b = event.sections) == null ? void 0 : _b.length) || 0} status=${event.status || "-"}`, "color: #3b82f6; font-weight: bold", "color: inherit");
36
+ setScratchpadStore(store.produce((s) => {
37
+ var _a2;
38
+ if (!s.current || s.current.id !== event.id) {
39
+ console.warn(`[MCP-UI] dispatchScratchpad: update for id=${event.id} but current is ${((_a2 = s.current) == null ? void 0 : _a2.id) || "null"}. Ignoring.`);
40
+ return;
41
+ }
42
+ if (event.sections) {
43
+ const mode = event.sectionMode || "replace";
44
+ if (mode === "replace") {
45
+ s.current.sections = event.sections;
46
+ } else if (mode === "append") {
47
+ s.current.sections = [...s.current.sections, ...event.sections];
48
+ } else if (mode === "upsert") {
49
+ let matchCount = 0;
50
+ for (const incoming of event.sections) {
51
+ const idx = s.current.sections.findIndex((sec) => sec.id === incoming.id);
52
+ if (idx >= 0) {
53
+ s.current.sections[idx] = incoming;
54
+ matchCount++;
55
+ } else {
56
+ s.current.sections.push(incoming);
57
+ }
58
+ }
59
+ if (matchCount === 0 && event.sections.length > 0) {
60
+ console.warn(`[MCP-UI] dispatchScratchpad: sectionMode='upsert' but no IDs matched. Incoming: [${event.sections.map((s2) => s2.id).join(", ")}] Existing: [${s.current.sections.map((s2) => s2.id).join(", ")}]. All appended.`);
61
+ }
62
+ }
63
+ }
64
+ if (event.agentMessages) s.current.agentMessages = event.agentMessages;
65
+ if (event.status) s.current.status = event.status;
66
+ if (event.filters) s.current.filters = event.filters;
67
+ if (event.preview) s.current.preview = event.preview;
68
+ if (event.pinned != null) s.pinned = event.pinned;
69
+ if (event.turnHistory) s.current.turnHistory = event.turnHistory;
70
+ if (event.turn != null) s.current.turn = event.turn;
71
+ }));
72
+ } else if (event.action === "close") {
73
+ console.info(`%c[MCP-UI] dispatchScratchpad%c close id=${event.id}`, "color: #6b7280; font-weight: bold", "color: inherit");
74
+ setScratchpadStore({
75
+ current: null,
76
+ pinned: false
77
+ });
78
+ }
79
+ };
6
80
  return {
81
+ dispatch,
7
82
  state: () => scratchpadStore.current,
8
83
  pinned: () => scratchpadStore.pinned,
9
- close: () => setScratchpadStore({ current: null, pinned: false })
84
+ close: () => setScratchpadStore({
85
+ current: null,
86
+ pinned: false
87
+ })
10
88
  };
11
89
  }
90
+ const defaultStore = createScratchpadStore();
12
91
  function dispatchScratchpad(event) {
13
- var _a, _b;
14
- if (event.action === "create") {
15
- console.info(
16
- `%c[MCP-UI] dispatchScratchpad%c create id=${event.id} sections=${((_a = event.sections) == null ? void 0 : _a.length) || 0} status=${event.status || "loading"}${event.pinned ? " pinned" : ""}`,
17
- "color: #10b981; font-weight: bold",
18
- "color: inherit"
19
- );
20
- setScratchpadStore({
21
- current: {
22
- id: event.id,
23
- title: event.title || "",
24
- sections: event.sections || [],
25
- filters: event.filters || {},
26
- preview: event.preview,
27
- agentMessages: event.agentMessages || [],
28
- status: event.status || "loading",
29
- previewEndpoint: event.previewEndpoint,
30
- previewDebounce: event.previewDebounce,
31
- previewMethod: event.previewMethod,
32
- previewHeaders: event.previewHeaders,
33
- turn: event.turn,
34
- totalTurns: event.totalTurns,
35
- turnHistory: event.turnHistory
36
- },
37
- pinned: event.pinned || false
38
- });
39
- } else if (event.action === "update") {
40
- console.info(
41
- `%c[MCP-UI] dispatchScratchpad%c update id=${event.id} sectionMode=${event.sectionMode || "replace"} sections=${((_b = event.sections) == null ? void 0 : _b.length) || 0} status=${event.status || "-"}`,
42
- "color: #3b82f6; font-weight: bold",
43
- "color: inherit"
44
- );
45
- setScratchpadStore(store.produce((s) => {
46
- var _a2;
47
- if (!s.current || s.current.id !== event.id) {
48
- console.warn(`[MCP-UI] dispatchScratchpad: update for id=${event.id} but current is ${((_a2 = s.current) == null ? void 0 : _a2.id) || "null"}. Ignoring.`);
49
- return;
50
- }
51
- if (event.sections) {
52
- const mode = event.sectionMode || "replace";
53
- if (mode === "replace") {
54
- s.current.sections = event.sections;
55
- } else if (mode === "append") {
56
- s.current.sections = [...s.current.sections, ...event.sections];
57
- } else if (mode === "upsert") {
58
- let matchCount = 0;
59
- for (const incoming of event.sections) {
60
- const idx = s.current.sections.findIndex((sec) => sec.id === incoming.id);
61
- if (idx >= 0) {
62
- s.current.sections[idx] = incoming;
63
- matchCount++;
64
- } else {
65
- s.current.sections.push(incoming);
66
- }
67
- }
68
- if (matchCount === 0 && event.sections.length > 0) {
69
- console.warn(
70
- `[MCP-UI] dispatchScratchpad: sectionMode='upsert' but no IDs matched. Incoming: [${event.sections.map((s2) => s2.id).join(", ")}] Existing: [${s.current.sections.map((s2) => s2.id).join(", ")}]. All appended.`
71
- );
72
- }
73
- }
74
- }
75
- if (event.agentMessages) s.current.agentMessages = event.agentMessages;
76
- if (event.status) s.current.status = event.status;
77
- if (event.filters) s.current.filters = event.filters;
78
- if (event.preview) s.current.preview = event.preview;
79
- if (event.pinned != null) s.pinned = event.pinned;
80
- if (event.turnHistory) s.current.turnHistory = event.turnHistory;
81
- if (event.turn != null) s.current.turn = event.turn;
82
- }));
83
- } else if (event.action === "close") {
84
- console.info(`%c[MCP-UI] dispatchScratchpad%c close id=${event.id}`, "color: #6b7280; font-weight: bold", "color: inherit");
85
- setScratchpadStore({ current: null, pinned: false });
86
- }
92
+ defaultStore.dispatch(event);
93
+ }
94
+ const ScratchpadStoreContext = solidJs.createContext(void 0);
95
+ const ScratchpadStoreProvider = (props) => {
96
+ const store2 = props.store ?? createScratchpadStore();
97
+ return web.createComponent(ScratchpadStoreContext.Provider, {
98
+ value: store2,
99
+ get children() {
100
+ return props.children;
101
+ }
102
+ });
103
+ };
104
+ function useScratchpadState() {
105
+ const scoped = solidJs.useContext(ScratchpadStoreContext);
106
+ const handle = scoped ?? defaultStore;
107
+ return {
108
+ state: handle.state,
109
+ pinned: handle.pinned,
110
+ close: handle.close
111
+ };
87
112
  }
113
+ exports.ScratchpadStoreContext = ScratchpadStoreContext;
114
+ exports.ScratchpadStoreProvider = ScratchpadStoreProvider;
115
+ exports.createScratchpadStore = createScratchpadStore;
88
116
  exports.dispatchScratchpad = dispatchScratchpad;
89
117
  exports.useScratchpadState = useScratchpadState;
90
118
  //# sourceMappingURL=scratchpad-store.cjs.map