@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +37 -0
  3. package/dist/adapters/connector.cjs +112 -0
  4. package/dist/adapters/connector.cjs.map +1 -0
  5. package/dist/adapters/connector.d.ts +71 -0
  6. package/dist/adapters/connector.d.ts.map +1 -0
  7. package/dist/adapters/connector.js +112 -0
  8. package/dist/adapters/connector.js.map +1 -0
  9. package/dist/adapters/index.d.ts +18 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters.cjs +6 -0
  12. package/dist/adapters.cjs.map +1 -0
  13. package/dist/adapters.d.cts +18 -0
  14. package/dist/adapters.d.ts +18 -0
  15. package/dist/adapters.js +6 -0
  16. package/dist/adapters.js.map +1 -0
  17. package/dist/components/ActionGroupRenderer.cjs +12 -3
  18. package/dist/components/ActionGroupRenderer.cjs.map +1 -1
  19. package/dist/components/ActionGroupRenderer.d.ts.map +1 -1
  20. package/dist/components/ActionGroupRenderer.js +12 -3
  21. package/dist/components/ActionGroupRenderer.js.map +1 -1
  22. package/dist/components/ExpandableWrapper.cjs +24 -6
  23. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  24. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  25. package/dist/components/ExpandableWrapper.js +24 -6
  26. package/dist/components/ExpandableWrapper.js.map +1 -1
  27. package/dist/components/FeedbackInline.cjs +6 -2
  28. package/dist/components/FeedbackInline.cjs.map +1 -1
  29. package/dist/components/FeedbackInline.d.ts +2 -2
  30. package/dist/components/FeedbackInline.d.ts.map +1 -1
  31. package/dist/components/FeedbackInline.js +7 -3
  32. package/dist/components/FeedbackInline.js.map +1 -1
  33. package/dist/components/PresentationFeedback.cjs +207 -0
  34. package/dist/components/PresentationFeedback.cjs.map +1 -0
  35. package/dist/components/PresentationFeedback.d.ts +113 -0
  36. package/dist/components/PresentationFeedback.d.ts.map +1 -0
  37. package/dist/components/PresentationFeedback.js +207 -0
  38. package/dist/components/PresentationFeedback.js.map +1 -0
  39. package/dist/components/StreamingUIRenderer.cjs +82 -195
  40. package/dist/components/StreamingUIRenderer.cjs.map +1 -1
  41. package/dist/components/StreamingUIRenderer.d.ts +25 -5
  42. package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
  43. package/dist/components/StreamingUIRenderer.js +84 -197
  44. package/dist/components/StreamingUIRenderer.js.map +1 -1
  45. package/dist/components/UIResourceRenderer.cjs +22 -15
  46. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  47. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  48. package/dist/components/UIResourceRenderer.js +22 -15
  49. package/dist/components/UIResourceRenderer.js.map +1 -1
  50. package/dist/components/index.d.ts +2 -0
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components.cjs +3 -0
  53. package/dist/components.cjs.map +1 -1
  54. package/dist/components.d.cts +2 -0
  55. package/dist/components.d.ts +2 -0
  56. package/dist/components.js +3 -0
  57. package/dist/components.js.map +1 -1
  58. package/dist/context/MCPActionContext.cjs +4 -1
  59. package/dist/context/MCPActionContext.cjs.map +1 -1
  60. package/dist/context/MCPActionContext.d.ts +13 -1
  61. package/dist/context/MCPActionContext.d.ts.map +1 -1
  62. package/dist/context/MCPActionContext.js +4 -1
  63. package/dist/context/MCPActionContext.js.map +1 -1
  64. package/dist/context/MCPUIStringsContext.cjs +38 -0
  65. package/dist/context/MCPUIStringsContext.cjs.map +1 -0
  66. package/dist/context/MCPUIStringsContext.d.ts +95 -0
  67. package/dist/context/MCPUIStringsContext.d.ts.map +1 -0
  68. package/dist/context/MCPUIStringsContext.js +38 -0
  69. package/dist/context/MCPUIStringsContext.js.map +1 -0
  70. package/dist/index.cjs +8 -0
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.d.cts +5 -0
  73. package/dist/index.d.ts +5 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +8 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/mcp-ui-spec/dist/schemas.cjs +103 -0
  78. package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
  79. package/dist/mcp-ui-spec/dist/schemas.js +103 -0
  80. package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
  81. package/docs/briefs/ROADMAP-opendata-macro-mcpui.md +912 -0
  82. package/package.json +17 -5
  83. package/src/adapters/connector.test.ts +165 -0
  84. package/src/adapters/connector.ts +234 -0
  85. package/src/adapters/index.ts +24 -0
  86. package/src/components/ActionGroupRenderer.test.tsx +1 -0
  87. package/src/components/ActionGroupRenderer.tsx +19 -4
  88. package/src/components/ActionSubmit.test.tsx +188 -0
  89. package/src/components/ExpandableWrapper.test.tsx +5 -2
  90. package/src/components/ExpandableWrapper.tsx +8 -6
  91. package/src/components/FeedbackInline.test.tsx +6 -3
  92. package/src/components/FeedbackInline.tsx +8 -6
  93. package/src/components/PresentationFeedback.test.tsx +163 -0
  94. package/src/components/PresentationFeedback.tsx +326 -0
  95. package/src/components/StreamingUIRenderer.parity.test.tsx +158 -0
  96. package/src/components/StreamingUIRenderer.tsx +42 -166
  97. package/src/components/UIResourceRenderer.tsx +19 -6
  98. package/src/components/index.ts +10 -0
  99. package/src/context/MCPActionContext.tsx +17 -1
  100. package/src/context/MCPUIStringsContext.test.tsx +116 -0
  101. package/src/context/MCPUIStringsContext.tsx +128 -0
  102. package/src/index.ts +27 -0
  103. package/tsconfig.tsbuildinfo +1 -1
  104. package/vite.config.ts +1 -0
@@ -1,8 +1,22 @@
1
1
  /**
2
- * StreamingUIRenderer Component - Phase 2
2
+ * StreamingUIRenderer Component
3
3
  *
4
- * Renders streaming dashboard components with skeleton states and progress indicators.
5
- * Uses the useStreamingUI hook for SSE connection and state management.
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, onCleanup } from 'solid-js'
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 { validateComponent } from '../services/validation'
28
- import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
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). See `ValidationErrorMode` in `UIResourceRenderer`.
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
- * Component Renderer - Inline lightweight version
48
- * (Full implementation in UIResourceRenderer)
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 StreamingComponentRenderer(props: {
51
- component: UIComponent
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
- Retry
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
- <StreamingComponentRenderer
318
- component={component}
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 || (params.action === 'tool-call' && isExecuting())
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={params.action === 'submit' ? 'submit' : 'button'}
1595
+ type="button"
1583
1596
  disabled={isDisabled()}
1584
- aria-busy={isExecuting() && params.action === 'tool-call'}
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() && params.action === 'tool-call'}>
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() && params.action === 'tool-call')}>
1613
+ <Show when={params.icon && !(isExecuting() && isExecutable())}>
1601
1614
  <span aria-hidden="true">{params.icon}</span>
1602
1615
  </Show>
1603
1616
  {params.label}
@@ -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,