@seed-ship/mcp-ui-solid 5.0.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.
- package/CHANGELOG.md +85 -0
- package/README.md +160 -6
- package/dist/components/ChatPrompt.cjs +71 -53
- package/dist/components/ChatPrompt.cjs.map +1 -1
- package/dist/components/ChatPrompt.d.ts +37 -2
- package/dist/components/ChatPrompt.d.ts.map +1 -1
- package/dist/components/ChatPrompt.js +72 -54
- package/dist/components/ChatPrompt.js.map +1 -1
- package/dist/components/FeedbackInline.cjs +57 -0
- package/dist/components/FeedbackInline.cjs.map +1 -0
- package/dist/components/FeedbackInline.d.ts +71 -0
- package/dist/components/FeedbackInline.d.ts.map +1 -0
- package/dist/components/FeedbackInline.js +57 -0
- package/dist/components/FeedbackInline.js.map +1 -0
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/services/chat-bus.cjs +71 -0
- package/dist/services/chat-bus.cjs.map +1 -1
- package/dist/services/chat-bus.d.ts +31 -1
- package/dist/services/chat-bus.d.ts.map +1 -1
- package/dist/services/chat-bus.js +71 -0
- package/dist/services/chat-bus.js.map +1 -1
- package/dist/services/chat-prompt-controller.cjs +83 -0
- package/dist/services/chat-prompt-controller.cjs.map +1 -0
- package/dist/services/chat-prompt-controller.d.ts +93 -0
- package/dist/services/chat-prompt-controller.d.ts.map +1 -0
- package/dist/services/chat-prompt-controller.js +83 -0
- package/dist/services/chat-prompt-controller.js.map +1 -0
- package/dist/stores/scratchpad-store.cjs +105 -77
- package/dist/stores/scratchpad-store.cjs.map +1 -1
- package/dist/stores/scratchpad-store.d.ts +88 -19
- package/dist/stores/scratchpad-store.d.ts.map +1 -1
- package/dist/stores/scratchpad-store.js +105 -77
- package/dist/stores/scratchpad-store.js.map +1 -1
- package/dist/types/chat-bus.d.ts +164 -22
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ChatPrompt.test.tsx +122 -0
- package/src/components/ChatPrompt.tsx +70 -15
- package/src/components/FeedbackInline.test.tsx +117 -0
- package/src/components/FeedbackInline.tsx +143 -0
- package/src/index.ts +24 -1
- package/src/services/chat-bus.test.ts +154 -2
- package/src/services/chat-bus.ts +115 -0
- package/src/services/chat-prompt-controller.test.ts +144 -0
- package/src/services/chat-prompt-controller.ts +214 -0
- package/src/stores/scratchpad-store.test.tsx +140 -0
- package/src/stores/scratchpad-store.tsx +244 -0
- package/src/types/chat-bus.ts +166 -22
- package/tsconfig.tsbuildinfo +1 -1
- package/src/stores/scratchpad-store.ts +0 -126
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for scratchpad-store — v5.2.0 createScratchpadStore factory + provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
6
|
+
import { render, cleanup } from '@solidjs/testing-library'
|
|
7
|
+
import {
|
|
8
|
+
createScratchpadStore,
|
|
9
|
+
dispatchScratchpad,
|
|
10
|
+
useScratchpadState,
|
|
11
|
+
ScratchpadStoreProvider,
|
|
12
|
+
} from './scratchpad-store'
|
|
13
|
+
import type { ScratchpadEvent } from '../types/chat-bus'
|
|
14
|
+
|
|
15
|
+
const createEvent = (id: string, title: string): ScratchpadEvent => ({
|
|
16
|
+
id,
|
|
17
|
+
action: 'create',
|
|
18
|
+
title,
|
|
19
|
+
sections: [],
|
|
20
|
+
status: 'ready',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('createScratchpadStore — v5.2.0', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
cleanup()
|
|
26
|
+
// Silence the info/warn logs that dispatch emits
|
|
27
|
+
vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
28
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('two stores do not share state', () => {
|
|
32
|
+
const storeA = createScratchpadStore()
|
|
33
|
+
const storeB = createScratchpadStore()
|
|
34
|
+
|
|
35
|
+
storeA.dispatch(createEvent('a', 'Store A'))
|
|
36
|
+
|
|
37
|
+
expect(storeA.state()?.id).toBe('a')
|
|
38
|
+
expect(storeA.state()?.title).toBe('Store A')
|
|
39
|
+
expect(storeB.state()).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('close() resets state independently per store', () => {
|
|
43
|
+
const storeA = createScratchpadStore()
|
|
44
|
+
const storeB = createScratchpadStore()
|
|
45
|
+
|
|
46
|
+
storeA.dispatch(createEvent('a', 'Store A'))
|
|
47
|
+
storeB.dispatch(createEvent('b', 'Store B'))
|
|
48
|
+
|
|
49
|
+
storeA.close()
|
|
50
|
+
|
|
51
|
+
expect(storeA.state()).toBeNull()
|
|
52
|
+
expect(storeB.state()?.id).toBe('b')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('pinned flag is per-store', () => {
|
|
56
|
+
const storeA = createScratchpadStore()
|
|
57
|
+
const storeB = createScratchpadStore()
|
|
58
|
+
|
|
59
|
+
storeA.dispatch({ ...createEvent('a', 'A'), pinned: true })
|
|
60
|
+
storeB.dispatch({ ...createEvent('b', 'B'), pinned: false })
|
|
61
|
+
|
|
62
|
+
expect(storeA.pinned()).toBe(true)
|
|
63
|
+
expect(storeB.pinned()).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('ScratchpadStoreProvider without store prop creates a fresh store', () => {
|
|
67
|
+
let capturedState: ReturnType<typeof useScratchpadState> | null = null
|
|
68
|
+
|
|
69
|
+
const Child = () => {
|
|
70
|
+
capturedState = useScratchpadState()
|
|
71
|
+
return <div>child</div>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render(() => (
|
|
75
|
+
<ScratchpadStoreProvider>
|
|
76
|
+
<Child />
|
|
77
|
+
</ScratchpadStoreProvider>
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
expect(capturedState).not.toBeNull()
|
|
81
|
+
expect(capturedState!.state()).toBeNull()
|
|
82
|
+
expect(capturedState!.pinned()).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('ScratchpadStoreProvider with explicit store prop binds children to it', () => {
|
|
86
|
+
const scoped = createScratchpadStore()
|
|
87
|
+
scoped.dispatch(createEvent('scoped', 'Scoped title'))
|
|
88
|
+
|
|
89
|
+
let capturedState: ReturnType<typeof useScratchpadState> | null = null
|
|
90
|
+
const Child = () => {
|
|
91
|
+
capturedState = useScratchpadState()
|
|
92
|
+
return <div>child</div>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(() => (
|
|
96
|
+
<ScratchpadStoreProvider store={scoped}>
|
|
97
|
+
<Child />
|
|
98
|
+
</ScratchpadStoreProvider>
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
expect(capturedState!.state()?.id).toBe('scoped')
|
|
102
|
+
expect(capturedState!.state()?.title).toBe('Scoped title')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('useScratchpadState outside provider falls back to module singleton', () => {
|
|
106
|
+
// Reset singleton first by writing a unique id, then verifying dispatch lands on it
|
|
107
|
+
dispatchScratchpad(createEvent('singleton-test', 'Singleton'))
|
|
108
|
+
|
|
109
|
+
let capturedState: ReturnType<typeof useScratchpadState> | null = null
|
|
110
|
+
const Child = () => {
|
|
111
|
+
capturedState = useScratchpadState()
|
|
112
|
+
return <div>child</div>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render(() => <Child />)
|
|
116
|
+
|
|
117
|
+
expect(capturedState!.state()?.id).toBe('singleton-test')
|
|
118
|
+
// Cleanup: close singleton for test isolation
|
|
119
|
+
capturedState!.close()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('dispatchScratchpad (module fn) only writes to singleton, not scoped stores', () => {
|
|
123
|
+
const scoped = createScratchpadStore()
|
|
124
|
+
dispatchScratchpad(createEvent('global', 'Global'))
|
|
125
|
+
|
|
126
|
+
// scoped store is still empty
|
|
127
|
+
expect(scoped.state()).toBeNull()
|
|
128
|
+
|
|
129
|
+
// and the singleton carries the dispatched event
|
|
130
|
+
let singletonSnap: ReturnType<typeof useScratchpadState> | null = null
|
|
131
|
+
const Child = () => {
|
|
132
|
+
singletonSnap = useScratchpadState()
|
|
133
|
+
return <div>child</div>
|
|
134
|
+
}
|
|
135
|
+
render(() => <Child />)
|
|
136
|
+
expect(singletonSnap!.state()?.id).toBe('global')
|
|
137
|
+
// Cleanup
|
|
138
|
+
singletonSnap!.close()
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -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
|
+
}
|
package/src/types/chat-bus.ts
CHANGED
|
@@ -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 ──────────────────────────────────────────────
|
|
@@ -52,6 +53,7 @@ export interface ChatEvents {
|
|
|
52
53
|
// --- Interactions ---
|
|
53
54
|
onChatPromptResponse: (event: ChatEventBase & { response: ChatPromptResponse }) => void
|
|
54
55
|
onClarificationNeeded: (event: ChatEventBase & { clarification: ClarificationEvent }) => void
|
|
56
|
+
onElicitation: (event: ChatEventBase & { elicitation: ElicitationEvent }) => void
|
|
55
57
|
|
|
56
58
|
// --- Agentic (handled by app, not MCP-UI) ---
|
|
57
59
|
onAgentSwitch: (event: ChatEventBase & { agent: AgentContext }) => void
|
|
@@ -97,15 +99,44 @@ export interface ChatCommands {
|
|
|
97
99
|
/**
|
|
98
100
|
* Show a ChatPrompt (choice, confirm, form) above the input (C4).
|
|
99
101
|
*
|
|
100
|
-
* **
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* **No default handler in v5.0.0 / v5.1.0.** `showChatPrompt` is a command
|
|
103
|
+
* *name*, not a default implementation — mcp-ui ships `ChatPrompt` (the
|
|
104
|
+
* presentation component) and the bus (event/command plumbing), but the
|
|
105
|
+
* handler that threads a Promise resolver through the SolidJS lifecycle is
|
|
106
|
+
* the consumer's responsibility. Every host app calls
|
|
107
|
+
* `bus.commands.handle('showChatPrompt', (config, signal?) => { ... })`.
|
|
105
108
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
+
* ### Implementer contract
|
|
110
|
+
*
|
|
111
|
+
* A conforming handler MUST:
|
|
112
|
+
*
|
|
113
|
+
* 1. Return a `Promise<ChatPromptResponse>`.
|
|
114
|
+
* 2. Resolve the Promise from the `ChatPrompt` component's `onSubmit`
|
|
115
|
+
* (explicit answer) or from `onDismiss` (dismissed flag true).
|
|
116
|
+
* 3. If a `signal` is provided:
|
|
117
|
+
* - If `signal.aborted` is already `true`, reject with
|
|
118
|
+
* `new DOMException('Prompt aborted', 'AbortError')` synchronously
|
|
119
|
+
* (or via `Promise.reject`) and do NOT show the UI.
|
|
120
|
+
* - Otherwise, register `signal.addEventListener('abort', () =>
|
|
121
|
+
* reject(new DOMException('Prompt aborted', 'AbortError')))` and
|
|
122
|
+
* clean up the listener on resolve/dismiss.
|
|
123
|
+
* 4. Enforce re-entrance policy — if a previous prompt is still active
|
|
124
|
+
* when a new one arrives, the recommended behavior is auto-reject the
|
|
125
|
+
* previous Promise with a custom error (e.g. `PromptReplacedError`).
|
|
126
|
+
* Alternatives: FIFO queue, or throw synchronously.
|
|
127
|
+
*
|
|
128
|
+
* The `DOMException('AbortError')` shape is the Web Platform convention
|
|
129
|
+
* (matches `fetch()`, `Response.body.cancel()`, `WritableStream.abort()`).
|
|
130
|
+
* Consumers branching on the error can do
|
|
131
|
+
* `catch (err) { if (err.name === 'AbortError') return; throw err }`.
|
|
132
|
+
*
|
|
133
|
+
* ### Planned primitive (v5.2.0)
|
|
134
|
+
*
|
|
135
|
+
* A `createChatPromptController(setActivePrompt)` helper will centralise
|
|
136
|
+
* the resolver lifecycle + abort + re-entrance logic once, so consumers
|
|
137
|
+
* can write `bus.commands.handle('showChatPrompt', ctrl.handle)` instead
|
|
138
|
+
* of threading a `let chatPromptResolver` closure by hand. Design doc:
|
|
139
|
+
* `docs/2026/r&d/mcpui-v5.1.0-consensus.md`.
|
|
109
140
|
*/
|
|
110
141
|
showChatPrompt: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>
|
|
111
142
|
/** Dismiss the active ChatPrompt */
|
|
@@ -208,21 +239,95 @@ export interface ChatPromptConfig {
|
|
|
208
239
|
config: ChoicePromptConfig | ConfirmPromptConfig | FormPromptConfig
|
|
209
240
|
}
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
/**
|
|
243
|
+
* A single choice option. The generic `TMeta` parameter flows through the
|
|
244
|
+
* whole `ChoicePromptConfig<TMeta>` shape so consumers can strongly-type
|
|
245
|
+
* their metadata in `optionRenderer` without casting.
|
|
246
|
+
*
|
|
247
|
+
* @since v4.3.9 (metadata), v5.1.0 (generic TMeta + optionRenderer typing)
|
|
248
|
+
*/
|
|
249
|
+
export interface ChoiceOption<TMeta = Record<string, unknown>> {
|
|
250
|
+
value: string
|
|
251
|
+
label: string
|
|
252
|
+
icon?: string
|
|
253
|
+
description?: string
|
|
254
|
+
/**
|
|
255
|
+
* Free-form metadata (confidence, source, tags, ...).
|
|
256
|
+
* Opaque to the default renderer — use `optionRenderer` to display it.
|
|
257
|
+
* Preserved through `showChatPrompt → ChatPromptResponse` roundtrip.
|
|
258
|
+
* @since v4.3.9
|
|
259
|
+
*/
|
|
260
|
+
metadata?: TMeta
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface ChoicePromptConfig<TMeta = Record<string, unknown>> {
|
|
264
|
+
options: Array<ChoiceOption<TMeta>>
|
|
225
265
|
layout?: 'horizontal' | 'vertical' | 'grid'
|
|
266
|
+
/**
|
|
267
|
+
* Optional render prop for custom option bodies (badges, confidence
|
|
268
|
+
* indicators, rich layouts). Replaces the default `label + icon +
|
|
269
|
+
* description` body. mcp-ui still wraps the returned JSX in a `<button>`
|
|
270
|
+
* with the `onClick` handler, keyboard support, and focus styles — only
|
|
271
|
+
* the *content* of the button is yours.
|
|
272
|
+
*
|
|
273
|
+
* @param option The full `ChoiceOption` including strongly-typed `metadata`.
|
|
274
|
+
* @param index Zero-based position in the `options` array.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```tsx
|
|
278
|
+
* interface ConfBadgeMeta { confidence: number; source: string }
|
|
279
|
+
*
|
|
280
|
+
* bus.commands.exec('showChatPrompt', {
|
|
281
|
+
* type: 'choice',
|
|
282
|
+
* title: 'Pick an intent',
|
|
283
|
+
* config: {
|
|
284
|
+
* layout: 'vertical',
|
|
285
|
+
* options: [
|
|
286
|
+
* { value: 'a', label: 'Immobilier', metadata: { confidence: 0.9, source: 'llm' } },
|
|
287
|
+
* { value: 'b', label: 'Santé', metadata: { confidence: 0.4, source: 'llm' } },
|
|
288
|
+
* ],
|
|
289
|
+
* optionRenderer: (opt: ChoiceOption<ConfBadgeMeta>) => (
|
|
290
|
+
* <div>
|
|
291
|
+
* {opt.label}
|
|
292
|
+
* <span class="ml-2 text-xs">
|
|
293
|
+
* ({Math.round((opt.metadata?.confidence ?? 0) * 100)}%)
|
|
294
|
+
* </span>
|
|
295
|
+
* </div>
|
|
296
|
+
* ),
|
|
297
|
+
* },
|
|
298
|
+
* } as ChatPromptConfig)
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* ### ⚠️ Accessibility
|
|
302
|
+
* Do NOT return `<button>`, `<a href>`, or other interactive elements from
|
|
303
|
+
* `optionRenderer`. mcp-ui already wraps the content in a `<button>`, and
|
|
304
|
+
* nested interactive elements break screen-reader semantics, keyboard
|
|
305
|
+
* focus order, and click-through behaviour.
|
|
306
|
+
*
|
|
307
|
+
* ### ⚠️ Stale closures
|
|
308
|
+
* `optionRenderer` is called once per option per render. If you capture
|
|
309
|
+
* SolidJS signals inside the closure, wrap the access in a thunk so the
|
|
310
|
+
* framework tracks the dependency correctly. Don't destructure signal
|
|
311
|
+
* values into locals outside reactive scopes.
|
|
312
|
+
*
|
|
313
|
+
* @since v5.1.0
|
|
314
|
+
*/
|
|
315
|
+
optionRenderer?: (option: ChoiceOption<TMeta>, index: number) => JSX.Element
|
|
316
|
+
/**
|
|
317
|
+
* Custom Tailwind classes appended to each option button (after mcp-ui's
|
|
318
|
+
* defaults). Escape hatch for colour/border/radius tweaks that don't
|
|
319
|
+
* warrant a full `optionRenderer`.
|
|
320
|
+
*
|
|
321
|
+
* @since v5.1.0
|
|
322
|
+
*/
|
|
323
|
+
buttonClass?: string
|
|
324
|
+
/**
|
|
325
|
+
* Custom Tailwind classes appended to the options container (the
|
|
326
|
+
* flex/grid wrapper that lays out the buttons).
|
|
327
|
+
*
|
|
328
|
+
* @since v5.1.0
|
|
329
|
+
*/
|
|
330
|
+
containerClass?: string
|
|
226
331
|
}
|
|
227
332
|
|
|
228
333
|
export interface ConfirmPromptConfig {
|
|
@@ -464,6 +569,45 @@ export interface ToolCallEvent {
|
|
|
464
569
|
duration_ms?: number
|
|
465
570
|
}
|
|
466
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
|
+
|
|
467
611
|
export interface ClarificationEvent {
|
|
468
612
|
/** The question to ask the user */
|
|
469
613
|
question: string
|