@seed-ship/mcp-ui-solid 5.1.0 → 5.2.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 (48) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +64 -13
  3. package/dist/components/FeedbackInline.cjs +57 -0
  4. package/dist/components/FeedbackInline.cjs.map +1 -0
  5. package/dist/components/FeedbackInline.d.ts +71 -0
  6. package/dist/components/FeedbackInline.d.ts.map +1 -0
  7. package/dist/components/FeedbackInline.js +57 -0
  8. package/dist/components/FeedbackInline.js.map +1 -0
  9. package/dist/index.cjs +9 -0
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +8 -2
  12. package/dist/index.d.ts +8 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +11 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/services/chat-bus.cjs +71 -0
  17. package/dist/services/chat-bus.cjs.map +1 -1
  18. package/dist/services/chat-bus.d.ts +31 -1
  19. package/dist/services/chat-bus.d.ts.map +1 -1
  20. package/dist/services/chat-bus.js +71 -0
  21. package/dist/services/chat-bus.js.map +1 -1
  22. package/dist/services/chat-prompt-controller.cjs +83 -0
  23. package/dist/services/chat-prompt-controller.cjs.map +1 -0
  24. package/dist/services/chat-prompt-controller.d.ts +93 -0
  25. package/dist/services/chat-prompt-controller.d.ts.map +1 -0
  26. package/dist/services/chat-prompt-controller.js +83 -0
  27. package/dist/services/chat-prompt-controller.js.map +1 -0
  28. package/dist/stores/scratchpad-store.cjs +105 -77
  29. package/dist/stores/scratchpad-store.cjs.map +1 -1
  30. package/dist/stores/scratchpad-store.d.ts +88 -19
  31. package/dist/stores/scratchpad-store.d.ts.map +1 -1
  32. package/dist/stores/scratchpad-store.js +105 -77
  33. package/dist/stores/scratchpad-store.js.map +1 -1
  34. package/dist/types/chat-bus.d.ts +39 -0
  35. package/dist/types/chat-bus.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/FeedbackInline.test.tsx +117 -0
  38. package/src/components/FeedbackInline.tsx +143 -0
  39. package/src/index.ts +23 -1
  40. package/src/services/chat-bus.test.ts +154 -2
  41. package/src/services/chat-bus.ts +115 -0
  42. package/src/services/chat-prompt-controller.test.ts +144 -0
  43. package/src/services/chat-prompt-controller.ts +214 -0
  44. package/src/stores/scratchpad-store.test.tsx +140 -0
  45. package/src/stores/scratchpad-store.tsx +244 -0
  46. package/src/types/chat-bus.ts +40 -0
  47. package/tsconfig.tsbuildinfo +1 -1
  48. package/src/stores/scratchpad-store.ts +0 -126
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Scratchpad Store — reactive state for HITL scratchpad
3
+ *
4
+ * @experimental
5
+ *
6
+ * **v5.2.0 :** the store is now a factory (`createScratchpadStore()`) with a
7
+ * module-level singleton kept as default. Two consumption modes :
8
+ *
9
+ * 1. **Singleton mode (default, zero-config)** — `dispatchScratchpad(event)` +
10
+ * `useScratchpadState()` read/write the module singleton. This is the v4.x
11
+ * path and keeps working unchanged.
12
+ *
13
+ * 2. **Multi-instance mode** — wrap a subtree in `<ScratchpadStoreProvider>`
14
+ * (it creates a scoped `ScratchpadStoreHandle` internally, or use your own
15
+ * via the `store` prop). `useScratchpadState()` auto-detects the context
16
+ * and reads from it; `ScratchpadPanel` mounted inside the provider reads
17
+ * the scoped store. Non-reactive callers (SSE parsers) should pass the
18
+ * handle explicitly — do NOT try to reach context from a non-reactive
19
+ * scope.
20
+ */
21
+
22
+ import { createContext, useContext, type ParentComponent, type JSX } from 'solid-js'
23
+ import { createStore, produce } from 'solid-js/store'
24
+ import type { ScratchpadState, ScratchpadEvent, ScratchpadSection } from '../types/chat-bus'
25
+
26
+ // ─── Handle shape ─────────────────────────────────────────────
27
+
28
+ export interface ScratchpadStoreHandle {
29
+ /** Mutate the store from an SSE/parser callback. */
30
+ dispatch: (event: ScratchpadEvent) => void
31
+ /** Reactive accessor for the current scratchpad state (null when closed). */
32
+ state: () => ScratchpadState | null
33
+ /** Reactive accessor for the pinned flag. */
34
+ pinned: () => boolean
35
+ /** Close the scratchpad (equivalent to dispatching an action='close'). */
36
+ close: () => void
37
+ }
38
+
39
+ // ─── Factory ──────────────────────────────────────────────────
40
+
41
+ /**
42
+ * Create an isolated scratchpad store instance.
43
+ *
44
+ * Use this when you need two or more scratchpads live at the same time
45
+ * (e.g. chat scratchpad + admin dashboard scratchpad). Pair with
46
+ * `<ScratchpadStoreProvider store={...}>` to scope a SolidJS subtree.
47
+ *
48
+ * @experimental
49
+ * @since v5.2.0
50
+ */
51
+ export function createScratchpadStore(): ScratchpadStoreHandle {
52
+ const [scratchpadStore, setScratchpadStore] = createStore<{
53
+ current: ScratchpadState | null
54
+ pinned: boolean
55
+ }>({ current: null, pinned: false })
56
+
57
+ const dispatch = (event: ScratchpadEvent): void => {
58
+ if (event.action === 'create') {
59
+ console.info(
60
+ `%c[MCP-UI] dispatchScratchpad%c create id=${event.id} sections=${event.sections?.length || 0} status=${event.status || 'loading'}${event.pinned ? ' pinned' : ''}`,
61
+ 'color: #10b981; font-weight: bold',
62
+ 'color: inherit'
63
+ )
64
+ setScratchpadStore({
65
+ current: {
66
+ id: event.id,
67
+ title: event.title || '',
68
+ sections: event.sections || [],
69
+ filters: event.filters || {},
70
+ preview: event.preview,
71
+ agentMessages: event.agentMessages || [],
72
+ status: event.status || 'loading',
73
+ previewEndpoint: (event as any).previewEndpoint,
74
+ previewDebounce: (event as any).previewDebounce,
75
+ previewMethod: (event as any).previewMethod,
76
+ previewHeaders: (event as any).previewHeaders,
77
+ turn: (event as any).turn,
78
+ totalTurns: (event as any).totalTurns,
79
+ turnHistory: (event as any).turnHistory,
80
+ },
81
+ pinned: event.pinned || false,
82
+ })
83
+ } else if (event.action === 'update') {
84
+ console.info(
85
+ `%c[MCP-UI] dispatchScratchpad%c update id=${event.id} sectionMode=${event.sectionMode || 'replace'} sections=${event.sections?.length || 0} status=${event.status || '-'}`,
86
+ 'color: #3b82f6; font-weight: bold',
87
+ 'color: inherit'
88
+ )
89
+ setScratchpadStore(
90
+ produce((s) => {
91
+ if (!s.current || s.current.id !== event.id) {
92
+ console.warn(
93
+ `[MCP-UI] dispatchScratchpad: update for id=${event.id} but current is ${s.current?.id || 'null'}. Ignoring.`
94
+ )
95
+ return
96
+ }
97
+
98
+ if (event.sections) {
99
+ const mode = event.sectionMode || 'replace'
100
+ if (mode === 'replace') {
101
+ s.current.sections = event.sections
102
+ } else if (mode === 'append') {
103
+ s.current.sections = [...s.current.sections, ...event.sections]
104
+ } else if (mode === 'upsert') {
105
+ let matchCount = 0
106
+ for (const incoming of event.sections) {
107
+ const idx = s.current.sections.findIndex(
108
+ (sec: ScratchpadSection) => sec.id === incoming.id
109
+ )
110
+ if (idx >= 0) {
111
+ s.current.sections[idx] = incoming
112
+ matchCount++
113
+ } else {
114
+ s.current.sections.push(incoming)
115
+ }
116
+ }
117
+ if (matchCount === 0 && event.sections.length > 0) {
118
+ console.warn(
119
+ `[MCP-UI] dispatchScratchpad: sectionMode='upsert' but no IDs matched. ` +
120
+ `Incoming: [${event.sections.map((s: ScratchpadSection) => s.id).join(', ')}] ` +
121
+ `Existing: [${s.current.sections.map((s: ScratchpadSection) => s.id).join(', ')}]. All appended.`
122
+ )
123
+ }
124
+ }
125
+ }
126
+ if (event.agentMessages) s.current.agentMessages = event.agentMessages
127
+ if (event.status) s.current.status = event.status
128
+ if (event.filters) s.current.filters = event.filters
129
+ if (event.preview) s.current.preview = event.preview
130
+ if (event.pinned != null) s.pinned = event.pinned
131
+ if ((event as any).turnHistory) s.current.turnHistory = (event as any).turnHistory
132
+ if ((event as any).turn != null) s.current.turn = (event as any).turn
133
+ })
134
+ )
135
+ } else if (event.action === 'close') {
136
+ console.info(
137
+ `%c[MCP-UI] dispatchScratchpad%c close id=${event.id}`,
138
+ 'color: #6b7280; font-weight: bold',
139
+ 'color: inherit'
140
+ )
141
+ setScratchpadStore({ current: null, pinned: false })
142
+ }
143
+ }
144
+
145
+ return {
146
+ dispatch,
147
+ state: () => scratchpadStore.current,
148
+ pinned: () => scratchpadStore.pinned,
149
+ close: () => setScratchpadStore({ current: null, pinned: false }),
150
+ }
151
+ }
152
+
153
+ // ─── Module-level singleton (v4.x-compatible default) ─────────
154
+
155
+ const defaultStore: ScratchpadStoreHandle = createScratchpadStore()
156
+
157
+ /**
158
+ * Function for the PARSER/STORE — mutates the **module-level singleton**
159
+ * scratchpad state. Use this when you only need one scratchpad at a time
160
+ * (single-instance consumer, the v4.x pattern).
161
+ *
162
+ * For multi-instance scenarios, prefer `createScratchpadStore()` and pass the
163
+ * handle around explicitly.
164
+ *
165
+ * @example
166
+ * // In your SSE parser callback — ONE LINE
167
+ * onScratchpad: (data) => dispatchScratchpad(data as ScratchpadEvent)
168
+ */
169
+ export function dispatchScratchpad(event: ScratchpadEvent): void {
170
+ defaultStore.dispatch(event)
171
+ }
172
+
173
+ // ─── Context (v5.2.0) ─────────────────────────────────────────
174
+
175
+ /**
176
+ * Context for a scoped scratchpad store. Populated by
177
+ * `<ScratchpadStoreProvider>`. Read by `useScratchpadState()` with automatic
178
+ * fallback to the module-level singleton when the context is absent.
179
+ *
180
+ * @experimental
181
+ * @since v5.2.0
182
+ */
183
+ export const ScratchpadStoreContext = createContext<ScratchpadStoreHandle | undefined>(undefined)
184
+
185
+ /**
186
+ * Provide a scoped `ScratchpadStoreHandle` to a SolidJS subtree. Children
187
+ * reading via `useScratchpadState()` or rendering a `<ScratchpadPanel>` will
188
+ * bind to this store instead of the module singleton.
189
+ *
190
+ * If no `store` prop is passed, a fresh store is created for the provider's
191
+ * lifetime. Pass `store` explicitly when you need the handle outside the
192
+ * tree (e.g. in an SSE parser that lives at the app root).
193
+ *
194
+ * @experimental
195
+ * @since v5.2.0
196
+ *
197
+ * @example
198
+ * const chatStore = createScratchpadStore()
199
+ * const adminStore = createScratchpadStore()
200
+ *
201
+ * <ScratchpadStoreProvider store={chatStore}>
202
+ * <ChatInterface />
203
+ * </ScratchpadStoreProvider>
204
+ * <ScratchpadStoreProvider store={adminStore}>
205
+ * <AdminDashboard />
206
+ * </ScratchpadStoreProvider>
207
+ */
208
+ export const ScratchpadStoreProvider: ParentComponent<{
209
+ store?: ScratchpadStoreHandle
210
+ }> = (props): JSX.Element => {
211
+ const store = props.store ?? createScratchpadStore()
212
+ return (
213
+ <ScratchpadStoreContext.Provider value={store}>{props.children}</ScratchpadStoreContext.Provider>
214
+ )
215
+ }
216
+
217
+ // ─── Reactive hook (context-aware) ────────────────────────────
218
+
219
+ /**
220
+ * Hook for the COMPONENT — reads the scratchpad state reactively.
221
+ *
222
+ * **v5.2.0 :** if called inside a `<ScratchpadStoreProvider>`, reads the
223
+ * scoped handle; otherwise falls back to the module singleton. Old v4.x
224
+ * consumers keep working unchanged.
225
+ *
226
+ * @example
227
+ * const { state, pinned, close } = useScratchpadState()
228
+ * <Show when={state()}>
229
+ * <ScratchpadPanel state={state()!} pinned={pinned()} onClose={close} />
230
+ * </Show>
231
+ */
232
+ export function useScratchpadState(): {
233
+ state: () => ScratchpadState | null
234
+ pinned: () => boolean
235
+ close: () => void
236
+ } {
237
+ const scoped = useContext(ScratchpadStoreContext)
238
+ const handle = scoped ?? defaultStore
239
+ return {
240
+ state: handle.state,
241
+ pinned: handle.pinned,
242
+ close: handle.close,
243
+ }
244
+ }
@@ -53,6 +53,7 @@ export interface ChatEvents {
53
53
  // --- Interactions ---
54
54
  onChatPromptResponse: (event: ChatEventBase & { response: ChatPromptResponse }) => void
55
55
  onClarificationNeeded: (event: ChatEventBase & { clarification: ClarificationEvent }) => void
56
+ onElicitation: (event: ChatEventBase & { elicitation: ElicitationEvent }) => void
56
57
 
57
58
  // --- Agentic (handled by app, not MCP-UI) ---
58
59
  onAgentSwitch: (event: ChatEventBase & { agent: AgentContext }) => void
@@ -568,6 +569,45 @@ export interface ToolCallEvent {
568
569
  duration_ms?: number
569
570
  }
570
571
 
572
+ /**
573
+ * MCP `elicitation/create` request payload — server asks the client to
574
+ * collect input from the user according to a JSON Schema.
575
+ *
576
+ * Derived from MCP spec 2025-06-18. See
577
+ * `elicitationToPromptConfig()` in `services/chat-bus.ts` for the
578
+ * helper that converts this to a `ChatPromptConfig`.
579
+ *
580
+ * @experimental
581
+ * @since v5.2.0
582
+ */
583
+ export interface ElicitationEvent {
584
+ /** Question / instruction to present to the user. */
585
+ message: string
586
+ /** JSON Schema describing the expected response shape. Object with primitive properties only. */
587
+ requestedSchema: ElicitationRequestedSchema
588
+ }
589
+
590
+ export interface ElicitationRequestedSchema {
591
+ type: 'object'
592
+ properties: Record<string, ElicitationPropertySchema>
593
+ required?: string[]
594
+ }
595
+
596
+ export interface ElicitationPropertySchema {
597
+ type: 'string' | 'number' | 'integer' | 'boolean'
598
+ title?: string
599
+ description?: string
600
+ /** Enum of allowed values (strings or numbers). */
601
+ enum?: Array<string | number>
602
+ /** Parallel array with display labels for each enum entry. */
603
+ enumNames?: string[]
604
+ default?: unknown
605
+ minimum?: number
606
+ maximum?: number
607
+ /** String format hint — date, date-time, email, uri. */
608
+ format?: 'date' | 'date-time' | 'email' | 'uri'
609
+ }
610
+
571
611
  export interface ClarificationEvent {
572
612
  /** The question to ask the user */
573
613
  question: string