@seed-ship/mcp-ui-solid 6.14.0 → 6.15.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 +19 -0
- package/dist/components/UIResourceRenderer.cjs +100 -75
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +100 -75
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/package.json +1 -1
- package/src/components/UIResourceRenderer.tsx +35 -0
- package/src/components/UnsupportedComponent.test.tsx +77 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1478,6 +1478,14 @@ function ComponentRenderer(props: {
|
|
|
1478
1478
|
const mode: ValidationErrorMode = props.errorMode ?? 'block'
|
|
1479
1479
|
const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
|
|
1480
1480
|
|
|
1481
|
+
// P1.6 — an UNKNOWN component type must never produce a silent blank,
|
|
1482
|
+
// whatever the errorMode. The renderer has no branch for it, so even
|
|
1483
|
+
// `silent` would otherwise render nothing. Always surface a visible
|
|
1484
|
+
// "Unsupported component type" notice + a render:error telemetry signal.
|
|
1485
|
+
if (validation.errors?.some((e) => e.code === 'UNKNOWN_COMPONENT_TYPE')) {
|
|
1486
|
+
return <UnsupportedComponentFallback component={props.component} />
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1481
1489
|
if (mode === 'silent') {
|
|
1482
1490
|
return null
|
|
1483
1491
|
}
|
|
@@ -1586,10 +1594,37 @@ function ComponentRenderer(props: {
|
|
|
1586
1594
|
<Show when={props.component.type === 'graph'}>
|
|
1587
1595
|
<GraphRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
|
|
1588
1596
|
</Show>
|
|
1597
|
+
{/* P1.6 — `footer` is a valid type with a real renderer but had no
|
|
1598
|
+
dispatch branch, so a standalone footer component rendered a silent
|
|
1599
|
+
blank (it was only auto-injected at the layout level). */}
|
|
1600
|
+
<Show when={props.component.type === 'footer'}>
|
|
1601
|
+
<FooterRenderer params={props.component.params as any} />
|
|
1602
|
+
</Show>
|
|
1589
1603
|
</GenerativeUIErrorBoundary>
|
|
1590
1604
|
)
|
|
1591
1605
|
}
|
|
1592
1606
|
|
|
1607
|
+
/**
|
|
1608
|
+
* Visible fallback for a component whose `type` is not recognized (audit P1.6).
|
|
1609
|
+
*
|
|
1610
|
+
* An unknown type has no renderer branch, so without this it would render a
|
|
1611
|
+
* **silent blank** — even under `errorMode: 'silent'`. The validation gate
|
|
1612
|
+
* routes unknown types here regardless of mode, so the user always sees an
|
|
1613
|
+
* "Unsupported component type: X" notice. The telemetry signal is emitted by
|
|
1614
|
+
* the gate itself (`validation:failed` with `firstErrorCode:
|
|
1615
|
+
* 'UNKNOWN_COMPONENT_TYPE'`), so this component stays purely presentational.
|
|
1616
|
+
*/
|
|
1617
|
+
function UnsupportedComponentFallback(props: { component: UIComponent }) {
|
|
1618
|
+
return (
|
|
1619
|
+
<div
|
|
1620
|
+
role="alert"
|
|
1621
|
+
class="w-full rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-200"
|
|
1622
|
+
>
|
|
1623
|
+
Unsupported component type: <code class="font-mono">{props.component.type}</code>
|
|
1624
|
+
</div>
|
|
1625
|
+
)
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1593
1628
|
/**
|
|
1594
1629
|
* Render an action component (button or link)
|
|
1595
1630
|
* Refactored in Phase 5.0 to use useAction hook for Context-based execution
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.15.0 — audit P1.6: a component that reaches the renderer with a type that
|
|
3
|
+
* has no render branch must never produce a silent blank.
|
|
4
|
+
*
|
|
5
|
+
* `errorMode: 'silent'` lets a payload past the validation gate without an
|
|
6
|
+
* error card, so a bogus / known-but-unrendered type would otherwise render
|
|
7
|
+
* nothing. We assert the visible "Unsupported component type" notice instead,
|
|
8
|
+
* and that a real type (`footer`, previously missing a branch) now renders.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
11
|
+
import { render, cleanup, waitFor } from '@solidjs/testing-library'
|
|
12
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
13
|
+
import { MCPUITelemetryProvider } from '../context/MCPUITelemetryContext'
|
|
14
|
+
import type { UIComponent } from '../types'
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup)
|
|
17
|
+
|
|
18
|
+
const bogus = {
|
|
19
|
+
id: 'x1',
|
|
20
|
+
type: 'totally-made-up' as any,
|
|
21
|
+
position: { colStart: 1, colSpan: 12 },
|
|
22
|
+
params: {},
|
|
23
|
+
} satisfies UIComponent
|
|
24
|
+
|
|
25
|
+
describe('Unsupported component never renders blank (P1.6)', () => {
|
|
26
|
+
it('shows a visible "Unsupported component type" notice', async () => {
|
|
27
|
+
// errorMode: 'silent' skips the validation error card, so the bogus type
|
|
28
|
+
// reaches the render dispatch — the catch-all must still surface it.
|
|
29
|
+
const { container } = render(() => (
|
|
30
|
+
<UIResourceRenderer content={bogus} errorMode="silent" />
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
await waitFor(() => {
|
|
34
|
+
expect(container.textContent ?? '').toContain('Unsupported component type')
|
|
35
|
+
})
|
|
36
|
+
expect(container.textContent).toContain('totally-made-up')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('emits a validation:failed telemetry signal for the unsupported type', async () => {
|
|
40
|
+
const events: Array<{ type: string; errorMessage?: string }> = []
|
|
41
|
+
render(() => (
|
|
42
|
+
<MCPUITelemetryProvider
|
|
43
|
+
sink={(batch) => events.push(...batch)}
|
|
44
|
+
options={{ bufferMs: 0 }}
|
|
45
|
+
>
|
|
46
|
+
<UIResourceRenderer content={bogus} errorMode="silent" />
|
|
47
|
+
</MCPUITelemetryProvider>
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
// The validation gate emits `validation:failed` with the precise
|
|
51
|
+
// `UNKNOWN_COMPONENT_TYPE` code — the privacy-safe drift signal hosts watch.
|
|
52
|
+
await waitFor(() => {
|
|
53
|
+
expect(
|
|
54
|
+
events.some(
|
|
55
|
+
(e: any) =>
|
|
56
|
+
e.type === 'validation:failed' && e.firstErrorCode === 'UNKNOWN_COMPONENT_TYPE'
|
|
57
|
+
)
|
|
58
|
+
).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('renders a standalone footer component (previously a silent blank)', async () => {
|
|
63
|
+
const footer: UIComponent = {
|
|
64
|
+
id: 'f1',
|
|
65
|
+
type: 'footer',
|
|
66
|
+
position: { colStart: 1, colSpan: 12 },
|
|
67
|
+
params: { poweredBy: 'Deposium', sourceCount: 3 } as any,
|
|
68
|
+
}
|
|
69
|
+
const { container } = render(() => <UIResourceRenderer content={footer} />)
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(container.textContent ?? '').toContain('Deposium')
|
|
73
|
+
})
|
|
74
|
+
// Not the unsupported notice — footer is a real renderer now.
|
|
75
|
+
expect(container.textContent).not.toContain('Unsupported component type')
|
|
76
|
+
})
|
|
77
|
+
})
|