@miragon/mcp-toolkit-ui 2.7.0 → 2.8.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 (387) hide show
  1. package/README.md +7 -4
  2. package/dist/app/adapt-data-widget.d.ts +1 -22
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -1
  4. package/dist/app/adapt-data-widget.js +1 -35
  5. package/dist/app/adapt-data-widget.js.map +1 -1
  6. package/dist/app/builder/BuilderBanners.js +1 -1
  7. package/dist/app/builder/BuilderBanners.js.map +1 -1
  8. package/dist/app/builder/CanvasRow.d.ts +4 -2
  9. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  10. package/dist/app/builder/CanvasRow.js +21 -7
  11. package/dist/app/builder/CanvasRow.js.map +1 -1
  12. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  13. package/dist/app/builder/CatalogueSheet.js +11 -4
  14. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  15. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  16. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.js +10 -3
  18. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  19. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  20. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  21. package/dist/app/builder/DiscardDialog.js +18 -0
  22. package/dist/app/builder/DiscardDialog.js.map +1 -0
  23. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  24. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  25. package/dist/app/builder/LayoutSurface.js +74 -23
  26. package/dist/app/builder/LayoutSurface.js.map +1 -1
  27. package/dist/app/builder/PipelineStrip.d.ts +2 -1
  28. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  29. package/dist/app/builder/PipelineStrip.js +26 -13
  30. package/dist/app/builder/PipelineStrip.js.map +1 -1
  31. package/dist/app/builder/SaveDialog.d.ts +9 -1
  32. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  33. package/dist/app/builder/SaveDialog.js +8 -2
  34. package/dist/app/builder/SaveDialog.js.map +1 -1
  35. package/dist/app/builder/Toolbar.d.ts +12 -4
  36. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  37. package/dist/app/builder/Toolbar.js +18 -7
  38. package/dist/app/builder/Toolbar.js.map +1 -1
  39. package/dist/app/builder/Workspace.d.ts +23 -3
  40. package/dist/app/builder/Workspace.d.ts.map +1 -1
  41. package/dist/app/builder/Workspace.js +58 -8
  42. package/dist/app/builder/Workspace.js.map +1 -1
  43. package/dist/app/builder/builder-model.d.ts +143 -1
  44. package/dist/app/builder/builder-model.d.ts.map +1 -1
  45. package/dist/app/builder/builder-model.js +191 -1
  46. package/dist/app/builder/builder-model.js.map +1 -1
  47. package/dist/app/builder/reveal.d.ts +10 -0
  48. package/dist/app/builder/reveal.d.ts.map +1 -0
  49. package/dist/app/builder/reveal.js +10 -0
  50. package/dist/app/builder/reveal.js.map +1 -0
  51. package/dist/app/builder/use-builder-catalogue.d.ts +4 -1
  52. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  53. package/dist/app/builder/use-builder-catalogue.js +44 -17
  54. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  55. package/dist/app/builder/use-builder-session.d.ts +30 -0
  56. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  57. package/dist/app/builder/use-builder-session.js +58 -0
  58. package/dist/app/builder/use-builder-session.js.map +1 -0
  59. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  60. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  61. package/dist/app/builder/use-display-resolver.js +16 -0
  62. package/dist/app/builder/use-display-resolver.js.map +1 -0
  63. package/dist/app/index.d.ts +8 -3
  64. package/dist/app/index.d.ts.map +1 -1
  65. package/dist/app/index.js +8 -2
  66. package/dist/app/index.js.map +1 -1
  67. package/dist/app/layout-builder.d.ts +13 -25
  68. package/dist/app/layout-builder.d.ts.map +1 -1
  69. package/dist/app/layout-builder.js +75 -81
  70. package/dist/app/layout-builder.js.map +1 -1
  71. package/dist/app/mcp-app-view-chrome.d.ts +12 -3
  72. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  73. package/dist/app/mcp-app-view-chrome.js +21 -5
  74. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  75. package/dist/app/mcp-app-view-states.d.ts +11 -3
  76. package/dist/app/mcp-app-view-states.d.ts.map +1 -1
  77. package/dist/app/mcp-app-view-states.js +29 -8
  78. package/dist/app/mcp-app-view-states.js.map +1 -1
  79. package/dist/app/mcp-app-view.d.ts +33 -1
  80. package/dist/app/mcp-app-view.d.ts.map +1 -1
  81. package/dist/app/mcp-app-view.js +181 -127
  82. package/dist/app/mcp-app-view.js.map +1 -1
  83. package/dist/app/mcp-toolkit-app.d.ts +6 -1
  84. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  85. package/dist/app/mcp-toolkit-app.js +8 -1
  86. package/dist/app/mcp-toolkit-app.js.map +1 -1
  87. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  88. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  89. package/dist/app/mcp-use-host-bridge.js +51 -22
  90. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  91. package/dist/app/model-context.d.ts +71 -0
  92. package/dist/app/model-context.d.ts.map +1 -0
  93. package/dist/app/model-context.js +101 -0
  94. package/dist/app/model-context.js.map +1 -0
  95. package/dist/app/use-build-mode-focus.d.ts +33 -0
  96. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  97. package/dist/app/use-build-mode-focus.js +59 -0
  98. package/dist/app/use-build-mode-focus.js.map +1 -0
  99. package/dist/app/use-view-actions.d.ts +31 -0
  100. package/dist/app/use-view-actions.d.ts.map +1 -0
  101. package/dist/app/use-view-actions.js +81 -0
  102. package/dist/app/use-view-actions.js.map +1 -0
  103. package/dist/app/use-view-lifecycle.d.ts +4 -0
  104. package/dist/app/use-view-lifecycle.d.ts.map +1 -1
  105. package/dist/app/use-view-lifecycle.js +2 -0
  106. package/dist/app/use-view-lifecycle.js.map +1 -1
  107. package/dist/app/view-lifecycle.d.ts +80 -4
  108. package/dist/app/view-lifecycle.d.ts.map +1 -1
  109. package/dist/app/view-lifecycle.js +90 -3
  110. package/dist/app/view-lifecycle.js.map +1 -1
  111. package/dist/app/widget-fixture.d.ts +10 -3
  112. package/dist/app/widget-fixture.d.ts.map +1 -1
  113. package/dist/app/widget-fixture.js +21 -8
  114. package/dist/app/widget-fixture.js.map +1 -1
  115. package/dist/app/widget-props-sheet.d.ts +3 -1
  116. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  117. package/dist/app/widget-props-sheet.js +22 -10
  118. package/dist/app/widget-props-sheet.js.map +1 -1
  119. package/dist/app/widget-renderer.js +1 -1
  120. package/dist/app/widget-renderer.js.map +1 -1
  121. package/dist/components/DrillButton.d.ts +9 -6
  122. package/dist/components/DrillButton.d.ts.map +1 -1
  123. package/dist/components/DrillButton.js +8 -6
  124. package/dist/components/DrillButton.js.map +1 -1
  125. package/dist/components/EmptyState.d.ts.map +1 -1
  126. package/dist/components/EmptyState.js +4 -2
  127. package/dist/components/EmptyState.js.map +1 -1
  128. package/dist/components/ErrorState.d.ts +5 -2
  129. package/dist/components/ErrorState.d.ts.map +1 -1
  130. package/dist/components/ErrorState.js +25 -8
  131. package/dist/components/ErrorState.js.map +1 -1
  132. package/dist/components/FilterBar.d.ts +15 -4
  133. package/dist/components/FilterBar.d.ts.map +1 -1
  134. package/dist/components/FilterBar.js +11 -6
  135. package/dist/components/FilterBar.js.map +1 -1
  136. package/dist/components/KpiGrid.d.ts +66 -21
  137. package/dist/components/KpiGrid.d.ts.map +1 -1
  138. package/dist/components/KpiGrid.js +101 -34
  139. package/dist/components/KpiGrid.js.map +1 -1
  140. package/dist/components/ListFooter.d.ts +31 -4
  141. package/dist/components/ListFooter.d.ts.map +1 -1
  142. package/dist/components/ListFooter.js +30 -11
  143. package/dist/components/ListFooter.js.map +1 -1
  144. package/dist/components/PagedListFooter.d.ts +57 -0
  145. package/dist/components/PagedListFooter.d.ts.map +1 -0
  146. package/dist/components/PagedListFooter.js +27 -0
  147. package/dist/components/PagedListFooter.js.map +1 -0
  148. package/dist/components/Pills.d.ts +27 -7
  149. package/dist/components/Pills.d.ts.map +1 -1
  150. package/dist/components/Pills.js +21 -6
  151. package/dist/components/Pills.js.map +1 -1
  152. package/dist/components/SectionHeading.d.ts +3 -1
  153. package/dist/components/SectionHeading.d.ts.map +1 -1
  154. package/dist/components/SectionHeading.js +6 -2
  155. package/dist/components/SectionHeading.js.map +1 -1
  156. package/dist/components/Toast.d.ts +9 -3
  157. package/dist/components/Toast.d.ts.map +1 -1
  158. package/dist/components/Toast.js +31 -16
  159. package/dist/components/Toast.js.map +1 -1
  160. package/dist/components/WidgetHeader.d.ts +34 -10
  161. package/dist/components/WidgetHeader.d.ts.map +1 -1
  162. package/dist/components/WidgetHeader.js +11 -6
  163. package/dist/components/WidgetHeader.js.map +1 -1
  164. package/dist/hooks/index.d.ts +4 -4
  165. package/dist/hooks/index.d.ts.map +1 -1
  166. package/dist/hooks/index.js +1 -1
  167. package/dist/hooks/index.js.map +1 -1
  168. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  169. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  170. package/dist/hooks/tool-result-recovery-model.js +273 -0
  171. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  172. package/dist/hooks/use-tool-query.d.ts +56 -14
  173. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  174. package/dist/hooks/use-tool-query.js +102 -25
  175. package/dist/hooks/use-tool-query.js.map +1 -1
  176. package/dist/hooks/use-tool-result-recovery.d.ts +47 -52
  177. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  178. package/dist/hooks/use-tool-result-recovery.js +58 -120
  179. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  180. package/dist/hooks/use-view-data.d.ts +52 -10
  181. package/dist/hooks/use-view-data.d.ts.map +1 -1
  182. package/dist/hooks/use-view-data.js +81 -16
  183. package/dist/hooks/use-view-data.js.map +1 -1
  184. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  185. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  186. package/dist/hooks/use-view-tool-query.js +15 -14
  187. package/dist/hooks/use-view-tool-query.js.map +1 -1
  188. package/dist/index.d.ts +17 -10
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +18 -6
  191. package/dist/index.js.map +1 -1
  192. package/dist/lib/focus-return.d.ts +61 -0
  193. package/dist/lib/focus-return.d.ts.map +1 -0
  194. package/dist/lib/focus-return.js +46 -0
  195. package/dist/lib/focus-return.js.map +1 -0
  196. package/dist/lib/parse-tool-result.d.ts +23 -1
  197. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  198. package/dist/lib/parse-tool-result.js +38 -2
  199. package/dist/lib/parse-tool-result.js.map +1 -1
  200. package/dist/lib/toast-model.d.ts +41 -1
  201. package/dist/lib/toast-model.d.ts.map +1 -1
  202. package/dist/lib/toast-model.js +67 -1
  203. package/dist/lib/toast-model.js.map +1 -1
  204. package/dist/lib/tone-utils.d.ts +58 -6
  205. package/dist/lib/tone-utils.d.ts.map +1 -1
  206. package/dist/lib/tone-utils.js +66 -4
  207. package/dist/lib/tone-utils.js.map +1 -1
  208. package/dist/lib/tool-query.d.ts +107 -0
  209. package/dist/lib/tool-query.d.ts.map +1 -0
  210. package/dist/lib/tool-query.js +182 -0
  211. package/dist/lib/tool-query.js.map +1 -0
  212. package/dist/lib/tool-result-observer.d.ts +108 -0
  213. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  214. package/dist/lib/tool-result-observer.js +158 -0
  215. package/dist/lib/tool-result-observer.js.map +1 -0
  216. package/dist/lib/typography.d.ts +37 -0
  217. package/dist/lib/typography.d.ts.map +1 -0
  218. package/dist/lib/typography.js +41 -0
  219. package/dist/lib/typography.js.map +1 -0
  220. package/dist/lib/utils.d.ts.map +1 -1
  221. package/dist/lib/utils.js +11 -1
  222. package/dist/lib/utils.js.map +1 -1
  223. package/dist/locale/index.d.ts +1 -1
  224. package/dist/locale/index.d.ts.map +1 -1
  225. package/dist/locale/message-types.d.ts +117 -4
  226. package/dist/locale/message-types.d.ts.map +1 -1
  227. package/dist/locale/messages-de.d.ts.map +1 -1
  228. package/dist/locale/messages-de.js +44 -10
  229. package/dist/locale/messages-de.js.map +1 -1
  230. package/dist/locale/messages-en.d.ts.map +1 -1
  231. package/dist/locale/messages-en.js +44 -10
  232. package/dist/locale/messages-en.js.map +1 -1
  233. package/dist/locale/toolkit-messages.d.ts +19 -1
  234. package/dist/locale/toolkit-messages.d.ts.map +1 -1
  235. package/dist/locale/toolkit-messages.js +54 -1
  236. package/dist/locale/toolkit-messages.js.map +1 -1
  237. package/dist/primitives/alert.d.ts +1 -1
  238. package/dist/primitives/alert.d.ts.map +1 -1
  239. package/dist/primitives/alert.js +2 -0
  240. package/dist/primitives/alert.js.map +1 -1
  241. package/dist/primitives/badge.d.ts +1 -1
  242. package/dist/primitives/badge.d.ts.map +1 -1
  243. package/dist/primitives/badge.js +9 -4
  244. package/dist/primitives/badge.js.map +1 -1
  245. package/dist/primitives/button.d.ts +1 -1
  246. package/dist/primitives/button.js +1 -1
  247. package/dist/primitives/button.js.map +1 -1
  248. package/dist/primitives/dialog.d.ts +1 -1
  249. package/dist/primitives/dialog.d.ts.map +1 -1
  250. package/dist/primitives/dialog.js +10 -3
  251. package/dist/primitives/dialog.js.map +1 -1
  252. package/dist/primitives/select.js +1 -1
  253. package/dist/primitives/select.js.map +1 -1
  254. package/dist/primitives/sheet.d.ts +1 -1
  255. package/dist/primitives/sheet.d.ts.map +1 -1
  256. package/dist/primitives/sheet.js +6 -2
  257. package/dist/primitives/sheet.js.map +1 -1
  258. package/dist/providers/query-provider.d.ts +32 -1
  259. package/dist/providers/query-provider.d.ts.map +1 -1
  260. package/dist/providers/query-provider.js +53 -4
  261. package/dist/providers/query-provider.js.map +1 -1
  262. package/dist/render-cases-primitives.js +1 -1
  263. package/dist/render-cases-primitives.js.map +1 -1
  264. package/dist/render-cases.d.ts.map +1 -1
  265. package/dist/render-cases.js +30 -6
  266. package/dist/render-cases.js.map +1 -1
  267. package/dist/render-effects.d.ts +42 -0
  268. package/dist/render-effects.d.ts.map +1 -0
  269. package/dist/render-effects.js +81 -0
  270. package/dist/render-effects.js.map +1 -0
  271. package/dist/theme/create-theme.d.ts +19 -1
  272. package/dist/theme/create-theme.d.ts.map +1 -1
  273. package/dist/theme/create-theme.js +15 -0
  274. package/dist/theme/create-theme.js.map +1 -1
  275. package/package.json +5 -4
  276. package/src/app/adapt-data-widget.tsx +2 -42
  277. package/src/app/builder/BuilderBanners.tsx +1 -1
  278. package/src/app/builder/CanvasRow.tsx +63 -17
  279. package/src/app/builder/CatalogueSheet.tsx +50 -29
  280. package/src/app/builder/CellPropsSheet.tsx +17 -2
  281. package/src/app/builder/DiscardDialog.tsx +59 -0
  282. package/src/app/builder/LayoutSurface.tsx +206 -66
  283. package/src/app/builder/PipelineStrip.tsx +253 -153
  284. package/src/app/builder/SaveDialog.tsx +14 -2
  285. package/src/app/builder/Toolbar.tsx +144 -59
  286. package/src/app/builder/Workspace.tsx +226 -58
  287. package/src/app/builder/builder-model.ts +341 -2
  288. package/src/app/builder/builder-model.ux.test.ts +456 -0
  289. package/src/app/builder/builder-ux.test.tsx +844 -0
  290. package/src/app/builder/reveal.ts +10 -0
  291. package/src/app/builder/use-builder-catalogue.ts +56 -19
  292. package/src/app/builder/use-builder-session.ts +68 -0
  293. package/src/app/builder/use-display-resolver.ts +24 -0
  294. package/src/app/index.ts +12 -2
  295. package/src/app/layout-builder.tsx +178 -191
  296. package/src/app/mcp-app-view-chrome.test.tsx +141 -1
  297. package/src/app/mcp-app-view-chrome.tsx +56 -6
  298. package/src/app/mcp-app-view-states.tsx +51 -11
  299. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  300. package/src/app/mcp-app-view.test.tsx +222 -2
  301. package/src/app/mcp-app-view.tsx +267 -117
  302. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  303. package/src/app/mcp-toolkit-app.tsx +8 -1
  304. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  305. package/src/app/mcp-use-host-bridge.tsx +67 -24
  306. package/src/app/model-context.test.tsx +116 -0
  307. package/src/app/model-context.tsx +125 -0
  308. package/src/app/use-build-mode-focus.ts +62 -0
  309. package/src/app/use-view-actions.ts +89 -0
  310. package/src/app/use-view-lifecycle.ts +6 -0
  311. package/src/app/view-lifecycle.test.ts +216 -0
  312. package/src/app/view-lifecycle.ts +165 -5
  313. package/src/app/widget-fixture.effects.test.tsx +51 -0
  314. package/src/app/widget-fixture.render.test.tsx +81 -0
  315. package/src/app/widget-fixture.tsx +34 -11
  316. package/src/app/widget-props-sheet.tsx +57 -16
  317. package/src/app/widget-renderer.tsx +1 -1
  318. package/src/components/DrillButton.tsx +23 -10
  319. package/src/components/EmptyState.tsx +6 -4
  320. package/src/components/ErrorState.tsx +31 -9
  321. package/src/components/FilterBar.test.tsx +82 -0
  322. package/src/components/FilterBar.tsx +52 -26
  323. package/src/components/KpiGrid.test.tsx +279 -0
  324. package/src/components/KpiGrid.tsx +297 -90
  325. package/src/components/ListFooter.test.tsx +116 -0
  326. package/src/components/ListFooter.tsx +102 -15
  327. package/src/components/PagedListFooter.test.tsx +140 -0
  328. package/src/components/PagedListFooter.tsx +107 -0
  329. package/src/components/Pills.test.tsx +77 -0
  330. package/src/components/Pills.tsx +63 -10
  331. package/src/components/SectionHeading.tsx +9 -3
  332. package/src/components/Toast.tsx +37 -19
  333. package/src/components/WidgetHeader.test.tsx +78 -0
  334. package/src/components/WidgetHeader.tsx +63 -16
  335. package/src/components/feedback-states.test.tsx +98 -9
  336. package/src/components/tone-figures.test.tsx +8 -4
  337. package/src/hooks/index.ts +6 -3
  338. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  339. package/src/hooks/tool-result-recovery-model.ts +418 -0
  340. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  341. package/src/hooks/use-tool-query.ts +156 -33
  342. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  343. package/src/hooks/use-tool-result-recovery.test.ts +3 -4
  344. package/src/hooks/use-tool-result-recovery.ts +84 -142
  345. package/src/hooks/use-view-data.test.tsx +118 -9
  346. package/src/hooks/use-view-data.ts +128 -18
  347. package/src/hooks/use-view-tool-query.ts +25 -19
  348. package/src/i18n-coverage.test.tsx +104 -2
  349. package/src/index.ts +48 -11
  350. package/src/lib/focus-return.test.ts +288 -0
  351. package/src/lib/focus-return.ts +96 -0
  352. package/src/lib/parse-tool-result.test.ts +46 -1
  353. package/src/lib/parse-tool-result.ts +40 -2
  354. package/src/lib/toast-model.test.ts +166 -1
  355. package/src/lib/toast-model.ts +104 -2
  356. package/src/lib/tone-utils.test.ts +86 -10
  357. package/src/lib/tone-utils.ts +89 -8
  358. package/src/lib/tool-query.test.ts +569 -0
  359. package/src/lib/tool-query.ts +253 -0
  360. package/src/lib/tool-result-observer.test.ts +296 -0
  361. package/src/lib/tool-result-observer.ts +211 -0
  362. package/src/lib/typography.test.ts +45 -0
  363. package/src/lib/typography.ts +47 -0
  364. package/src/lib/utils.ts +12 -1
  365. package/src/locale/index.ts +1 -0
  366. package/src/locale/message-types.ts +118 -2
  367. package/src/locale/messages-de.ts +49 -10
  368. package/src/locale/messages-en.ts +49 -10
  369. package/src/locale/toolkit-messages.test.ts +133 -0
  370. package/src/locale/toolkit-messages.ts +63 -2
  371. package/src/primitives/alert.tsx +3 -0
  372. package/src/primitives/badge.tsx +9 -4
  373. package/src/primitives/button.tsx +1 -1
  374. package/src/primitives/dialog.tsx +10 -1
  375. package/src/primitives/select.tsx +1 -1
  376. package/src/primitives/sheet.tsx +7 -0
  377. package/src/providers/query-provider.test.tsx +154 -0
  378. package/src/providers/query-provider.tsx +66 -5
  379. package/src/render-cases-primitives.tsx +1 -1
  380. package/src/render-cases.tsx +38 -6
  381. package/src/render-effects.tsx +96 -0
  382. package/src/theme/cd-tokens.test.ts +3 -1
  383. package/src/theme/create-theme.test.ts +10 -0
  384. package/src/theme/create-theme.ts +28 -0
  385. package/src/theme/theme-css.test.ts +51 -0
  386. package/src/theme.css +31 -1
  387. package/ui-catalog.json +448 -41
@@ -1,19 +1,27 @@
1
- import { useState, useCallback, useMemo, type ReactNode } from "react"
1
+ import { useState, useCallback, useEffect, useMemo, type ReactNode } from "react"
2
2
  import { LucideProvider } from "lucide-react"
3
- import { ToolCancelledError, useDisplayMode, useHostContext, useToolContext } from "mcp-use/react"
4
- import { ToastProvider, useToast } from "../components/Toast.js"
5
- import { ToolkitLocaleFallback, useToolkitMessages } from "../locale/locale-provider.js"
6
- import type { ViewMessages } from "../locale/toolkit-messages.js"
7
- import { AppQueryProvider, queryClient } from "../providers/query-provider.js"
3
+ import { ToolCancelledError, useHostContext, useToolContext } from "mcp-use/react"
4
+ import { ToastProvider } from "../components/Toast.js"
8
5
  import {
6
+ ToolkitLocaleFallback,
7
+ useToolkitLocale,
8
+ useToolkitMessages,
9
+ } from "../locale/locale-provider.js"
10
+ import { formatClockTime, resolveFormatLocale } from "../locale/toolkit-messages.js"
11
+ import { AppQueryProvider, useAppQueryClient } from "../providers/query-provider.js"
12
+ import { resolveToolSafety } from "../hooks/tool-result-recovery-model.js"
13
+ import {
14
+ DEFAULT_ASSUME_STRIPPED_AFTER_MS,
9
15
  resolveRecoveryToolName,
10
16
  useToolResultRecovery,
17
+ type RecoveryPolicy,
11
18
  } from "../hooks/use-tool-result-recovery.js"
12
19
  import { ICON_STROKE } from "../lib/icon-stroke.js"
13
20
  import { useHostBridge } from "./host-bridge.js"
21
+ import { useViewResultSink } from "./mcp-use-host-bridge.js"
22
+ import { HostModelContext } from "./model-context.js"
14
23
  import {
15
24
  applyBuilderCommit,
16
- fullscreenAvailable,
17
25
  isCompleteViewData,
18
26
  StepErrors,
19
27
  ViewBody,
@@ -27,13 +35,22 @@ import {
27
35
  } from "./mcp-app-view-chrome.js"
28
36
  import { ViewPlaceholder, type DescribeViewError } from "./mcp-app-view-states.js"
29
37
  import type { LayoutBuilderLabels } from "./layout-builder.js"
38
+ import { useBuildModeFocus } from "./use-build-mode-focus.js"
39
+ import { useFullscreenToggle, useViewRefresh } from "./use-view-actions.js"
30
40
  import { useRetryFocus, useViewLifecycle } from "./use-view-lifecycle.js"
31
- import { DEFAULT_SLOW_HINT_MS, decodeViewResult } from "./view-lifecycle.js"
41
+ import {
42
+ DEFAULT_SLOW_HINT_MS,
43
+ describeViewForModel,
44
+ reloadedAtOf,
45
+ type ViewLifecycle,
46
+ type ViewSource,
47
+ } from "./view-lifecycle.js"
32
48
  import type { WidgetComponent } from "./widget-renderer.js"
33
49
 
34
50
  export type { McpAppViewLabels, ViewTitleLevel, ViewTitleMode } from "./mcp-app-view-chrome.js"
35
51
  export type { DescribeStep } from "./mcp-app-view-chrome.js"
36
- export type { DescribeViewError } from "./mcp-app-view-states.js"
52
+ export type { DescribeViewError, ViewErrorKind } from "./mcp-app-view-states.js"
53
+ export type { RecoveryPolicy } from "../hooks/use-tool-result-recovery.js"
37
54
 
38
55
  export interface McpAppViewProps {
39
56
  /**
@@ -88,6 +105,34 @@ export interface McpAppViewProps {
88
105
  describeError?: DescribeViewError
89
106
  /** Human title of a pipeline step in the failed-steps alert (default: the step id). */
90
107
  describeStep?: DescribeStep
108
+ /**
109
+ * Which originating tools the view may re-execute on its own when the host
110
+ * strips the result's `structuredContent` (claude.ai, Claude Desktop).
111
+ * `"auto"` (default): any tool the host does not report as writing
112
+ * (`readOnlyHint: false` or `destructiveHint: true`); `"read-only"`: only
113
+ * tools reported with `readOnlyHint: true`; `"never"`: none. A recovery
114
+ * the policy forbids shows a note that the chat did not pass the result to
115
+ * the view. It offers to load the view only for a tool the host does not
116
+ * report as writing; for a writing tool it asks for the current state in
117
+ * the chat, so one click never runs the write a second time.
118
+ */
119
+ recoverStrippedResult?: RecoveryPolicy
120
+ /**
121
+ * Grace period (ms) after which a silent call counts as a stripped result.
122
+ * Only used where the view could not watch the host's notifications from
123
+ * the start (e.g. a custom shell mounted under its own `bootstrapView`
124
+ * without `installToolResultObserver`); a view mounted by
125
+ * `mountMcpToolkitApp` sees the stripped result itself and never guesses,
126
+ * so a slow tool is never re-run. Default 2500 ms.
127
+ */
128
+ strippedResultGraceMs?: number
129
+ /**
130
+ * Put the tool name into the cache keys of the data hooks inside the view
131
+ * (`[...queryKey, toolName, args]`, see `toolQueryKey`), so two tools never
132
+ * share an entry. Omitted: an enclosing `AppQueryProvider`'s setting, else
133
+ * the 2.x key `[...queryKey, args]`. The next major makes it the default.
134
+ */
135
+ toolNameInQueryKey?: boolean
91
136
  }
92
137
 
93
138
  /**
@@ -97,6 +142,8 @@ export interface McpAppViewProps {
97
142
  * the host's locale unless a `LocaleProvider` sets one, so the chrome follows
98
143
  * the host language; `useLocale()` in consumer widgets is unaffected. Lucide
99
144
  * icons draw at the CI stroke (2.5) throughout, the toast stack included.
145
+ * The widgets' data hooks fetch through an enclosing `AppQueryProvider`'s
146
+ * client, else the toolkit's shared `queryClient`.
100
147
  */
101
148
  export function McpAppView(props: McpAppViewProps) {
102
149
  const { locale } = useHostContext()
@@ -112,17 +159,32 @@ export function McpAppView(props: McpAppViewProps) {
112
159
  )
113
160
  }
114
161
 
162
+ /** The payload the view shows, where it came from, and when the view loaded it itself. */
163
+ interface ShownView {
164
+ data: ViewData | null
165
+ source: ViewSource | null
166
+ /**
167
+ * When the view re-loaded the payload itself (epoch ms), so its figures can
168
+ * differ from the result the chat answered from (`reloadedAtOf`); `null`
169
+ * otherwise.
170
+ */
171
+ reloadedAt: number | null
172
+ }
173
+
174
+ const NO_VIEW: ShownView = { data: null, source: null, reloadedAt: null }
175
+
115
176
  /**
116
- * Host-delivered payload → local `viewData`, recovered payloads included.
117
- * Render-phase setState (not an effect) so the React Compiler doesn't flag a
118
- * cascading render.
177
+ * Host-delivered payload → the shown view, recovered payloads included, each
178
+ * tagged with its source. Render-phase setState (not an effect) so the React
179
+ * Compiler doesn't flag a cascading render.
119
180
  */
120
181
  function useHostViewData(
121
182
  initialViewData: ViewData | undefined,
122
183
  isPending: boolean,
123
184
  recovered: ViewData | null,
185
+ recoveredAt: number | null,
124
186
  ) {
125
- const [viewData, setViewData] = useState<ViewData | null>(null)
187
+ const [view, setView] = useState<ShownView>(NO_VIEW)
126
188
  // `prev` MUST start as `null`, not `initialViewData`: hosts that expose the
127
189
  // complete tool output before the app's first render (e.g. window.openai
128
190
  // globals injected ahead of script execution) would otherwise never trip the
@@ -132,95 +194,148 @@ function useHostViewData(
132
194
  const [prevInitial, setPrevInitial] = useState<ViewData | undefined | null>(null)
133
195
  if (initialViewData !== prevInitial) {
134
196
  setPrevInitial(initialViewData)
135
- if (!isPending && isCompleteViewData(initialViewData)) setViewData(initialViewData)
197
+ if (!isPending && isCompleteViewData(initialViewData)) {
198
+ setView({ data: initialViewData, source: "host", reloadedAt: null })
199
+ }
136
200
  }
137
201
  const [prevRecovered, setPrevRecovered] = useState<ViewData | null>(null)
138
202
  if (recovered !== prevRecovered) {
139
203
  setPrevRecovered(recovered)
140
- // The host-delivered payload always wins; recovery only fills the gap.
141
- if (recovered && !viewData) setViewData(recovered)
204
+ // The host-delivered payload always wins; a recovery fills the gap, and a
205
+ // recovery for newer arguments replaces an older recovered payload (N21).
206
+ if (recovered) {
207
+ setView((prev) =>
208
+ prev.data === null || prev.source === "recovered"
209
+ ? { data: recovered, source: "recovered", reloadedAt: recoveredAt }
210
+ : prev,
211
+ )
212
+ }
142
213
  }
143
- return [viewData, setViewData] as const
214
+ return [view, setView] as const
144
215
  }
145
216
 
146
217
  /**
147
- * Refresh via the refresh tool. A failure keeps the data and says so in a
148
- * toast in catalog words only; the raw reason goes to the console, never to
149
- * the user.
218
+ * Hands the payload the view shows (the host's, its own recovery's, the
219
+ * user's retry or refresh) to the host bridge, so `HostBridge.getWidgetData()`
220
+ * returns what the view shows (toolkit#176 K15), also after a latch the view
221
+ * could not render.
150
222
  */
151
- function useViewRefresh(options: {
152
- viewData: ViewData | null
153
- setViewData: (next: ViewData) => void
154
- callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>
155
- refreshToolName: string
156
- labels: Pick<ViewMessages, "refreshFailed" | "refreshFailedHint">
157
- }) {
158
- const { viewData, setViewData, callTool, refreshToolName, labels } = options
159
- const toast = useToast()
160
- const [isRefreshing, setIsRefreshing] = useState(false)
161
- const refreshView = useCallback(async () => {
162
- if (!viewData?._refreshParams) return
163
- setIsRefreshing(true)
164
- let failure: unknown = null
165
- try {
166
- const result = await callTool(
167
- refreshToolName,
168
- viewData._refreshParams as unknown as Record<string, unknown>,
169
- )
170
- // One decoding convention with `useToolQuery` (structured-first), with a
171
- // typed outcome: an unusable payload is a failure, never exception text.
172
- const outcome = decodeViewResult(result, isCompleteViewData)
173
- if (outcome.ok) setViewData(outcome.data)
174
- else failure = outcome.reason ?? "the refresh tool returned no view"
175
- // The call went through, so self-fetching widgets refresh as well;
176
- // a tool error refreshes nothing.
177
- if (outcome.ok || !outcome.isError) void queryClient.invalidateQueries()
178
- } catch (e) {
179
- failure = e
180
- } finally {
181
- setIsRefreshing(false)
182
- }
183
- if (failure === null) return
184
- console.warn("[mcp-toolkit] refresh failed:", failure)
185
- toast.show({
186
- tone: "danger",
187
- title: labels.refreshFailed,
188
- description: labels.refreshFailedHint,
189
- })
190
- }, [callTool, labels, refreshToolName, setViewData, toast, viewData])
191
- return { isRefreshing, refreshView }
223
+ function useReportShownPayload(view: ShownView) {
224
+ const report = useViewResultSink()
225
+ const payload = view.data
226
+ useEffect(() => {
227
+ if (payload) report?.(payload)
228
+ }, [payload, report])
229
+ }
230
+
231
+ /** The rendering call as mcp-use 2.x latches it, plus the tool the host names. */
232
+ function useRenderCall() {
233
+ const tool = useToolContext()
234
+ const { hostContext } = useHostContext()
235
+ // Since mcp-use 2.7.1 a host cancellation latches as `status: "error"`.
236
+ const isCancelled = tool.status === "error" && tool.error instanceof ToolCancelledError
237
+ const isCallError = tool.status === "error" && !isCancelled
238
+ return {
239
+ tool,
240
+ hostContext,
241
+ isPending: tool.status === "pending",
242
+ isCancelled,
243
+ isCallError,
244
+ callError: isCallError ? tool.error : undefined,
245
+ initialViewData: tool.status === "ready" ? (tool.toolOutput as ViewData) : undefined,
246
+ toolName: resolveRecoveryToolName(hostContext),
247
+ /** The host reports the tool as writing: the view never offers to re-run it after a block. */
248
+ toolWrites: resolveToolSafety(hostContext) === "writes",
249
+ }
192
250
  }
193
251
 
194
252
  /**
195
- * The fullscreen toggle, gated on the host's offer. A rejected request shows
196
- * a toast in catalog words; the raw reason goes to the console.
253
+ * The payload the view shows: the host's, else the stripped-result recovery's.
254
+ * Hosts that strip `structuredContent` (claude.ai) never latch a result under
255
+ * mcp-use 2.x; the recovery re-executes the call once when the stripped
256
+ * result arrives. It never fires on a cancelled or failed call, nor while a
257
+ * slow tool is still running (#176): those render their state.
197
258
  */
198
- function useFullscreenToggle(
199
- labels: Pick<ViewMessages, "fullscreenFailed" | "fullscreenFailedHint">,
259
+ function useShownView(
260
+ call: ReturnType<typeof useRenderCall>,
261
+ callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>,
262
+ policy: RecoveryPolicy = "auto",
263
+ graceMs: number = DEFAULT_ASSUME_STRIPPED_AFTER_MS,
200
264
  ) {
201
- const { displayMode, availableDisplayModes, requestDisplayMode } = useDisplayMode()
202
- const toast = useToast()
203
- const toggle = useCallback(async () => {
204
- const mode = displayMode === "fullscreen" ? "inline" : "fullscreen"
205
- try {
206
- // Advisory since 2.x: `displayMode` updates when the host applies it.
207
- await requestDisplayMode({ mode })
208
- } catch (e) {
209
- console.warn("[mcp-toolkit] display-mode request failed:", e)
210
- toast.show({
211
- tone: "danger",
212
- title: labels.fullscreenFailed,
213
- description: labels.fullscreenFailedHint,
214
- })
215
- }
216
- }, [displayMode, labels, requestDisplayMode, toast])
265
+ const recovery = useToolResultRecovery<ViewData>({
266
+ resultReady: !call.isPending,
267
+ cancelled: call.isCancelled,
268
+ errored: call.isCallError,
269
+ props: call.initialViewData,
270
+ isValid: isCompleteViewData,
271
+ toolInput: call.tool.toolInput,
272
+ hostContext: call.hostContext,
273
+ callTool,
274
+ policy,
275
+ assumeStrippedAfterMs: graceMs,
276
+ })
277
+ const [view, setView] = useHostViewData(
278
+ call.initialViewData,
279
+ call.isPending,
280
+ recovery.data,
281
+ recovery.recoveredAt,
282
+ )
283
+ useReportShownPayload(view)
284
+ // A re-run's figures can differ from the answer in the chat (N23): stamp
285
+ // it, unless the original call failed and the chat never had figures.
286
+ const callFailed = call.isCallError || call.isCancelled
287
+ const showRetried = useCallback(
288
+ (data: ViewData) =>
289
+ setView({ data, source: "retry", reloadedAt: reloadedAtOf("retry", Date.now(), callFailed) }),
290
+ [setView, callFailed],
291
+ )
292
+ const showRefreshed = useCallback(
293
+ (data: ViewData) =>
294
+ setView({
295
+ data,
296
+ source: "refresh",
297
+ reloadedAt: reloadedAtOf("refresh", Date.now(), callFailed),
298
+ }),
299
+ [setView, callFailed],
300
+ )
217
301
  return {
218
- isFullscreen: displayMode === "fullscreen",
219
- canFullscreen: fullscreenAvailable(displayMode, availableDisplayModes),
220
- toggle,
302
+ view,
303
+ setView,
304
+ showRetried,
305
+ showRefreshed,
306
+ // Without a named tool nothing can re-run it: a plain failure. A policy
307
+ // block is its own state: the tool ran, and the view must not repeat it.
308
+ recoveryFailed: recovery.failed || recovery.blocked === "no-tool",
309
+ recoveryBlocked: recovery.blocked === "policy",
221
310
  }
222
311
  }
223
312
 
313
+ /**
314
+ * The view's own line of model context (toolkit#176 K20, N23): an error the
315
+ * user sees instead of the view, failed data steps, or data the view
316
+ * re-loaded itself (a recovery, a retry, a refresh). Renders nothing while
317
+ * there is nothing to report.
318
+ */
319
+ function ViewModelContext({
320
+ lifecycle,
321
+ toolName,
322
+ view,
323
+ }: {
324
+ lifecycle: ViewLifecycle
325
+ toolName: string | undefined
326
+ view: ShownView
327
+ }) {
328
+ const content = describeViewForModel({
329
+ lifecycle,
330
+ toolName,
331
+ stepErrors: view.data?.context.errors,
332
+ stepsRetryable: Boolean(view.data?._refreshParams),
333
+ reloadedAt: view.reloadedAt,
334
+ reloadedBy: reloadedByOf(view.source),
335
+ })
336
+ return content ? <HostModelContext content={content} /> : null
337
+ }
338
+
224
339
  function McpAppViewShell({
225
340
  widgets,
226
341
  refreshToolName = "refresh-view",
@@ -233,13 +348,21 @@ function McpAppViewShell({
233
348
  slowHintAfterMs = DEFAULT_SLOW_HINT_MS,
234
349
  describeError,
235
350
  describeStep,
351
+ recoverStrippedResult,
352
+ strippedResultGraceMs,
353
+ toolNameInQueryKey,
236
354
  }: McpAppViewProps) {
237
355
  const messages = useToolkitMessages()
238
356
  const L = useMemo(() => ({ ...messages.view, ...labels }), [messages.view, labels])
239
357
  // The mcp-use 2.x view channels (all need the `bootstrapView` mount
240
358
  // `mountMcpToolkitApp` provides); tools go through the HostBridge.
241
- const tool = useToolContext()
242
- const { safeArea, hostContext } = useHostContext()
359
+ const call = useRenderCall()
360
+ const { safeArea, locale, timeZone } = useHostContext()
361
+ // A time in the language of the sentence around it, in the host's zone.
362
+ const formatLocale = resolveFormatLocale(useToolkitLocale(), locale)
363
+ // The client the view's widgets fetch through (an enclosing
364
+ // AppQueryProvider's, else the shared one): a refresh invalidates it.
365
+ const viewClient = useAppQueryClient()
243
366
  const bridge = useHostBridge()
244
367
  const callTool = useCallback(
245
368
  (name: string, args: Record<string, unknown>) => bridge.callTool(name, args),
@@ -251,56 +374,46 @@ function McpAppViewShell({
251
374
  (name: string, args: object) => bridge.callTool(name, args as Record<string, unknown>),
252
375
  [bridge],
253
376
  )
254
- const isPending = tool.status === "pending"
255
- // Since mcp-use 2.7.1 a host cancellation latches as `status: "error"`.
256
- const isCancelled = tool.status === "error" && tool.error instanceof ToolCancelledError
257
- const isCallError = tool.status === "error" && !isCancelled
258
- const initialViewData = tool.status === "ready" ? (tool.toolOutput as ViewData) : undefined
259
-
260
- // Hosts that strip `structuredContent` (claude.ai) never latch a result under
261
- // mcp-use 2.x; the recovery re-executes the call once to fill the gap. It
262
- // never fires on a cancelled or failed call (#176): those render their state.
263
- const recovery = useToolResultRecovery<ViewData>({
264
- resultReady: !isPending,
265
- cancelled: isCancelled,
266
- errored: isCallError,
267
- props: initialViewData,
268
- isValid: isCompleteViewData,
269
- toolInput: tool.toolInput,
270
- hostContext,
271
- callTool,
272
- })
273
- const [viewData, setViewData] = useHostViewData(initialViewData, isPending, recovery.data)
377
+ const shown = useShownView(call, callTool, recoverStrippedResult, strippedResultGraceMs)
378
+ const { view, setView } = shown
379
+ const viewData = view.data
274
380
  const { lifecycle, retry: retryCall } = useViewLifecycle({
275
381
  hasViewData: viewData !== null,
276
- callStatus: tool.status,
277
- cancelled: isCancelled,
278
- callError: isCallError ? tool.error : undefined,
279
- recoveryFailed: recovery.failed,
280
- toolName: resolveRecoveryToolName(hostContext),
281
- toolInput: tool.toolInput,
382
+ callStatus: call.tool.status,
383
+ cancelled: call.isCancelled,
384
+ callError: call.callError,
385
+ recoveryFailed: shown.recoveryFailed,
386
+ recoveryBlocked: shown.recoveryBlocked,
387
+ toolWrites: call.toolWrites,
388
+ toolName: call.toolName,
389
+ toolInput: call.tool.toolInput,
282
390
  callTool,
283
391
  isValid: isCompleteViewData,
284
- onResult: setViewData,
392
+ onResult: shown.showRetried,
285
393
  slowHintAfterMs,
286
394
  })
287
395
  const { frameRef, retry } = useRetryFocus(lifecycle.phase, retryCall)
288
396
  const { isRefreshing, refreshView } = useViewRefresh({
289
397
  viewData,
290
- setViewData,
398
+ setViewData: shown.showRefreshed,
291
399
  callTool,
292
400
  refreshToolName,
293
401
  labels: L,
402
+ client: viewClient,
294
403
  })
295
404
  const fullscreen = useFullscreenToggle(L)
296
405
  // In-iframe build mode is purely a UI state — the LLM never sees the catalogue.
297
406
  const [buildMode, setBuildMode] = useState(false)
407
+ useBuildModeFocus(buildMode, frameRef)
298
408
  const exitBuildMode = useCallback(
299
409
  (commit?: BuilderCommit) => {
300
- if (commit) setViewData((prev) => (prev ? applyBuilderCommit(prev, commit) : prev))
410
+ if (commit) setView((prev) => withBuilderCommit(prev, commit))
301
411
  setBuildMode(false)
302
412
  },
303
- [setViewData],
413
+ [setView],
414
+ )
415
+ const viewContext = (
416
+ <ViewModelContext lifecycle={lifecycle} toolName={call.toolName} view={view} />
304
417
  )
305
418
 
306
419
  // `isPending` must not gate the render: on stripping hosts the latch stays
@@ -309,6 +422,7 @@ function McpAppViewShell({
309
422
  if (!viewData) {
310
423
  return (
311
424
  <div ref={frameRef} tabIndex={-1} data-slot="mcp-app-view" className="outline-hidden">
425
+ {viewContext}
312
426
  <ViewPlaceholder
313
427
  lifecycle={lifecycle}
314
428
  labels={L}
@@ -321,6 +435,7 @@ function McpAppViewShell({
321
435
 
322
436
  return (
323
437
  <div ref={frameRef} tabIndex={-1} data-slot="mcp-app-view" className="outline-hidden">
438
+ {viewContext}
324
439
  <main
325
440
  style={{
326
441
  // Fullscreen: the view owns the viewport and scrolls itself. Inline:
@@ -347,6 +462,7 @@ function McpAppViewShell({
347
462
  onRefresh={refreshView}
348
463
  onEnterBuildMode={() => setBuildMode(true)}
349
464
  onToggleFullscreen={fullscreen.toggle}
465
+ dataAsOf={formatDataAsOf(view.reloadedAt, formatLocale, timeZone)}
350
466
  />
351
467
 
352
468
  <StepErrors
@@ -358,7 +474,11 @@ function McpAppViewShell({
358
474
  onRetry={viewData._refreshParams ? () => void refreshView() : undefined}
359
475
  />
360
476
 
361
- <AppQueryProvider callTool={callToolFn}>
477
+ <AppQueryProvider
478
+ callTool={callToolFn}
479
+ client={viewClient}
480
+ toolNameInQueryKey={toolNameInQueryKey}
481
+ >
362
482
  <ViewBody
363
483
  buildMode={buildMode}
364
484
  viewData={viewData}
@@ -372,3 +492,33 @@ function McpAppViewShell({
372
492
  </div>
373
493
  )
374
494
  }
495
+
496
+ /** How the model line names the view's own re-load of the shown payload. */
497
+ function reloadedByOf(source: ViewSource | null): "recovery" | "refresh" | "builder" {
498
+ if (source === "refresh" || source === "builder") return source
499
+ return "recovery"
500
+ }
501
+
502
+ /**
503
+ * The shown view with a builder commit applied (nothing to apply without
504
+ * data). A commit that carries the builder's live context shows figures the
505
+ * builder loaded itself (its catalogue call ran the steps again), so it is
506
+ * stamped like a refresh: the toolbar says when, and the model learns that
507
+ * they can differ from the result the chat answered from. A commit without a
508
+ * context keeps the shown figures, and with them their source and stamp.
509
+ */
510
+ function withBuilderCommit(view: ShownView, commit: BuilderCommit): ShownView {
511
+ if (!view.data) return view
512
+ const data = applyBuilderCommit(view.data, commit)
513
+ if (!commit.context) return { ...view, data }
514
+ return { data, source: "builder", reloadedAt: reloadedAtOf("builder", Date.now(), false) }
515
+ }
516
+
517
+ /** The toolbar's "as of" time of a re-loaded payload, `undefined` for any other. */
518
+ function formatDataAsOf(
519
+ reloadedAt: number | null,
520
+ locale: string,
521
+ timeZone: string | undefined,
522
+ ): string | undefined {
523
+ return reloadedAt === null ? undefined : formatClockTime(reloadedAt, locale, timeZone)
524
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import { mountMcpToolkitApp } from "./mcp-toolkit-app.js"
3
+
4
+ /**
5
+ * The mount order behind K13 (toolkit#176): `mountMcpToolkitApp` installs the
6
+ * tool-result observer BEFORE `bootstrapView` connects the view to its host,
7
+ * so the observer sees the handshake and the view never mistakes a slow tool
8
+ * for a stripped result.
9
+ */
10
+
11
+ const calls = vi.hoisted(() => ({ order: [] as string[] }))
12
+
13
+ vi.mock(import("../lib/tool-result-observer.js"), async (importOriginal) => {
14
+ const actual = await importOriginal()
15
+ return {
16
+ ...actual,
17
+ installToolResultObserver: () => {
18
+ calls.order.push("installToolResultObserver")
19
+ return null
20
+ },
21
+ }
22
+ })
23
+
24
+ vi.mock(import("mcp-use/react"), async (importOriginal) => {
25
+ const actual = await importOriginal()
26
+ return {
27
+ ...actual,
28
+ bootstrapView: (() => {
29
+ calls.order.push("bootstrapView")
30
+ }) as unknown as typeof actual.bootstrapView,
31
+ }
32
+ })
33
+
34
+ describe("mountMcpToolkitApp", () => {
35
+ it("installs the tool-result observer before bootstrapView connects to the host", () => {
36
+ calls.order = []
37
+ mountMcpToolkitApp({ widgets: {} })
38
+ expect(calls.order).toEqual(["installToolResultObserver", "bootstrapView"])
39
+ })
40
+ })
@@ -1,5 +1,6 @@
1
1
  import { useLayoutEffect, type ReactNode } from "react"
2
2
  import { ThemeProvider, bootstrapView, useHostContext } from "mcp-use/react"
3
+ import { installToolResultObserver } from "../lib/tool-result-observer.js"
3
4
  import { adoptHostFontVariables } from "../theme/host-fonts.js"
4
5
  import { McpAppView, type McpAppViewProps } from "./mcp-app-view.js"
5
6
  import { McpUseHostBridgeProvider } from "./mcp-use-host-bridge.js"
@@ -54,7 +55,12 @@ export function McpToolkitApp(props: McpAppViewProps) {
54
55
  *
55
56
  * `bootstrapView` owns the mount: it connects the ext-apps postMessage bridge
56
57
  * to the host, installs an error boundary, and auto-reports size changes
57
- * (replacing 1.x's `createRoot` + `McpUseProvider` pair).
58
+ * (replacing 1.x's `createRoot` + `McpUseProvider` pair). The tool-result
59
+ * observer goes in first, so it sees the whole host lifecycle from the
60
+ * handshake on: the view then tells a stripped result from a slow tool and
61
+ * never re-runs a call the host is still answering (toolkit#176). A custom
62
+ * shell under its own `bootstrapView` calls `installToolResultObserver()`
63
+ * before it for the same effect.
58
64
  *
59
65
  * ```tsx
60
66
  * // main.tsx of the widget bundle
@@ -62,5 +68,6 @@ export function McpToolkitApp(props: McpAppViewProps) {
62
68
  * ```
63
69
  */
64
70
  export function mountMcpToolkitApp(props: McpAppViewProps): void {
71
+ installToolResultObserver()
65
72
  bootstrapView({ default: () => <McpToolkitApp {...props} /> })
66
73
  }