@seed-ship/mcp-ui-solid 6.5.0 → 6.6.1
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 +161 -0
- package/README.md +37 -0
- package/dist/adapters/connector.cjs +112 -0
- package/dist/adapters/connector.cjs.map +1 -0
- package/dist/adapters/connector.d.ts +71 -0
- package/dist/adapters/connector.d.ts.map +1 -0
- package/dist/adapters/connector.js +112 -0
- package/dist/adapters/connector.js.map +1 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters.cjs +6 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +18 -0
- package/dist/adapters.d.ts +18 -0
- package/dist/adapters.js +6 -0
- package/dist/adapters.js.map +1 -0
- package/dist/components/ActionGroupRenderer.cjs +12 -3
- package/dist/components/ActionGroupRenderer.cjs.map +1 -1
- package/dist/components/ActionGroupRenderer.d.ts.map +1 -1
- package/dist/components/ActionGroupRenderer.js +12 -3
- package/dist/components/ActionGroupRenderer.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +24 -6
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +24 -6
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/FeedbackInline.cjs +6 -2
- package/dist/components/FeedbackInline.cjs.map +1 -1
- package/dist/components/FeedbackInline.d.ts +2 -2
- package/dist/components/FeedbackInline.d.ts.map +1 -1
- package/dist/components/FeedbackInline.js +7 -3
- package/dist/components/FeedbackInline.js.map +1 -1
- package/dist/components/PresentationFeedback.cjs +207 -0
- package/dist/components/PresentationFeedback.cjs.map +1 -0
- package/dist/components/PresentationFeedback.d.ts +113 -0
- package/dist/components/PresentationFeedback.d.ts.map +1 -0
- package/dist/components/PresentationFeedback.js +207 -0
- package/dist/components/PresentationFeedback.js.map +1 -0
- package/dist/components/StreamingUIRenderer.cjs +82 -195
- package/dist/components/StreamingUIRenderer.cjs.map +1 -1
- package/dist/components/StreamingUIRenderer.d.ts +25 -5
- package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
- package/dist/components/StreamingUIRenderer.js +84 -197
- package/dist/components/StreamingUIRenderer.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +22 -15
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +22 -15
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +3 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +2 -0
- package/dist/components.d.ts +2 -0
- package/dist/components.js +3 -0
- package/dist/components.js.map +1 -1
- package/dist/context/MCPActionContext.cjs +4 -1
- package/dist/context/MCPActionContext.cjs.map +1 -1
- package/dist/context/MCPActionContext.d.ts +13 -1
- package/dist/context/MCPActionContext.d.ts.map +1 -1
- package/dist/context/MCPActionContext.js +4 -1
- package/dist/context/MCPActionContext.js.map +1 -1
- package/dist/context/MCPUIStringsContext.cjs +38 -0
- package/dist/context/MCPUIStringsContext.cjs.map +1 -0
- package/dist/context/MCPUIStringsContext.d.ts +95 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -0
- package/dist/context/MCPUIStringsContext.js +38 -0
- package/dist/context/MCPUIStringsContext.js.map +1 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +103 -0
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +103 -0
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/docs/briefs/ROADMAP-opendata-macro-mcpui.md +912 -0
- package/package.json +17 -5
- package/src/adapters/connector.test.ts +165 -0
- package/src/adapters/connector.ts +234 -0
- package/src/adapters/index.ts +24 -0
- package/src/components/ActionGroupRenderer.test.tsx +1 -0
- package/src/components/ActionGroupRenderer.tsx +19 -4
- package/src/components/ActionSubmit.test.tsx +188 -0
- package/src/components/ExpandableWrapper.test.tsx +5 -2
- package/src/components/ExpandableWrapper.tsx +8 -6
- package/src/components/FeedbackInline.test.tsx +6 -3
- package/src/components/FeedbackInline.tsx +8 -6
- package/src/components/PresentationFeedback.test.tsx +163 -0
- package/src/components/PresentationFeedback.tsx +326 -0
- package/src/components/StreamingUIRenderer.parity.test.tsx +158 -0
- package/src/components/StreamingUIRenderer.tsx +42 -166
- package/src/components/UIResourceRenderer.tsx +19 -6
- package/src/components/index.ts +10 -0
- package/src/context/MCPActionContext.tsx +17 -1
- package/src/context/MCPUIStringsContext.test.tsx +116 -0
- package/src/context/MCPUIStringsContext.tsx +128 -0
- package/src/index.ts +27 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +1 -0
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StreamingUIRenderer Component
|
|
2
|
+
* StreamingUIRenderer Component
|
|
3
3
|
*
|
|
4
|
-
* Renders streaming dashboard components with skeleton states and progress
|
|
5
|
-
* Uses the useStreamingUI hook for SSE connection and state
|
|
4
|
+
* Renders streaming dashboard components with skeleton states and progress
|
|
5
|
+
* indicators. Uses the `useStreamingUI` hook for SSE connection and state.
|
|
6
|
+
*
|
|
7
|
+
* ## Rendering parity (v6.6.0 — closes Gap 1 of ROADMAP-opendata-macro-mcpui)
|
|
8
|
+
*
|
|
9
|
+
* Each component received over SSE is delegated to the real
|
|
10
|
+
* `<UIResourceRenderer>`. Streamed `table` / `chart` / `map` / `action-group`
|
|
11
|
+
* therefore render with the SAME fidelity as a static layout — no more
|
|
12
|
+
* simplified "type + title" placeholder. Validation, telemetry, the error
|
|
13
|
+
* boundary and `errorMode` all come from `<UIResourceRenderer>`, so the two
|
|
14
|
+
* paths cannot drift.
|
|
15
|
+
*
|
|
16
|
+
* Delegation is a one-way value import (`UIResourceRenderer` never imports
|
|
17
|
+
* this file — no cycle). The streamed component's `position` is normalized
|
|
18
|
+
* to full-width before delegation : this component owns the 12-column grid,
|
|
19
|
+
* `<UIResourceRenderer>` only owns the component's own rendering.
|
|
6
20
|
*
|
|
7
21
|
* Features:
|
|
8
22
|
* - Skeleton loading states while components stream
|
|
@@ -21,14 +35,11 @@
|
|
|
21
35
|
* ```
|
|
22
36
|
*/
|
|
23
37
|
|
|
24
|
-
import { Show, For, createSignal, onMount
|
|
38
|
+
import { Show, For, createSignal, onMount } from 'solid-js'
|
|
25
39
|
import { useStreamingUI, type UseStreamingUIOptions } from '../hooks/useStreamingUI'
|
|
26
40
|
import type { UIComponent, RendererError } from '../types'
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { markRenderStart, markRenderEnd, PERF_PREFIX } from '../utils/perf'
|
|
30
|
-
import { useTelemetry } from '../context/MCPUITelemetryContext'
|
|
31
|
-
import type { ValidationErrorMode } from './UIResourceRenderer'
|
|
41
|
+
import { UIResourceRenderer, type ValidationErrorMode } from './UIResourceRenderer'
|
|
42
|
+
import { useMCPUIStrings } from '../context/MCPUIStringsContext'
|
|
32
43
|
|
|
33
44
|
export interface StreamingUIRendererProps extends UseStreamingUIOptions {
|
|
34
45
|
class?: string
|
|
@@ -38,164 +49,27 @@ export interface StreamingUIRendererProps extends UseStreamingUIOptions {
|
|
|
38
49
|
/**
|
|
39
50
|
* How to react when a streamed component fails `validateComponent()`
|
|
40
51
|
* (v5.4.0). Defaults to `'block'` (full red error card — pre-v5.4.0
|
|
41
|
-
* behavior).
|
|
52
|
+
* behavior). Forwarded to the delegated `<UIResourceRenderer>`.
|
|
42
53
|
*/
|
|
43
54
|
errorMode?: ValidationErrorMode
|
|
55
|
+
/**
|
|
56
|
+
* Visibility behavior of the inline expand button on streamed components
|
|
57
|
+
* wrapped in `<ExpandableWrapper>` (v6.6.0 — parity with the static
|
|
58
|
+
* `<UIResourceRenderer toolbarVariant>` prop). Forwarded as-is.
|
|
59
|
+
*/
|
|
60
|
+
toolbarVariant?: 'hover' | 'always-visible'
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
/**
|
|
47
|
-
*
|
|
48
|
-
* (
|
|
64
|
+
* The 12-column placement of a streamed component is owned by this
|
|
65
|
+
* component's outer grid (the cell `<div>` below). Delegating the component
|
|
66
|
+
* verbatim to `<UIResourceRenderer>` would re-apply that placement inside a
|
|
67
|
+
* fresh nested 12-column grid and visually misplace it. We hand
|
|
68
|
+
* `<UIResourceRenderer>` a full-width copy so it only renders the component,
|
|
69
|
+
* not a competing layout.
|
|
49
70
|
*/
|
|
50
|
-
function
|
|
51
|
-
component:
|
|
52
|
-
onError?: (error: RendererError) => void
|
|
53
|
-
errorMode?: ValidationErrorMode
|
|
54
|
-
}) {
|
|
55
|
-
// Performance marks (v5.4.0) — see utils/perf.ts
|
|
56
|
-
markRenderStart(props.component.id)
|
|
57
|
-
|
|
58
|
-
// Telemetry sink (B.5 — v5.6.0). Same wiring as ComponentRenderer in
|
|
59
|
-
// UIResourceRenderer.tsx — null when no Provider, no-op everywhere then.
|
|
60
|
-
const telemetry = useTelemetry()
|
|
61
|
-
|
|
62
|
-
onMount(() => {
|
|
63
|
-
markRenderEnd(props.component.id)
|
|
64
|
-
if (telemetry) {
|
|
65
|
-
const ts = Date.now()
|
|
66
|
-
telemetry.dispatch({
|
|
67
|
-
type: 'component:mounted',
|
|
68
|
-
id: props.component.id,
|
|
69
|
-
componentType: props.component.type,
|
|
70
|
-
ts,
|
|
71
|
-
})
|
|
72
|
-
if (typeof performance !== 'undefined' && typeof performance.getEntriesByName === 'function') {
|
|
73
|
-
const entries = performance.getEntriesByName(`${PERF_PREFIX}${props.component.id}:render`, 'measure')
|
|
74
|
-
const last = entries[entries.length - 1]
|
|
75
|
-
if (last) {
|
|
76
|
-
telemetry.dispatch({
|
|
77
|
-
type: 'component:rendered',
|
|
78
|
-
id: props.component.id,
|
|
79
|
-
componentType: props.component.type,
|
|
80
|
-
durationMs: last.duration,
|
|
81
|
-
ts,
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
onCleanup(() => {
|
|
89
|
-
if (telemetry) {
|
|
90
|
-
telemetry.dispatch({
|
|
91
|
-
type: 'component:unmounted',
|
|
92
|
-
id: props.component.id,
|
|
93
|
-
componentType: props.component.type,
|
|
94
|
-
ts: Date.now(),
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Validate component before rendering
|
|
100
|
-
const validation = validateComponent(props.component)
|
|
101
|
-
if (!validation.valid) {
|
|
102
|
-
props.onError?.({
|
|
103
|
-
type: 'validation',
|
|
104
|
-
message: 'Component validation failed',
|
|
105
|
-
componentId: props.component.id,
|
|
106
|
-
details: validation.errors,
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
if (telemetry) {
|
|
110
|
-
telemetry.dispatch({
|
|
111
|
-
type: 'validation:failed',
|
|
112
|
-
id: props.component.id,
|
|
113
|
-
componentType: props.component.type,
|
|
114
|
-
errorCount: validation.errors?.length ?? 0,
|
|
115
|
-
firstErrorCode: validation.errors?.[0]?.code ?? null,
|
|
116
|
-
ts: Date.now(),
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const mode: ValidationErrorMode = props.errorMode ?? 'block'
|
|
121
|
-
const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
|
|
122
|
-
|
|
123
|
-
if (mode === 'silent') {
|
|
124
|
-
return null
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (mode === 'inline-warn') {
|
|
128
|
-
return (
|
|
129
|
-
<div
|
|
130
|
-
class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200"
|
|
131
|
-
role="alert"
|
|
132
|
-
aria-label="Component validation warning"
|
|
133
|
-
title={firstError}
|
|
134
|
-
>
|
|
135
|
-
<svg
|
|
136
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
137
|
-
class="w-3.5 h-3.5"
|
|
138
|
-
viewBox="0 0 24 24"
|
|
139
|
-
fill="none"
|
|
140
|
-
stroke="currentColor"
|
|
141
|
-
stroke-width="2"
|
|
142
|
-
stroke-linecap="round"
|
|
143
|
-
stroke-linejoin="round"
|
|
144
|
-
aria-hidden="true"
|
|
145
|
-
>
|
|
146
|
-
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
147
|
-
<line x1="12" y1="9" x2="12" y2="13" />
|
|
148
|
-
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
149
|
-
</svg>
|
|
150
|
-
<span>Invalid {props.component.type}</span>
|
|
151
|
-
</div>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return (
|
|
156
|
-
<div class="w-full bg-error-subtle border border-border-error rounded-lg p-4">
|
|
157
|
-
<p class="text-sm font-medium text-error-primary">Validation Error</p>
|
|
158
|
-
<p class="text-xs text-text-secondary mt-1">
|
|
159
|
-
{firstError}
|
|
160
|
-
</p>
|
|
161
|
-
</div>
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Simplified renderer - just show component type and title
|
|
166
|
-
// Full rendering logic in UIResourceRenderer
|
|
167
|
-
const params = props.component.params as any
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<GenerativeUIErrorBoundary
|
|
171
|
-
componentId={props.component.id}
|
|
172
|
-
componentType={props.component.type}
|
|
173
|
-
onError={props.onError}
|
|
174
|
-
allowRetry={false}
|
|
175
|
-
>
|
|
176
|
-
<div class="w-full bg-surface-secondary border border-border-subtle rounded-lg p-4">
|
|
177
|
-
<div class="flex items-center gap-2 mb-2">
|
|
178
|
-
<span class="text-xs font-medium text-text-tertiary uppercase">
|
|
179
|
-
{props.component.type}
|
|
180
|
-
</span>
|
|
181
|
-
</div>
|
|
182
|
-
<Show when={params?.title}>
|
|
183
|
-
<h3 class="text-sm font-semibold text-text-primary">{params.title}</h3>
|
|
184
|
-
</Show>
|
|
185
|
-
<Show when={props.component.type === 'metric' && params?.value}>
|
|
186
|
-
<div class="mt-2">
|
|
187
|
-
<p class="text-2xl font-semibold text-text-primary">{params.value}</p>
|
|
188
|
-
<Show when={params.unit}>
|
|
189
|
-
<span class="text-sm text-text-secondary">{params.unit}</span>
|
|
190
|
-
</Show>
|
|
191
|
-
</div>
|
|
192
|
-
</Show>
|
|
193
|
-
<div class="mt-3 text-xs text-text-tertiary">
|
|
194
|
-
Component ID: {props.component.id.slice(0, 8)}...
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
</GenerativeUIErrorBoundary>
|
|
198
|
-
)
|
|
71
|
+
function asFullWidth(component: UIComponent): UIComponent {
|
|
72
|
+
return { ...component, position: { colStart: 1, colSpan: 12 } }
|
|
199
73
|
}
|
|
200
74
|
|
|
201
75
|
export function StreamingUIRenderer(props: StreamingUIRendererProps) {
|
|
@@ -210,6 +84,7 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
|
|
|
210
84
|
onComponentReceived: props.onComponentReceived,
|
|
211
85
|
})
|
|
212
86
|
|
|
87
|
+
const strings = useMCPUIStrings()
|
|
213
88
|
const [animatingComponents, setAnimatingComponents] = createSignal<Set<string>>(new Set())
|
|
214
89
|
|
|
215
90
|
// Track new components for animation
|
|
@@ -292,7 +167,7 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
|
|
|
292
167
|
class="mt-3 rounded-md bg-error-primary px-3 py-1.5 text-sm font-medium text-white hover:bg-error-hover"
|
|
293
168
|
onClick={() => startStreaming()}
|
|
294
169
|
>
|
|
295
|
-
|
|
170
|
+
{strings.retry}
|
|
296
171
|
</button>
|
|
297
172
|
</Show>
|
|
298
173
|
</div>
|
|
@@ -300,7 +175,7 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
|
|
|
300
175
|
|
|
301
176
|
{/* Components Grid */}
|
|
302
177
|
<div class="grid grid-cols-12 gap-4">
|
|
303
|
-
{/* Render received components */}
|
|
178
|
+
{/* Render received components — delegated to the real UIResourceRenderer */}
|
|
304
179
|
<For each={components()}>
|
|
305
180
|
{(component) => {
|
|
306
181
|
// Trigger animation on mount (SSR-safe, no 'use' directive needed)
|
|
@@ -314,10 +189,11 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
|
|
|
314
189
|
`}
|
|
315
190
|
style={`grid-column-start: ${component.position.colStart}; grid-column-end: ${component.position.colStart + component.position.colSpan}`}
|
|
316
191
|
>
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
onError={props.onRenderError}
|
|
192
|
+
<UIResourceRenderer
|
|
193
|
+
content={asFullWidth(component)}
|
|
320
194
|
errorMode={props.errorMode}
|
|
195
|
+
onError={props.onRenderError}
|
|
196
|
+
toolbarVariant={props.toolbarVariant}
|
|
321
197
|
/>
|
|
322
198
|
</div>
|
|
323
199
|
)
|
|
@@ -1524,9 +1524,13 @@ function ComponentRenderer(props: {
|
|
|
1524
1524
|
*/
|
|
1525
1525
|
function ActionRenderer(props: { component: UIComponent }) {
|
|
1526
1526
|
const params = props.component.params as any
|
|
1527
|
-
const { execute, isExecuting } = useAction()
|
|
1527
|
+
const { execute, executeAction, isExecuting } = useAction()
|
|
1528
1528
|
const telemetry = useTelemetry()
|
|
1529
1529
|
|
|
1530
|
+
// tool-call and submit both run through the host executor — loading +
|
|
1531
|
+
// disabled state apply to both. link does neither.
|
|
1532
|
+
const isExecutable = () => params.action === 'tool-call' || params.action === 'submit'
|
|
1533
|
+
|
|
1530
1534
|
// Telemetry: action:dispatched on click (B.5 — v5.6.0). Fires for every
|
|
1531
1535
|
// click attempt (tool-call or link), regardless of execute success.
|
|
1532
1536
|
// Privacy: actionName is `toolName` (tool-call) or the action kind
|
|
@@ -1549,11 +1553,20 @@ function ActionRenderer(props: { component: UIComponent }) {
|
|
|
1549
1553
|
if (params.action === 'tool-call' && params.toolName) {
|
|
1550
1554
|
e.preventDefault()
|
|
1551
1555
|
await execute(params.toolName, params.params || {})
|
|
1556
|
+
} else if (params.action === 'submit') {
|
|
1557
|
+
// submit is NOT a tool call — route through the executor with the
|
|
1558
|
+
// `action: 'submit'` kind preserved. Works outside any <form>.
|
|
1559
|
+
e.preventDefault()
|
|
1560
|
+
await executeAction({
|
|
1561
|
+
action: 'submit',
|
|
1562
|
+
toolName: params.toolName || 'submit',
|
|
1563
|
+
params: params.params || {},
|
|
1564
|
+
})
|
|
1552
1565
|
}
|
|
1553
1566
|
}
|
|
1554
1567
|
|
|
1555
1568
|
// Determine if button should be disabled (explicit disable or currently executing)
|
|
1556
|
-
const isDisabled = () => params.disabled || (
|
|
1569
|
+
const isDisabled = () => params.disabled || (isExecutable() && isExecuting())
|
|
1557
1570
|
|
|
1558
1571
|
if (params.type === 'link' || params.action === 'link') {
|
|
1559
1572
|
return (
|
|
@@ -1579,9 +1592,9 @@ function ActionRenderer(props: { component: UIComponent }) {
|
|
|
1579
1592
|
|
|
1580
1593
|
return (
|
|
1581
1594
|
<button
|
|
1582
|
-
type=
|
|
1595
|
+
type="button"
|
|
1583
1596
|
disabled={isDisabled()}
|
|
1584
|
-
aria-busy={isExecuting() &&
|
|
1597
|
+
aria-busy={isExecuting() && isExecutable()}
|
|
1585
1598
|
aria-label={params.ariaLabel || params.label}
|
|
1586
1599
|
class={`inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
|
|
1587
1600
|
${params.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm' :
|
|
@@ -1594,10 +1607,10 @@ function ActionRenderer(props: { component: UIComponent }) {
|
|
|
1594
1607
|
${params.className || ''}`}
|
|
1595
1608
|
onClick={handleClick}
|
|
1596
1609
|
>
|
|
1597
|
-
<Show when={isExecuting() &&
|
|
1610
|
+
<Show when={isExecuting() && isExecutable()}>
|
|
1598
1611
|
<span class="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" aria-hidden="true" />
|
|
1599
1612
|
</Show>
|
|
1600
|
-
<Show when={params.icon && !(isExecuting() &&
|
|
1613
|
+
<Show when={params.icon && !(isExecuting() && isExecutable())}>
|
|
1601
1614
|
<span aria-hidden="true">{params.icon}</span>
|
|
1602
1615
|
</Show>
|
|
1603
1616
|
{params.label}
|
package/src/components/index.ts
CHANGED
|
@@ -10,6 +10,16 @@ export type { UIResourceRendererProps } from './UIResourceRenderer'
|
|
|
10
10
|
export { StreamingUIRenderer } from './StreamingUIRenderer'
|
|
11
11
|
export type { StreamingUIRendererProps } from './StreamingUIRenderer'
|
|
12
12
|
|
|
13
|
+
// Presentation feedback (v6.6.0 — R3, distinct from FeedbackInline)
|
|
14
|
+
export {
|
|
15
|
+
PresentationFeedback,
|
|
16
|
+
DEFAULT_PRESENTATION_FEEDBACK_LABELS,
|
|
17
|
+
} from './PresentationFeedback'
|
|
18
|
+
export type {
|
|
19
|
+
PresentationFeedbackProps,
|
|
20
|
+
PresentationFeedbackLabels,
|
|
21
|
+
} from './PresentationFeedback'
|
|
22
|
+
|
|
13
23
|
export { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
|
|
14
24
|
export type { GenerativeUIErrorBoundaryProps } from './GenerativeUIErrorBoundary'
|
|
15
25
|
|
|
@@ -10,7 +10,9 @@ import { createContext, createSignal, useContext, ParentComponent, Accessor } fr
|
|
|
10
10
|
*/
|
|
11
11
|
export interface ActionRequest {
|
|
12
12
|
/**
|
|
13
|
-
* MCP tool name to execute
|
|
13
|
+
* MCP tool name to execute. For a `submit` action with no associated
|
|
14
|
+
* tool, renderers pass the sentinel `'submit'` — branch on `action`,
|
|
15
|
+
* not on `toolName`, to tell a submit apart from a tool call.
|
|
14
16
|
*/
|
|
15
17
|
toolName: string
|
|
16
18
|
|
|
@@ -28,6 +30,17 @@ export interface ActionRequest {
|
|
|
28
30
|
* Optional macro ID for template execution
|
|
29
31
|
*/
|
|
30
32
|
macroId?: string
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Action kind (v6.6.1). Lets a host `executor` tell a tool call apart
|
|
36
|
+
* from a form-style `submit`. Absent ⇒ treat as `'tool-call'` (backward
|
|
37
|
+
* compatible — every pre-v6.6.1 request omits it).
|
|
38
|
+
*
|
|
39
|
+
* A `submit` action carries its payload in `params` (e.g. `submit_url`,
|
|
40
|
+
* `connector_id`, `feedback_value`) and **must NOT** be executed as a
|
|
41
|
+
* tool call — the host routes it (e.g. POST to `params.submit_url`).
|
|
42
|
+
*/
|
|
43
|
+
action?: 'tool-call' | 'submit' | 'link'
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -149,6 +162,9 @@ const defaultExecutor = async (request: ActionRequest): Promise<ActionResult> =>
|
|
|
149
162
|
params: request.params || {},
|
|
150
163
|
spaceIds: request.spaceIds,
|
|
151
164
|
macroId: request.macroId,
|
|
165
|
+
// v6.6.1 — action kind so a window-level listener can route a
|
|
166
|
+
// `submit` (POST to params.submit_url) vs a tool call.
|
|
167
|
+
action: request.action ?? 'tool-call',
|
|
152
168
|
},
|
|
153
169
|
bubbles: true,
|
|
154
170
|
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.6.0 — MCPUIStringsProvider (D2 / R4 of ROADMAP-opendata-macro-mcpui).
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. Defaults are English, available with no provider mounted
|
|
6
|
+
* 2. Provider does a partial merge over the EN defaults
|
|
7
|
+
* 3. FeedbackInline reads chrome strings from the provider
|
|
8
|
+
* 4. FeedbackInline `positiveAck` / `negativeAck` props still win over the provider
|
|
9
|
+
* 5. ExpandableWrapper reads the expand-button tooltip from the provider
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
13
|
+
import { render, cleanup, fireEvent } from '@solidjs/testing-library'
|
|
14
|
+
import {
|
|
15
|
+
MCPUIStringsProvider,
|
|
16
|
+
useMCPUIStrings,
|
|
17
|
+
DEFAULT_MCPUI_STRINGS,
|
|
18
|
+
} from './MCPUIStringsContext'
|
|
19
|
+
import { FeedbackInline } from '../components/FeedbackInline'
|
|
20
|
+
import { ExpandableWrapper } from '../components/ExpandableWrapper'
|
|
21
|
+
|
|
22
|
+
describe('MCPUIStringsContext (v6.6.0)', () => {
|
|
23
|
+
beforeEach(() => cleanup())
|
|
24
|
+
|
|
25
|
+
it('defaults are English', () => {
|
|
26
|
+
expect(DEFAULT_MCPUI_STRINGS.expand).toBe('Expand')
|
|
27
|
+
expect(DEFAULT_MCPUI_STRINGS.feedbackUseful).toBe('Useful')
|
|
28
|
+
expect(DEFAULT_MCPUI_STRINGS.feedbackPositiveAck).toBe('Thanks!')
|
|
29
|
+
expect(DEFAULT_MCPUI_STRINGS.retry).toBe('Retry')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('useMCPUIStrings returns the EN defaults with no provider mounted', () => {
|
|
33
|
+
let captured: ReturnType<typeof useMCPUIStrings> | undefined
|
|
34
|
+
const Probe = () => {
|
|
35
|
+
captured = useMCPUIStrings()
|
|
36
|
+
return <span>probe</span>
|
|
37
|
+
}
|
|
38
|
+
render(() => <Probe />)
|
|
39
|
+
expect(captured).toEqual(DEFAULT_MCPUI_STRINGS)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('provider partial-merges over the EN defaults', () => {
|
|
43
|
+
let captured: ReturnType<typeof useMCPUIStrings> | undefined
|
|
44
|
+
const Probe = () => {
|
|
45
|
+
captured = useMCPUIStrings()
|
|
46
|
+
return <span>probe</span>
|
|
47
|
+
}
|
|
48
|
+
render(() => (
|
|
49
|
+
<MCPUIStringsProvider strings={{ expand: 'Agrandir', feedbackUseful: 'Utile' }}>
|
|
50
|
+
<Probe />
|
|
51
|
+
</MCPUIStringsProvider>
|
|
52
|
+
))
|
|
53
|
+
// Overridden keys
|
|
54
|
+
expect(captured!.expand).toBe('Agrandir')
|
|
55
|
+
expect(captured!.feedbackUseful).toBe('Utile')
|
|
56
|
+
// Untouched keys fall back to EN
|
|
57
|
+
expect(captured!.retry).toBe('Retry')
|
|
58
|
+
expect(captured!.closeExpandedView).toBe('Close expanded view')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('FeedbackInline reads its ack from the provider (FR override)', () => {
|
|
62
|
+
const { getByText, container } = render(() => (
|
|
63
|
+
<MCPUIStringsProvider
|
|
64
|
+
strings={{ feedbackPositiveAck: 'Merci !', feedbackUseful: 'Utile' }}
|
|
65
|
+
>
|
|
66
|
+
<FeedbackInline onSubmit={() => {}} />
|
|
67
|
+
</MCPUIStringsProvider>
|
|
68
|
+
))
|
|
69
|
+
// Tooltip from provider
|
|
70
|
+
const upBtn = container.querySelector('[data-feedback-inline-rating="positive"]')
|
|
71
|
+
expect(upBtn?.getAttribute('title')).toBe('Utile')
|
|
72
|
+
// Ack from provider after click
|
|
73
|
+
fireEvent.click(upBtn!)
|
|
74
|
+
expect(getByText('Merci !')).toBeTruthy()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('FeedbackInline defaults to EN ack when no provider is mounted', () => {
|
|
78
|
+
const { getByText, container } = render(() => <FeedbackInline onSubmit={() => {}} />)
|
|
79
|
+
const upBtn = container.querySelector('[data-feedback-inline-rating="positive"]')
|
|
80
|
+
fireEvent.click(upBtn!)
|
|
81
|
+
expect(getByText('Thanks!')).toBeTruthy()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('FeedbackInline positiveAck prop still wins over the provider', () => {
|
|
85
|
+
const { getByText, container } = render(() => (
|
|
86
|
+
<MCPUIStringsProvider strings={{ feedbackPositiveAck: 'FromProvider' }}>
|
|
87
|
+
<FeedbackInline onSubmit={() => {}} positiveAck="FromProp" />
|
|
88
|
+
</MCPUIStringsProvider>
|
|
89
|
+
))
|
|
90
|
+
const upBtn = container.querySelector('[data-feedback-inline-rating="positive"]')
|
|
91
|
+
fireEvent.click(upBtn!)
|
|
92
|
+
expect(getByText('FromProp')).toBeTruthy()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('ExpandableWrapper reads the expand-button tooltip from the provider', () => {
|
|
96
|
+
const { container } = render(() => (
|
|
97
|
+
<MCPUIStringsProvider strings={{ expand: 'Plein écran' }}>
|
|
98
|
+
<ExpandableWrapper title="Données">
|
|
99
|
+
<div>content</div>
|
|
100
|
+
</ExpandableWrapper>
|
|
101
|
+
</MCPUIStringsProvider>
|
|
102
|
+
))
|
|
103
|
+
const expandBtn = container.querySelector('button[aria-label="Expand to fullscreen"]')
|
|
104
|
+
expect(expandBtn?.getAttribute('title')).toBe('Plein écran')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('ExpandableWrapper falls back to EN with no provider', () => {
|
|
108
|
+
const { container } = render(() => (
|
|
109
|
+
<ExpandableWrapper title="Data">
|
|
110
|
+
<div>content</div>
|
|
111
|
+
</ExpandableWrapper>
|
|
112
|
+
))
|
|
113
|
+
const expandBtn = container.querySelector('button[aria-label="Expand to fullscreen"]')
|
|
114
|
+
expect(expandBtn?.getAttribute('title')).toBe('Expand')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPUIStringsContext — i18n for the library's own "chrome" strings.
|
|
3
|
+
*
|
|
4
|
+
* @since v6.6.0 (D2 / R4 of ROADMAP-opendata-macro-mcpui)
|
|
5
|
+
*
|
|
6
|
+
* ## Scope — chrome only, NOT content
|
|
7
|
+
*
|
|
8
|
+
* MCP-UI renders two kinds of text :
|
|
9
|
+
*
|
|
10
|
+
* - **Content** — table headers, chart titles, action labels, prompt
|
|
11
|
+
* questions. These come from the payload and are ALREADY localized by
|
|
12
|
+
* whoever produced the payload (the connector / MCP server). MCP-UI
|
|
13
|
+
* renders them verbatim and this context never touches them.
|
|
14
|
+
* - **Chrome** — the handful of strings the library itself hardcodes :
|
|
15
|
+
* the expand-button tooltip, the feedback acknowledgements, etc. THIS
|
|
16
|
+
* is what `MCPUIStrings` covers.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately no full i18n framework here : no per-renderer
|
|
19
|
+
* `locale` prop, no message catalogue, no ICU. A flat string map behind a
|
|
20
|
+
* context is enough — the chrome surface is small and static.
|
|
21
|
+
*
|
|
22
|
+
* ## Defaults are English
|
|
23
|
+
*
|
|
24
|
+
* `DEFAULT_MCPUI_STRINGS` is English. A published library should not ship
|
|
25
|
+
* hardcoded French. Consumers that want another language wrap their tree
|
|
26
|
+
* in `<MCPUIStringsProvider strings={...}>` with a partial override.
|
|
27
|
+
*
|
|
28
|
+
* Component props that already carry a label (e.g. `FeedbackInline`'s
|
|
29
|
+
* `positiveAck`, `ExpandableWrapper`'s `copyLabel`) keep priority over the
|
|
30
|
+
* provider — the provider only fills the gap when no explicit prop is set.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* import { MCPUIStringsProvider } from '@seed-ship/mcp-ui-solid'
|
|
35
|
+
*
|
|
36
|
+
* <MCPUIStringsProvider strings={{ expand: 'Agrandir', feedbackUseful: 'Utile' }}>
|
|
37
|
+
* <App />
|
|
38
|
+
* </MCPUIStringsProvider>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { createContext, useContext, type JSX } from 'solid-js'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The library's own chrome strings. Flat map, no interpolation.
|
|
46
|
+
*
|
|
47
|
+
* Marked exhaustive as of v6.6.0 ; new chrome strings added by later
|
|
48
|
+
* renderers extend this interface (the `MCPUIStringsProvider` merge keeps
|
|
49
|
+
* older consumers working — any unset key falls back to the EN default).
|
|
50
|
+
*/
|
|
51
|
+
export interface MCPUIStrings {
|
|
52
|
+
// ── ExpandableWrapper toolbar ──────────────────────────────
|
|
53
|
+
/** `title` of the expand-to-fullscreen button. */
|
|
54
|
+
expand: string
|
|
55
|
+
/** Heading + `aria-label` of the fullscreen modal when no title is given. */
|
|
56
|
+
expandedView: string
|
|
57
|
+
/** Default tooltip of the copy button (overridden by `copyLabel` prop). */
|
|
58
|
+
copyToClipboard: string
|
|
59
|
+
/** `aria-label` of the close button in the fullscreen modal. */
|
|
60
|
+
closeExpandedView: string
|
|
61
|
+
|
|
62
|
+
// ── FeedbackInline (response-quality feedback) ─────────────
|
|
63
|
+
/** `title` of the thumb-up button. */
|
|
64
|
+
feedbackUseful: string
|
|
65
|
+
/** `title` of the thumb-down button. */
|
|
66
|
+
feedbackNotUseful: string
|
|
67
|
+
/** Acknowledgement shown after a positive rating (overridden by `positiveAck`). */
|
|
68
|
+
feedbackPositiveAck: string
|
|
69
|
+
/** Acknowledgement shown after a negative rating (overridden by `negativeAck`). */
|
|
70
|
+
feedbackNegativeAck: string
|
|
71
|
+
|
|
72
|
+
// ── Generic chrome ────────────────────────────────────────
|
|
73
|
+
/** Label of the streaming retry button. */
|
|
74
|
+
retry: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* English defaults. A published library ships no hardcoded non-English
|
|
79
|
+
* chrome — consumers localize via `<MCPUIStringsProvider>`.
|
|
80
|
+
*/
|
|
81
|
+
export const DEFAULT_MCPUI_STRINGS: MCPUIStrings = {
|
|
82
|
+
expand: 'Expand',
|
|
83
|
+
expandedView: 'Expanded view',
|
|
84
|
+
copyToClipboard: 'Copy to clipboard',
|
|
85
|
+
closeExpandedView: 'Close expanded view',
|
|
86
|
+
feedbackUseful: 'Useful',
|
|
87
|
+
feedbackNotUseful: 'Not useful',
|
|
88
|
+
feedbackPositiveAck: 'Thanks!',
|
|
89
|
+
feedbackNegativeAck: "Noted — we'll improve",
|
|
90
|
+
retry: 'Retry',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const MCPUIStringsContext = createContext<MCPUIStrings>(DEFAULT_MCPUI_STRINGS)
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reads the active chrome strings. Returns `DEFAULT_MCPUI_STRINGS` when no
|
|
97
|
+
* `<MCPUIStringsProvider>` is mounted above — every renderer works
|
|
98
|
+
* standalone with English chrome.
|
|
99
|
+
*/
|
|
100
|
+
export function useMCPUIStrings(): MCPUIStrings {
|
|
101
|
+
return useContext(MCPUIStringsContext)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface MCPUIStringsProviderProps {
|
|
105
|
+
/**
|
|
106
|
+
* Partial override of the chrome strings. Any key left unset falls back
|
|
107
|
+
* to the English `DEFAULT_MCPUI_STRINGS` — so a consumer can localize
|
|
108
|
+
* just the strings they care about.
|
|
109
|
+
*/
|
|
110
|
+
strings?: Partial<MCPUIStrings>
|
|
111
|
+
children: JSX.Element
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Provides localized chrome strings to every MCP-UI renderer below it.
|
|
116
|
+
* Merges the partial `strings` override over the English defaults.
|
|
117
|
+
*/
|
|
118
|
+
export function MCPUIStringsProvider(props: MCPUIStringsProviderProps): JSX.Element {
|
|
119
|
+
// `props.strings` is read inside the getter so a reactive override
|
|
120
|
+
// (signal-backed) re-propagates ; for the common static case it is read
|
|
121
|
+
// once at mount.
|
|
122
|
+
const value = (): MCPUIStrings => ({ ...DEFAULT_MCPUI_STRINGS, ...props.strings })
|
|
123
|
+
return (
|
|
124
|
+
<MCPUIStringsContext.Provider value={value()}>
|
|
125
|
+
{props.children}
|
|
126
|
+
</MCPUIStringsContext.Provider>
|
|
127
|
+
)
|
|
128
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,21 @@ export { ComponentToolbar } from './components/ComponentToolbar'
|
|
|
40
40
|
export { FeedbackInline } from './components/FeedbackInline'
|
|
41
41
|
export type { FeedbackInlineProps, FeedbackInlineContext } from './components/FeedbackInline'
|
|
42
42
|
|
|
43
|
+
// Presentation feedback (v6.6.0 — R3 ; separate axis from FeedbackInline)
|
|
44
|
+
export {
|
|
45
|
+
PresentationFeedback,
|
|
46
|
+
DEFAULT_PRESENTATION_FEEDBACK_LABELS,
|
|
47
|
+
} from './components/PresentationFeedback'
|
|
48
|
+
export type {
|
|
49
|
+
PresentationFeedbackProps,
|
|
50
|
+
PresentationFeedbackLabels,
|
|
51
|
+
} from './components/PresentationFeedback'
|
|
52
|
+
export type {
|
|
53
|
+
ConnectorRenderFeedback,
|
|
54
|
+
ConnectorRenderProblem,
|
|
55
|
+
ConnectorPreferredLayout,
|
|
56
|
+
} from '@seed-ship/mcp-ui-spec'
|
|
57
|
+
|
|
43
58
|
// Chat Bus (v2.4.0 — @experimental)
|
|
44
59
|
export { ChatBusProvider, useChatBus } from './hooks/useChatBus'
|
|
45
60
|
export { ChatPrompt } from './components/ChatPrompt'
|
|
@@ -113,6 +128,18 @@ export type {
|
|
|
113
128
|
DuplicateMountReporter,
|
|
114
129
|
} from './utils/duplicate-mount-registry'
|
|
115
130
|
|
|
131
|
+
// Chrome i18n — library's own strings, EN defaults (v6.6.0 — D2/R4)
|
|
132
|
+
export {
|
|
133
|
+
MCPUIStringsProvider,
|
|
134
|
+
MCPUIStringsContext,
|
|
135
|
+
useMCPUIStrings,
|
|
136
|
+
DEFAULT_MCPUI_STRINGS,
|
|
137
|
+
} from './context/MCPUIStringsContext'
|
|
138
|
+
export type {
|
|
139
|
+
MCPUIStrings,
|
|
140
|
+
MCPUIStringsProviderProps,
|
|
141
|
+
} from './context/MCPUIStringsContext'
|
|
142
|
+
|
|
116
143
|
// Telemetry sink (B.5 — v5.6.0)
|
|
117
144
|
export {
|
|
118
145
|
MCPUITelemetryProvider,
|