@miragon/mcp-toolkit-ui 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/README.md +8 -2
  2. package/dist/app/builder/BuilderBanners.d.ts +7 -2
  3. package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
  4. package/dist/app/builder/BuilderBanners.js +8 -3
  5. package/dist/app/builder/BuilderBanners.js.map +1 -1
  6. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  7. package/dist/app/builder/CanvasRow.js +12 -14
  8. package/dist/app/builder/CanvasRow.js.map +1 -1
  9. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  10. package/dist/app/builder/CatalogueSheet.js +6 -5
  11. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  12. package/dist/app/builder/EmptyHint.d.ts +4 -0
  13. package/dist/app/builder/EmptyHint.d.ts.map +1 -1
  14. package/dist/app/builder/EmptyHint.js +6 -1
  15. package/dist/app/builder/EmptyHint.js.map +1 -1
  16. package/dist/app/builder/LayoutSurface.js +3 -3
  17. package/dist/app/builder/LayoutSurface.js.map +1 -1
  18. package/dist/app/builder/PipelineStrip.d.ts +3 -1
  19. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  20. package/dist/app/builder/PipelineStrip.js +11 -6
  21. package/dist/app/builder/PipelineStrip.js.map +1 -1
  22. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  23. package/dist/app/builder/SaveDialog.js +2 -2
  24. package/dist/app/builder/SaveDialog.js.map +1 -1
  25. package/dist/app/builder/Toolbar.d.ts +3 -1
  26. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  27. package/dist/app/builder/Toolbar.js +9 -3
  28. package/dist/app/builder/Toolbar.js.map +1 -1
  29. package/dist/app/builder/Workspace.js +2 -2
  30. package/dist/app/builder/Workspace.js.map +1 -1
  31. package/dist/app/builder/builder-model.d.ts +3 -2
  32. package/dist/app/builder/builder-model.d.ts.map +1 -1
  33. package/dist/app/builder/builder-model.js +5 -4
  34. package/dist/app/builder/builder-model.js.map +1 -1
  35. package/dist/app/builder/labels.d.ts +13 -62
  36. package/dist/app/builder/labels.d.ts.map +1 -1
  37. package/dist/app/builder/labels.js +9 -58
  38. package/dist/app/builder/labels.js.map +1 -1
  39. package/dist/app/builder/status-chip.d.ts.map +1 -1
  40. package/dist/app/builder/status-chip.js +3 -2
  41. package/dist/app/builder/status-chip.js.map +1 -1
  42. package/dist/app/builder/use-builder-catalogue.d.ts +1 -0
  43. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  44. package/dist/app/builder/use-builder-catalogue.js +6 -0
  45. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  46. package/dist/app/builder/use-save-dashboard.d.ts +3 -1
  47. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
  48. package/dist/app/builder/use-save-dashboard.js +3 -1
  49. package/dist/app/builder/use-save-dashboard.js.map +1 -1
  50. package/dist/app/index.d.ts +1 -1
  51. package/dist/app/index.d.ts.map +1 -1
  52. package/dist/app/index.js.map +1 -1
  53. package/dist/app/layout-builder.d.ts +7 -2
  54. package/dist/app/layout-builder.d.ts.map +1 -1
  55. package/dist/app/layout-builder.js +17 -5
  56. package/dist/app/layout-builder.js.map +1 -1
  57. package/dist/app/mcp-app-view-chrome.d.ts +94 -20
  58. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  59. package/dist/app/mcp-app-view-chrome.js +109 -40
  60. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  61. package/dist/app/mcp-app-view-states.d.ts +15 -0
  62. package/dist/app/mcp-app-view-states.d.ts.map +1 -0
  63. package/dist/app/mcp-app-view-states.js +34 -0
  64. package/dist/app/mcp-app-view-states.js.map +1 -0
  65. package/dist/app/mcp-app-view.d.ts +40 -5
  66. package/dist/app/mcp-app-view.d.ts.map +1 -1
  67. package/dist/app/mcp-app-view.js +174 -128
  68. package/dist/app/mcp-app-view.js.map +1 -1
  69. package/dist/app/mcp-toolkit-app.d.ts +2 -1
  70. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  71. package/dist/app/mcp-toolkit-app.js +25 -3
  72. package/dist/app/mcp-toolkit-app.js.map +1 -1
  73. package/dist/app/use-view-lifecycle.d.ts +38 -0
  74. package/dist/app/use-view-lifecycle.d.ts.map +1 -0
  75. package/dist/app/use-view-lifecycle.js +79 -0
  76. package/dist/app/use-view-lifecycle.js.map +1 -0
  77. package/dist/app/view-lifecycle.d.ts +109 -0
  78. package/dist/app/view-lifecycle.d.ts.map +1 -0
  79. package/dist/app/view-lifecycle.js +120 -0
  80. package/dist/app/view-lifecycle.js.map +1 -0
  81. package/dist/app/widget-props-sheet.d.ts +3 -9
  82. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  83. package/dist/app/widget-props-sheet.js +6 -13
  84. package/dist/app/widget-props-sheet.js.map +1 -1
  85. package/dist/app/widget-renderer.d.ts +41 -1
  86. package/dist/app/widget-renderer.d.ts.map +1 -1
  87. package/dist/app/widget-renderer.js +82 -31
  88. package/dist/app/widget-renderer.js.map +1 -1
  89. package/dist/components/DrillButton.d.ts.map +1 -1
  90. package/dist/components/DrillButton.js +2 -1
  91. package/dist/components/DrillButton.js.map +1 -1
  92. package/dist/components/EmptyState.d.ts +33 -0
  93. package/dist/components/EmptyState.d.ts.map +1 -0
  94. package/dist/components/EmptyState.js +17 -0
  95. package/dist/components/EmptyState.js.map +1 -0
  96. package/dist/components/ErrorState.d.ts +50 -0
  97. package/dist/components/ErrorState.d.ts.map +1 -0
  98. package/dist/components/ErrorState.js +60 -0
  99. package/dist/components/ErrorState.js.map +1 -0
  100. package/dist/components/FilterBar.d.ts +1 -1
  101. package/dist/components/FilterBar.d.ts.map +1 -1
  102. package/dist/components/FilterBar.js +7 -4
  103. package/dist/components/FilterBar.js.map +1 -1
  104. package/dist/components/GroupCard.d.ts.map +1 -1
  105. package/dist/components/GroupCard.js +2 -1
  106. package/dist/components/GroupCard.js.map +1 -1
  107. package/dist/components/KpiGrid.d.ts +10 -3
  108. package/dist/components/KpiGrid.d.ts.map +1 -1
  109. package/dist/components/KpiGrid.js +31 -9
  110. package/dist/components/KpiGrid.js.map +1 -1
  111. package/dist/components/ListFooter.d.ts +3 -2
  112. package/dist/components/ListFooter.d.ts.map +1 -1
  113. package/dist/components/ListFooter.js +14 -4
  114. package/dist/components/ListFooter.js.map +1 -1
  115. package/dist/components/Pills.d.ts +10 -6
  116. package/dist/components/Pills.d.ts.map +1 -1
  117. package/dist/components/Pills.js +12 -6
  118. package/dist/components/Pills.js.map +1 -1
  119. package/dist/components/Toast.d.ts +37 -0
  120. package/dist/components/Toast.d.ts.map +1 -0
  121. package/dist/components/Toast.js +91 -0
  122. package/dist/components/Toast.js.map +1 -0
  123. package/dist/components/WidgetHeader.js +1 -1
  124. package/dist/hooks/use-tool-result-recovery.d.ts +23 -1
  125. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  126. package/dist/hooks/use-tool-result-recovery.js +21 -7
  127. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  128. package/dist/hooks/use-view-data.d.ts +6 -0
  129. package/dist/hooks/use-view-data.d.ts.map +1 -1
  130. package/dist/hooks/use-view-data.js +6 -1
  131. package/dist/hooks/use-view-data.js.map +1 -1
  132. package/dist/index.d.ts +12 -5
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +14 -4
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/focus-ring.d.ts +18 -0
  137. package/dist/lib/focus-ring.d.ts.map +1 -0
  138. package/dist/lib/focus-ring.js +18 -0
  139. package/dist/lib/focus-ring.js.map +1 -0
  140. package/dist/lib/icon-stroke.d.ts +12 -0
  141. package/dist/lib/icon-stroke.d.ts.map +1 -0
  142. package/dist/lib/icon-stroke.js +12 -0
  143. package/dist/lib/icon-stroke.js.map +1 -0
  144. package/dist/lib/toast-model.d.ts +46 -0
  145. package/dist/lib/toast-model.d.ts.map +1 -0
  146. package/dist/lib/toast-model.js +36 -0
  147. package/dist/lib/toast-model.js.map +1 -0
  148. package/dist/lib/tone-utils.d.ts +43 -9
  149. package/dist/lib/tone-utils.d.ts.map +1 -1
  150. package/dist/lib/tone-utils.js +36 -7
  151. package/dist/lib/tone-utils.js.map +1 -1
  152. package/dist/locale/index.d.ts +3 -1
  153. package/dist/locale/index.d.ts.map +1 -1
  154. package/dist/locale/index.js +2 -1
  155. package/dist/locale/index.js.map +1 -1
  156. package/dist/locale/locale-provider.d.ts +42 -1
  157. package/dist/locale/locale-provider.d.ts.map +1 -1
  158. package/dist/locale/locale-provider.js +44 -3
  159. package/dist/locale/locale-provider.js.map +1 -1
  160. package/dist/locale/message-types.d.ts +223 -0
  161. package/dist/locale/message-types.d.ts.map +1 -0
  162. package/dist/locale/message-types.js +7 -0
  163. package/dist/locale/message-types.js.map +1 -0
  164. package/dist/locale/messages-de.d.ts +9 -0
  165. package/dist/locale/messages-de.d.ts.map +1 -0
  166. package/dist/locale/messages-de.js +151 -0
  167. package/dist/locale/messages-de.js.map +1 -0
  168. package/dist/locale/messages-en.d.ts +8 -0
  169. package/dist/locale/messages-en.d.ts.map +1 -0
  170. package/dist/locale/messages-en.js +150 -0
  171. package/dist/locale/messages-en.js.map +1 -0
  172. package/dist/locale/toolkit-messages.d.ts +39 -0
  173. package/dist/locale/toolkit-messages.d.ts.map +1 -0
  174. package/dist/locale/toolkit-messages.js +50 -0
  175. package/dist/locale/toolkit-messages.js.map +1 -0
  176. package/dist/primitives/alert.d.ts.map +1 -1
  177. package/dist/primitives/alert.js +4 -2
  178. package/dist/primitives/alert.js.map +1 -1
  179. package/dist/primitives/badge.d.ts +1 -1
  180. package/dist/primitives/badge.d.ts.map +1 -1
  181. package/dist/primitives/badge.js +14 -6
  182. package/dist/primitives/badge.js.map +1 -1
  183. package/dist/primitives/button.d.ts.map +1 -1
  184. package/dist/primitives/button.js +12 -8
  185. package/dist/primitives/button.js.map +1 -1
  186. package/dist/primitives/card.js +4 -4
  187. package/dist/primitives/card.js.map +1 -1
  188. package/dist/primitives/dialog.d.ts +7 -3
  189. package/dist/primitives/dialog.d.ts.map +1 -1
  190. package/dist/primitives/dialog.js +14 -7
  191. package/dist/primitives/dialog.js.map +1 -1
  192. package/dist/primitives/input.d.ts.map +1 -1
  193. package/dist/primitives/input.js +4 -1
  194. package/dist/primitives/input.js.map +1 -1
  195. package/dist/primitives/scroll-area.js +1 -1
  196. package/dist/primitives/scroll-area.js.map +1 -1
  197. package/dist/primitives/select.d.ts.map +1 -1
  198. package/dist/primitives/select.js +5 -2
  199. package/dist/primitives/select.js.map +1 -1
  200. package/dist/primitives/sheet.d.ts +3 -1
  201. package/dist/primitives/sheet.d.ts.map +1 -1
  202. package/dist/primitives/sheet.js +12 -6
  203. package/dist/primitives/sheet.js.map +1 -1
  204. package/dist/primitives/skeleton.js +1 -1
  205. package/dist/primitives/skeleton.js.map +1 -1
  206. package/dist/primitives/switch.d.ts.map +1 -1
  207. package/dist/primitives/switch.js +2 -1
  208. package/dist/primitives/switch.js.map +1 -1
  209. package/dist/primitives/tabs.d.ts.map +1 -1
  210. package/dist/primitives/tabs.js +5 -3
  211. package/dist/primitives/tabs.js.map +1 -1
  212. package/dist/render-cases.d.ts.map +1 -1
  213. package/dist/render-cases.js +20 -2
  214. package/dist/render-cases.js.map +1 -1
  215. package/dist/render-scan.d.ts +25 -0
  216. package/dist/render-scan.d.ts.map +1 -0
  217. package/dist/render-scan.js +66 -0
  218. package/dist/render-scan.js.map +1 -0
  219. package/dist/theme/cd-tokens.d.ts +66 -0
  220. package/dist/theme/cd-tokens.d.ts.map +1 -0
  221. package/dist/theme/cd-tokens.js +64 -0
  222. package/dist/theme/cd-tokens.js.map +1 -0
  223. package/dist/theme/create-theme.d.ts +196 -39
  224. package/dist/theme/create-theme.d.ts.map +1 -1
  225. package/dist/theme/create-theme.js +189 -13
  226. package/dist/theme/create-theme.js.map +1 -1
  227. package/dist/theme/fonts.d.ts +38 -0
  228. package/dist/theme/fonts.d.ts.map +1 -0
  229. package/dist/theme/fonts.js +56 -0
  230. package/dist/theme/fonts.js.map +1 -0
  231. package/dist/theme/host-fonts.d.ts +39 -0
  232. package/dist/theme/host-fonts.d.ts.map +1 -0
  233. package/dist/theme/host-fonts.js +44 -0
  234. package/dist/theme/host-fonts.js.map +1 -0
  235. package/dist/theme/index.d.ts +8 -1
  236. package/dist/theme/index.d.ts.map +1 -1
  237. package/dist/theme/index.js +5 -1
  238. package/dist/theme/index.js.map +1 -1
  239. package/dist/theme/portal-container.d.ts +16 -0
  240. package/dist/theme/portal-container.d.ts.map +1 -0
  241. package/dist/theme/portal-container.js +19 -0
  242. package/dist/theme/portal-container.js.map +1 -0
  243. package/dist/theme/presets.d.ts +24 -9
  244. package/dist/theme/presets.d.ts.map +1 -1
  245. package/dist/theme/presets.js +44 -11
  246. package/dist/theme/presets.js.map +1 -1
  247. package/dist/theme/theme-provider.d.ts +27 -9
  248. package/dist/theme/theme-provider.d.ts.map +1 -1
  249. package/dist/theme/theme-provider.js +42 -20
  250. package/dist/theme/theme-provider.js.map +1 -1
  251. package/package.json +13 -8
  252. package/src/app/builder/BuilderBanners.tsx +26 -13
  253. package/src/app/builder/CanvasRow.tsx +22 -21
  254. package/src/app/builder/CatalogueSheet.tsx +16 -14
  255. package/src/app/builder/EmptyHint.tsx +16 -11
  256. package/src/app/builder/LayoutSurface.tsx +7 -7
  257. package/src/app/builder/PipelineStrip.tsx +26 -17
  258. package/src/app/builder/SaveDialog.tsx +11 -12
  259. package/src/app/builder/Toolbar.tsx +19 -7
  260. package/src/app/builder/Workspace.tsx +5 -3
  261. package/src/app/builder/builder-feedback.test.tsx +75 -0
  262. package/src/app/builder/builder-model.test.ts +5 -0
  263. package/src/app/builder/builder-model.ts +5 -4
  264. package/src/app/builder/labels.ts +15 -121
  265. package/src/app/builder/status-chip.tsx +8 -4
  266. package/src/app/builder/use-builder-catalogue.ts +6 -0
  267. package/src/app/builder/use-save-dashboard.ts +5 -0
  268. package/src/app/index.ts +8 -1
  269. package/src/app/layout-builder.tsx +23 -3
  270. package/src/app/mcp-app-view-chrome.test.tsx +347 -14
  271. package/src/app/mcp-app-view-chrome.tsx +278 -100
  272. package/src/app/mcp-app-view-states.tsx +100 -0
  273. package/src/app/mcp-app-view.test.tsx +258 -0
  274. package/src/app/mcp-app-view.tsx +292 -185
  275. package/src/app/mcp-toolkit-app.tsx +31 -7
  276. package/src/app/use-view-lifecycle.ts +116 -0
  277. package/src/app/view-lifecycle.test.ts +227 -0
  278. package/src/app/view-lifecycle.ts +174 -0
  279. package/src/app/widget-props-sheet.tsx +13 -26
  280. package/src/app/widget-renderer.render.test.tsx +131 -0
  281. package/src/app/widget-renderer.tsx +136 -38
  282. package/src/components/DrillButton.tsx +2 -1
  283. package/src/components/EmptyState.tsx +81 -0
  284. package/src/components/ErrorState.tsx +182 -0
  285. package/src/components/FilterBar.tsx +9 -6
  286. package/src/components/GroupCard.tsx +3 -2
  287. package/src/components/KpiGrid.tsx +75 -44
  288. package/src/components/ListFooter.tsx +17 -6
  289. package/src/components/Pills.tsx +20 -11
  290. package/src/components/Toast.tsx +188 -0
  291. package/src/components/WidgetHeader.tsx +1 -1
  292. package/src/components/feedback-states.test.tsx +225 -0
  293. package/src/components/tone-figures.test.tsx +54 -0
  294. package/src/fonts.css +19 -0
  295. package/src/globals.css +9 -165
  296. package/src/hooks/use-tool-result-recovery.test.ts +25 -1
  297. package/src/hooks/use-tool-result-recovery.ts +34 -6
  298. package/src/hooks/use-view-data.test.tsx +60 -0
  299. package/src/hooks/use-view-data.ts +12 -1
  300. package/src/i18n-coverage.test.tsx +323 -0
  301. package/src/index.ts +57 -5
  302. package/src/lib/focus-ring.ts +20 -0
  303. package/src/lib/icon-stroke.test.ts +59 -0
  304. package/src/lib/icon-stroke.ts +13 -0
  305. package/src/lib/toast-model.test.ts +68 -0
  306. package/src/lib/toast-model.ts +63 -0
  307. package/src/lib/tone-utils.test.ts +47 -14
  308. package/src/lib/tone-utils.ts +61 -13
  309. package/src/locale/index.ts +13 -1
  310. package/src/locale/locale-provider.test.tsx +87 -0
  311. package/src/locale/locale-provider.tsx +90 -3
  312. package/src/locale/message-types.ts +221 -0
  313. package/src/locale/messages-de.ts +159 -0
  314. package/src/locale/messages-en.ts +157 -0
  315. package/src/locale/toolkit-messages.test.ts +216 -0
  316. package/src/locale/toolkit-messages.ts +82 -0
  317. package/src/no-hardcoded-copy.test.ts +432 -0
  318. package/src/primitives/alert.tsx +4 -2
  319. package/src/primitives/badge.tsx +14 -7
  320. package/src/primitives/button.tsx +13 -8
  321. package/src/primitives/card.tsx +4 -4
  322. package/src/primitives/dialog.tsx +32 -7
  323. package/src/primitives/input.tsx +5 -3
  324. package/src/primitives/scroll-area.tsx +1 -1
  325. package/src/primitives/select.tsx +6 -3
  326. package/src/primitives/sheet.tsx +26 -6
  327. package/src/primitives/skeleton.tsx +1 -1
  328. package/src/primitives/switch.tsx +4 -2
  329. package/src/primitives/tabs.tsx +9 -6
  330. package/src/render-cases.tsx +45 -1
  331. package/src/render-scan.ts +75 -0
  332. package/src/theme/cd-tokens.generated.css +48 -0
  333. package/src/theme/cd-tokens.test.ts +107 -0
  334. package/src/theme/cd-tokens.ts +67 -0
  335. package/src/theme/create-theme.test.ts +243 -5
  336. package/src/theme/create-theme.ts +313 -41
  337. package/src/theme/fonts.test.ts +85 -0
  338. package/src/theme/fonts.ts +72 -0
  339. package/src/theme/host-fonts.test.ts +135 -0
  340. package/src/theme/host-fonts.ts +52 -0
  341. package/src/theme/index.ts +15 -1
  342. package/src/theme/portal-container.ts +20 -0
  343. package/src/theme/presets.ts +44 -11
  344. package/src/theme/theme-css.test.ts +568 -0
  345. package/src/theme/theme-provider.test.tsx +82 -0
  346. package/src/theme/theme-provider.tsx +60 -24
  347. package/src/theme.css +314 -0
  348. package/ui-catalog.json +344 -17
@@ -0,0 +1,116 @@
1
+ import { useCallback, useEffect, useReducer, useRef, useState, type RefObject } from "react"
2
+ import {
3
+ decodeViewResult,
4
+ errorMessage,
5
+ resolveViewLifecycle,
6
+ RETRY_IDLE,
7
+ retryFocusTarget,
8
+ retryReducer,
9
+ waitKey,
10
+ withSlowHint,
11
+ type RenderCallStatus,
12
+ type ViewLifecycle,
13
+ } from "./view-lifecycle.js"
14
+
15
+ export interface UseViewLifecycleOptions<T> {
16
+ hasViewData: boolean
17
+ callStatus: RenderCallStatus
18
+ cancelled: boolean
19
+ callError: unknown
20
+ recoveryFailed: boolean
21
+ /** The originating tool (`hostContext.toolInfo`); a retry needs it. */
22
+ toolName: string | undefined
23
+ toolInput: Record<string, unknown> | null | undefined
24
+ callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>
25
+ isValid: (value: unknown) => value is T
26
+ /** Receives the payload of a successful retry. */
27
+ onResult: (data: T) => void
28
+ slowHintAfterMs: number
29
+ }
30
+
31
+ /**
32
+ * Drives the {@link resolveViewLifecycle} state machine: owns the retry and
33
+ * the slow-hint timer, and returns the current phase plus `retry()`.
34
+ */
35
+ export function useViewLifecycle<T>(options: UseViewLifecycleOptions<T>): {
36
+ lifecycle: ViewLifecycle
37
+ retry: () => void
38
+ } {
39
+ const { toolName, toolInput, callTool, isValid, onResult, slowHintAfterMs } = options
40
+ const [retryState, dispatch] = useReducer(retryReducer, RETRY_IDLE)
41
+ // The wait that went slow; a newer wait (another attempt) starts fresh.
42
+ const [slowWait, setSlowWait] = useState<string | null>(null)
43
+
44
+ const unslowed = resolveViewLifecycle({
45
+ hasViewData: options.hasViewData,
46
+ callStatus: options.callStatus,
47
+ cancelled: options.cancelled,
48
+ callErrorReason: errorMessage(options.callError),
49
+ recoveryFailed: options.recoveryFailed,
50
+ retryState,
51
+ slow: false,
52
+ canRetry: toolName !== undefined,
53
+ })
54
+ const currentWait = waitKey(unslowed, retryState)
55
+
56
+ useEffect(() => {
57
+ if (currentWait === null) return
58
+ const timer = setTimeout(() => setSlowWait(currentWait), slowHintAfterMs)
59
+ return () => clearTimeout(timer)
60
+ }, [currentWait, slowHintAfterMs])
61
+
62
+ const lifecycle = withSlowHint(unslowed, slowWait, currentWait)
63
+
64
+ const retry = useCallback(() => {
65
+ if (toolName === undefined) return
66
+ dispatch({ type: "start" })
67
+ void callTool(toolName, toolInput ?? {}).then(
68
+ (result) => {
69
+ const outcome = decodeViewResult(result, isValid)
70
+ if (outcome.ok) {
71
+ onResult(outcome.data)
72
+ dispatch({ type: "succeeded" })
73
+ } else {
74
+ dispatch({ type: "failed", reason: outcome.reason })
75
+ }
76
+ },
77
+ (err: unknown) => dispatch({ type: "failed", reason: errorMessage(err) }),
78
+ )
79
+ }, [callTool, isValid, onResult, toolInput, toolName])
80
+
81
+ return { lifecycle, retry }
82
+ }
83
+
84
+ /** The retry button of the view's error state (the focus target after a failed retry). */
85
+ const RETRY_BUTTON = '[data-slot="error-state"] button'
86
+
87
+ /**
88
+ * Keeps keyboard focus in the view across a user retry: the pressed button
89
+ * unmounts when the view starts waiting, so focus moves to the frame (a
90
+ * `tabIndex={-1}` container), then to the new retry button when the retry
91
+ * failed, or stays on the frame once the view rendered (see
92
+ * {@link retryFocusTarget}). Focus the user moved elsewhere meanwhile is left
93
+ * alone. Returns the frame ref and `retry` wrapped to start the hand-off.
94
+ */
95
+ export function useRetryFocus(
96
+ phase: ViewLifecycle["phase"],
97
+ retry: () => void,
98
+ ): { frameRef: RefObject<HTMLDivElement | null>; retry: () => void } {
99
+ const frameRef = useRef<HTMLDivElement | null>(null)
100
+ const owed = useRef(false)
101
+ const retryWithFocus = useCallback(() => {
102
+ owed.current = true
103
+ retry()
104
+ }, [retry])
105
+ useEffect(() => {
106
+ const frame = frameRef.current
107
+ if (!owed.current || !frame) return
108
+ const { target, settled } = retryFocusTarget(phase)
109
+ if (settled) owed.current = false
110
+ const active = frame.ownerDocument.activeElement
111
+ if (active && active !== frame.ownerDocument.body && !frame.contains(active)) return
112
+ const button = target === "retry" ? frame.querySelector<HTMLElement>(RETRY_BUTTON) : null
113
+ ;(button ?? frame).focus()
114
+ }, [phase])
115
+ return { frameRef, retry: retryWithFocus }
116
+ }
@@ -0,0 +1,227 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import {
3
+ decodeViewResult,
4
+ DEFAULT_SLOW_HINT_MS,
5
+ errorMessage,
6
+ resolveViewLifecycle,
7
+ RETRY_IDLE,
8
+ retryFocusTarget,
9
+ retryReducer,
10
+ waitKey,
11
+ withSlowHint,
12
+ type RetryState,
13
+ type ViewLifecycleInput,
14
+ } from "./view-lifecycle.js"
15
+
16
+ /** The baseline: a rendering call still open on a host that knows the tool. */
17
+ const waiting: ViewLifecycleInput = {
18
+ hasViewData: false,
19
+ callStatus: "pending",
20
+ cancelled: false,
21
+ callErrorReason: undefined,
22
+ recoveryFailed: false,
23
+ retryState: RETRY_IDLE,
24
+ slow: false,
25
+ canRetry: true,
26
+ }
27
+
28
+ const at = (over: Partial<ViewLifecycleInput>) => resolveViewLifecycle({ ...waiting, ...over })
29
+
30
+ describe("resolveViewLifecycle (toolkit#176 state machine)", () => {
31
+ it("waits while the rendering call is open, slow once the hint delay passed", () => {
32
+ expect(at({})).toEqual({ phase: "waiting", slow: false })
33
+ expect(at({ slow: true })).toEqual({ phase: "waiting", slow: true })
34
+ // A latched-ready call without a complete payload (recovery in flight) still waits.
35
+ expect(at({ callStatus: "ready" })).toEqual({ phase: "waiting", slow: false })
36
+ })
37
+
38
+ it("an isError result is an error with its reason — never an endless wait (K14)", () => {
39
+ expect(at({ callStatus: "error", callErrorReason: "[503] engine down" })).toEqual({
40
+ phase: "error",
41
+ reason: "[503] engine down",
42
+ canRetry: true,
43
+ })
44
+ })
45
+
46
+ it("a failed stripped-result recovery is an error too (N22: a terminal state)", () => {
47
+ expect(at({ recoveryFailed: true })).toEqual({
48
+ phase: "error",
49
+ reason: undefined,
50
+ canRetry: true,
51
+ })
52
+ })
53
+
54
+ it("offers a retry only when the originating tool is known", () => {
55
+ expect(at({ callStatus: "error", canRetry: false })).toMatchObject({
56
+ phase: "error",
57
+ canRetry: false,
58
+ })
59
+ })
60
+
61
+ it("a latched non-view result nothing can recover is an error, not an endless wait", () => {
62
+ // No toolInfo: the recovery cannot re-run the tool, so the wait would never end.
63
+ expect(at({ callStatus: "ready", canRetry: false })).toEqual({
64
+ phase: "error",
65
+ reason: undefined,
66
+ canRetry: false,
67
+ })
68
+ // Still pending on such a host: the result may yet arrive.
69
+ expect(at({ callStatus: "pending", canRetry: false })).toEqual({
70
+ phase: "waiting",
71
+ slow: false,
72
+ })
73
+ // A running retry or a payload in hand still win.
74
+ expect(
75
+ at({ callStatus: "ready", canRetry: false, retryState: { status: "running", attempt: 1 } }),
76
+ ).toEqual({ phase: "waiting", slow: false })
77
+ expect(at({ callStatus: "ready", canRetry: false, hasViewData: true })).toEqual({
78
+ phase: "result",
79
+ })
80
+ })
81
+
82
+ it("a host cancellation is terminal and wins over the error status it latches as", () => {
83
+ expect(at({ cancelled: true, callStatus: "error", callErrorReason: "cancelled" })).toEqual({
84
+ phase: "cancelled",
85
+ })
86
+ })
87
+
88
+ it("a running retry waits (with its own slow flag), a failed retry shows ITS reason", () => {
89
+ const running: RetryState = { status: "running", attempt: 1 }
90
+ expect(at({ callStatus: "error", retryState: running })).toEqual({
91
+ phase: "waiting",
92
+ slow: false,
93
+ })
94
+ expect(at({ callStatus: "error", retryState: running, slow: true })).toEqual({
95
+ phase: "waiting",
96
+ slow: true,
97
+ })
98
+ const failed: RetryState = { status: "failed", attempt: 1, reason: "still down" }
99
+ expect(at({ callStatus: "error", callErrorReason: "first", retryState: failed })).toEqual({
100
+ phase: "error",
101
+ reason: "still down",
102
+ canRetry: true,
103
+ })
104
+ expect(at({ cancelled: true, retryState: failed, canRetry: false })).toEqual({
105
+ phase: "error",
106
+ reason: "still down",
107
+ canRetry: false,
108
+ })
109
+ })
110
+
111
+ it("a payload in hand always wins — the view renders", () => {
112
+ expect(at({ hasViewData: true, callStatus: "error", recoveryFailed: true })).toEqual({
113
+ phase: "result",
114
+ })
115
+ expect(at({ hasViewData: true, retryState: { status: "running", attempt: 2 } })).toEqual({
116
+ phase: "result",
117
+ })
118
+ expect(at({ hasViewData: true, cancelled: true })).toEqual({ phase: "result" })
119
+ })
120
+ })
121
+
122
+ describe("retryReducer", () => {
123
+ it("counts attempts: start runs the next one, failure keeps the count and the reason", () => {
124
+ const first = retryReducer(RETRY_IDLE, { type: "start" })
125
+ expect(first).toEqual({ status: "running", attempt: 1 })
126
+ const failed = retryReducer(first, { type: "failed", reason: "503" })
127
+ expect(failed).toEqual({ status: "failed", attempt: 1, reason: "503" })
128
+ const second = retryReducer(failed, { type: "start" })
129
+ expect(second).toEqual({ status: "running", attempt: 2 })
130
+ expect(retryReducer(second, { type: "succeeded" })).toEqual({ status: "idle", attempt: 2 })
131
+ expect(retryReducer(second, { type: "failed", reason: undefined })).toEqual({
132
+ status: "failed",
133
+ attempt: 2,
134
+ reason: undefined,
135
+ })
136
+ })
137
+ })
138
+
139
+ describe("waitKey", () => {
140
+ it("names each wait so the slow hint restarts per attempt and stops when not waiting", () => {
141
+ expect(waitKey({ phase: "waiting", slow: false }, RETRY_IDLE)).toBe("idle:0")
142
+ expect(waitKey({ phase: "waiting", slow: false }, { status: "running", attempt: 2 })).toBe(
143
+ "running:2",
144
+ )
145
+ expect(waitKey({ phase: "error", reason: undefined, canRetry: true }, RETRY_IDLE)).toBeNull()
146
+ expect(waitKey({ phase: "result" }, RETRY_IDLE)).toBeNull()
147
+ expect(waitKey({ phase: "cancelled" }, RETRY_IDLE)).toBeNull()
148
+ })
149
+
150
+ it("waits 8 s before the slow hint by default", () => {
151
+ expect(DEFAULT_SLOW_HINT_MS).toBe(8000)
152
+ })
153
+ })
154
+
155
+ describe("withSlowHint", () => {
156
+ const waiting = { phase: "waiting", slow: false } as const
157
+
158
+ it("turns the current wait slow once ITS delay passed", () => {
159
+ expect(withSlowHint(waiting, "idle:0", "idle:0")).toEqual({ phase: "waiting", slow: true })
160
+ })
161
+
162
+ it("keeps a new attempt and an unslowed wait without the hint", () => {
163
+ expect(withSlowHint(waiting, "idle:0", "running:1")).toBe(waiting)
164
+ expect(withSlowHint(waiting, null, "idle:0")).toBe(waiting)
165
+ expect(withSlowHint(waiting, null, null)).toBe(waiting)
166
+ })
167
+
168
+ it("never touches a phase that is not waiting", () => {
169
+ const error = { phase: "error", reason: "x", canRetry: true } as const
170
+ expect(withSlowHint(error, "idle:0", "idle:0")).toBe(error)
171
+ expect(withSlowHint({ phase: "result" }, "idle:0", "idle:0")).toEqual({ phase: "result" })
172
+ })
173
+ })
174
+
175
+ describe("retryFocusTarget", () => {
176
+ it("holds focus on the frame while the retry runs, then hands it on and settles", () => {
177
+ expect(retryFocusTarget("waiting")).toEqual({ target: "frame", settled: false })
178
+ expect(retryFocusTarget("error")).toEqual({ target: "retry", settled: true })
179
+ expect(retryFocusTarget("result")).toEqual({ target: "frame", settled: true })
180
+ expect(retryFocusTarget("cancelled")).toEqual({ target: "frame", settled: true })
181
+ })
182
+ })
183
+
184
+ describe("decodeViewResult", () => {
185
+ const isView = (v: unknown): v is { layout: unknown } =>
186
+ typeof v === "object" && v !== null && "layout" in v
187
+
188
+ it("returns the payload of a complete view", () => {
189
+ expect(decodeViewResult({ structuredContent: { layout: { rows: [] } } }, isView)).toEqual({
190
+ ok: true,
191
+ data: { layout: { rows: [] } },
192
+ })
193
+ })
194
+
195
+ it("types a tool error as such, with the isError text as the raw reason", () => {
196
+ expect(decodeViewResult({ isError: true, content: [{ text: "[503] down" }] }, isView)).toEqual({
197
+ ok: false,
198
+ isError: true,
199
+ reason: "[503] down",
200
+ })
201
+ // An isError result without text is still a tool error.
202
+ expect(decodeViewResult({ isError: true, content: [{ text: "" }] }, isView)).toEqual({
203
+ ok: false,
204
+ isError: true,
205
+ reason: undefined,
206
+ })
207
+ })
208
+
209
+ it("types a payload the shell cannot render as no view, without a reason", () => {
210
+ expect(decodeViewResult({ structuredContent: { other: 1 } }, isView)).toEqual({
211
+ ok: false,
212
+ isError: false,
213
+ reason: undefined,
214
+ })
215
+ })
216
+ })
217
+
218
+ describe("errorMessage", () => {
219
+ it("reads Error messages and strings, nothing else", () => {
220
+ expect(errorMessage(new Error("boom"))).toBe("boom")
221
+ expect(errorMessage(new Error(""))).toBeUndefined()
222
+ expect(errorMessage("plain")).toBe("plain")
223
+ expect(errorMessage("")).toBeUndefined()
224
+ expect(errorMessage({ message: "not an error" })).toBeUndefined()
225
+ expect(errorMessage(undefined)).toBeUndefined()
226
+ })
227
+ })
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The `McpAppView` lifecycle as an explicit state machine (toolkit#176).
3
+ *
4
+ * Before the view payload exists the shell is in exactly one phase:
5
+ *
6
+ * waiting ──result──▶ result
7
+ * │ ╲
8
+ * │ ╲──tool error / failed recovery / unrecoverable result──▶ error ──retry──▶ waiting
9
+ * │
10
+ * └──host cancelled──▶ cancelled (terminal: the user stopped it)
11
+ *
12
+ * `waiting` carries a `slow` flag once loading outlasts the slow-hint delay.
13
+ * An `isError` result (or a failed call) is an `error`, never an endless
14
+ * skeleton, and a retry re-runs the originating tool once on the user's click.
15
+ * A latched result that is no view on a host that names no originating tool
16
+ * is an `error` too: nothing could recover it, so waiting would never end.
17
+ * Pure functions only: the hook in `use-view-lifecycle.ts` feeds them.
18
+ */
19
+ import { parseToolResult } from "../lib/parse-tool-result.js"
20
+
21
+ /** `useToolContext().status` of the rendering call. */
22
+ export type RenderCallStatus = "pending" | "ready" | "error"
23
+
24
+ /** The user-triggered retry of the rendering call. */
25
+ export type RetryState =
26
+ | { status: "idle"; attempt: number }
27
+ | { status: "running"; attempt: number }
28
+ | { status: "failed"; attempt: number; reason: string | undefined }
29
+
30
+ export const RETRY_IDLE: RetryState = { status: "idle", attempt: 0 }
31
+
32
+ export type RetryAction =
33
+ { type: "start" } | { type: "failed"; reason: string | undefined } | { type: "succeeded" }
34
+
35
+ /** start → running (next attempt); failed → failed with reason; succeeded → idle. */
36
+ export function retryReducer(state: RetryState, action: RetryAction): RetryState {
37
+ switch (action.type) {
38
+ case "start":
39
+ return { status: "running", attempt: state.attempt + 1 }
40
+ case "failed":
41
+ return { status: "failed", attempt: state.attempt, reason: action.reason }
42
+ case "succeeded":
43
+ return { status: "idle", attempt: state.attempt }
44
+ }
45
+ }
46
+
47
+ export interface ViewLifecycleInput {
48
+ /** A renderable payload is in hand (host-delivered, recovered or retried). */
49
+ hasViewData: boolean
50
+ /** Status of the original rendering call. */
51
+ callStatus: RenderCallStatus
52
+ /** The host cancelled the original rendering call. */
53
+ cancelled: boolean
54
+ /** Message of the original call's error, when it failed. */
55
+ callErrorReason: string | undefined
56
+ /** The automatic stripped-result recovery ran and failed. */
57
+ recoveryFailed: boolean
58
+ /** The user-triggered retry. */
59
+ retryState: RetryState
60
+ /** Loading has outlasted the slow-hint delay. */
61
+ slow: boolean
62
+ /** The originating tool is known, so a retry can re-run it. */
63
+ canRetry: boolean
64
+ }
65
+
66
+ export type ViewLifecycle =
67
+ | { phase: "result" }
68
+ | { phase: "waiting"; slow: boolean }
69
+ | { phase: "cancelled" }
70
+ | { phase: "error"; reason: string | undefined; canRetry: boolean }
71
+
72
+ /**
73
+ * Resolve the single phase the view is in. Precedence: a payload always wins;
74
+ * a running retry is waiting; a failed retry is an error with ITS reason; a
75
+ * cancellation is terminal; a failed call, a failed recovery or a latched
76
+ * non-view result nothing can recover is an error; everything else is still
77
+ * waiting.
78
+ */
79
+ export function resolveViewLifecycle(input: ViewLifecycleInput): ViewLifecycle {
80
+ if (input.hasViewData) return { phase: "result" }
81
+ const { retryState } = input
82
+ if (retryState.status === "running") return { phase: "waiting", slow: input.slow }
83
+ if (retryState.status === "failed") {
84
+ return { phase: "error", reason: retryState.reason, canRetry: input.canRetry }
85
+ }
86
+ if (input.cancelled) return { phase: "cancelled" }
87
+ if (input.callStatus === "error") {
88
+ return { phase: "error", reason: input.callErrorReason, canRetry: input.canRetry }
89
+ }
90
+ if (input.recoveryFailed) return { phase: "error", reason: undefined, canRetry: input.canRetry }
91
+ // A latched result without a renderable view: the recovery re-runs the
92
+ // originating tool, so it can only help when the host names that tool.
93
+ if (input.callStatus === "ready" && !input.canRetry) {
94
+ return { phase: "error", reason: undefined, canRetry: false }
95
+ }
96
+ return { phase: "waiting", slow: input.slow }
97
+ }
98
+
99
+ /**
100
+ * The phase with its slow flag: a wait turns slow only while the wait that
101
+ * outlasted the delay (`slowWait`, a {@link waitKey}) is still the current one,
102
+ * so a new attempt starts without the hint.
103
+ */
104
+ export function withSlowHint(
105
+ lifecycle: ViewLifecycle,
106
+ slowWait: string | null,
107
+ currentWait: string | null,
108
+ ): ViewLifecycle {
109
+ return lifecycle.phase === "waiting" && slowWait !== null && slowWait === currentWait
110
+ ? { phase: "waiting", slow: true }
111
+ : lifecycle
112
+ }
113
+
114
+ /**
115
+ * Where keyboard focus goes after the user pressed retry (the button unmounts
116
+ * as the view starts waiting): the view frame while waiting, the new retry
117
+ * button when the retry failed, the frame again once the view or a
118
+ * cancellation replaced the state. `settled` ends the hand-off.
119
+ */
120
+ export function retryFocusTarget(phase: ViewLifecycle["phase"]): {
121
+ target: "frame" | "retry"
122
+ settled: boolean
123
+ } {
124
+ if (phase === "waiting") return { target: "frame", settled: false }
125
+ if (phase === "error") return { target: "retry", settled: true }
126
+ return { target: "frame", settled: true }
127
+ }
128
+
129
+ /**
130
+ * Identity of the current wait, so the slow-hint timer restarts for every new
131
+ * wait (the first load, then each retry attempt) and never for a finished one.
132
+ * `null` when the view is not waiting.
133
+ */
134
+ export function waitKey(lifecycle: ViewLifecycle, retryState: RetryState): string | null {
135
+ return lifecycle.phase === "waiting" ? `${retryState.status}:${retryState.attempt}` : null
136
+ }
137
+
138
+ /** Default delay before the waiting placeholder adds its slow hint. */
139
+ export const DEFAULT_SLOW_HINT_MS = 8000
140
+
141
+ /**
142
+ * A decoded view call (a retry or a refresh), typed so a failure never turns
143
+ * into UI text: `isError` tells a tool error (with its raw `reason`, for the
144
+ * Details disclosure or the console) from a payload that is no view.
145
+ */
146
+ export type ViewResultOutcome<T> =
147
+ { ok: true; data: T } | { ok: false; isError: boolean; reason: string | undefined }
148
+
149
+ /**
150
+ * Decode a view call's result: the payload when it is a complete view,
151
+ * otherwise a failure — a tool error with its `isError` text, or a payload
152
+ * the shell cannot render (no reason).
153
+ */
154
+ export function decodeViewResult<T>(
155
+ result: unknown,
156
+ isValid: (value: unknown) => value is T,
157
+ ): ViewResultOutcome<T> {
158
+ let decoded: unknown
159
+ try {
160
+ decoded = parseToolResult<unknown>(result)
161
+ } catch (err) {
162
+ return { ok: false, isError: true, reason: errorMessage(err) }
163
+ }
164
+ return isValid(decoded)
165
+ ? { ok: true, data: decoded }
166
+ : { ok: false, isError: false, reason: undefined }
167
+ }
168
+
169
+ /** A displayable message for anything thrown. */
170
+ export function errorMessage(err: unknown): string | undefined {
171
+ if (err instanceof Error) return err.message || undefined
172
+ if (typeof err === "string") return err || undefined
173
+ return undefined
174
+ }
@@ -16,6 +16,8 @@ import {
16
16
  SheetTitle,
17
17
  } from "../primitives/sheet.js"
18
18
  import { Switch } from "../primitives/switch.js"
19
+ import { useToolkitMessages } from "../locale/locale-provider.js"
20
+ import type { PropsSheetMessages } from "../locale/toolkit-messages.js"
19
21
 
20
22
  /**
21
23
  * Tiny JSON-Schema-driven editor for a layout cell's `props`. The
@@ -48,25 +50,8 @@ interface JsonSchemaObject {
48
50
  required?: string[]
49
51
  }
50
52
 
51
- export interface WidgetPropsSheetLabels {
52
- title?: string
53
- description?: string
54
- apply?: string
55
- cancel?: string
56
- reset?: string
57
- noProps?: string
58
- unsetOption?: string
59
- }
60
-
61
- const DEFAULT_LABELS: Required<WidgetPropsSheetLabels> = {
62
- title: "Configure widget",
63
- description: "Set per-instance props for this widget.",
64
- apply: "Apply",
65
- cancel: "Cancel",
66
- reset: "Reset",
67
- noProps: "This widget takes no per-instance props.",
68
- unsetOption: "(unset)",
69
- }
53
+ /** Override strings of the sheet; each defaults to the locale's `propsSheet` catalog. */
54
+ export type WidgetPropsSheetLabels = Partial<PropsSheetMessages>
70
55
 
71
56
  const UNSET_SENTINEL = "__unset__"
72
57
 
@@ -89,7 +74,8 @@ export function WidgetPropsSheet({
89
74
  onApply,
90
75
  labels,
91
76
  }: WidgetPropsSheetProps) {
92
- const L = { ...DEFAULT_LABELS, ...labels }
77
+ const messages = useToolkitMessages()
78
+ const L = { ...messages.propsSheet, ...labels }
93
79
  const obj = (schema ?? {}) as JsonSchemaObject
94
80
  const [draft, setDraft] = useState<Record<string, unknown>>(value ?? {})
95
81
 
@@ -121,9 +107,9 @@ export function WidgetPropsSheet({
121
107
  <SheetContent className="w-[400px] sm:max-w-md">
122
108
  <SheetHeader>
123
109
  <SheetTitle>{L.title}</SheetTitle>
124
- <SheetDescription>
125
- <span className="bg-muted rounded px-1 font-mono text-xs">{widgetId}</span> —{" "}
126
- {L.description}
110
+ <SheetDescription className="flex flex-col items-start gap-1">
111
+ <span>{L.description}</span>
112
+ <span className="bg-muted rounded px-1 font-mono text-xs">{widgetId}</span>
127
113
  </SheetDescription>
128
114
  </SheetHeader>
129
115
  <div className="flex flex-col gap-4 px-4 py-4">
@@ -137,6 +123,7 @@ export function WidgetPropsSheet({
137
123
  prop={prop}
138
124
  value={draft[key]}
139
125
  unsetLabel={L.unsetOption}
126
+ unsupportedLabel={L.unsupportedType}
140
127
  onChange={(v) =>
141
128
  setDraft((prev) => {
142
129
  if (v === undefined) {
@@ -172,12 +159,14 @@ function PropertyField({
172
159
  prop,
173
160
  value,
174
161
  unsetLabel,
162
+ unsupportedLabel,
175
163
  onChange,
176
164
  }: {
177
165
  name: string
178
166
  prop: JsonSchemaProperty
179
167
  value: unknown
180
168
  unsetLabel: string
169
+ unsupportedLabel: (type: string) => string
181
170
  onChange: (v: unknown) => void
182
171
  }) {
183
172
  const id = `prop-${name}`
@@ -255,9 +244,7 @@ function PropertyField({
255
244
 
256
245
  return (
257
246
  <FieldShell id={id} name={name} description={prop.description}>
258
- <p className="text-muted-foreground text-xs italic">
259
- Unsupported type "{prop.type ?? "(none)"}" — edit raw JSON to set this field.
260
- </p>
247
+ <p className="text-muted-foreground text-xs italic">{unsupportedLabel(prop.type ?? "?")}</p>
261
248
  </FieldShell>
262
249
  )
263
250
  }