@miragon/mcp-toolkit-ui 2.6.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 (511) hide show
  1. package/README.md +14 -5
  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.d.ts +7 -2
  7. package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
  8. package/dist/app/builder/BuilderBanners.js +8 -3
  9. package/dist/app/builder/BuilderBanners.js.map +1 -1
  10. package/dist/app/builder/CanvasRow.d.ts +4 -2
  11. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  12. package/dist/app/builder/CanvasRow.js +29 -17
  13. package/dist/app/builder/CanvasRow.js.map +1 -1
  14. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  15. package/dist/app/builder/CatalogueSheet.js +14 -6
  16. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  18. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  19. package/dist/app/builder/CellPropsSheet.js +10 -3
  20. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  21. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  22. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  23. package/dist/app/builder/DiscardDialog.js +18 -0
  24. package/dist/app/builder/DiscardDialog.js.map +1 -0
  25. package/dist/app/builder/EmptyHint.d.ts +4 -0
  26. package/dist/app/builder/EmptyHint.d.ts.map +1 -1
  27. package/dist/app/builder/EmptyHint.js +6 -1
  28. package/dist/app/builder/EmptyHint.js.map +1 -1
  29. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  30. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  31. package/dist/app/builder/LayoutSurface.js +74 -23
  32. package/dist/app/builder/LayoutSurface.js.map +1 -1
  33. package/dist/app/builder/PipelineStrip.d.ts +5 -2
  34. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  35. package/dist/app/builder/PipelineStrip.js +33 -15
  36. package/dist/app/builder/PipelineStrip.js.map +1 -1
  37. package/dist/app/builder/SaveDialog.d.ts +9 -1
  38. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  39. package/dist/app/builder/SaveDialog.js +9 -3
  40. package/dist/app/builder/SaveDialog.js.map +1 -1
  41. package/dist/app/builder/Toolbar.d.ts +14 -4
  42. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  43. package/dist/app/builder/Toolbar.js +24 -7
  44. package/dist/app/builder/Toolbar.js.map +1 -1
  45. package/dist/app/builder/Workspace.d.ts +23 -3
  46. package/dist/app/builder/Workspace.d.ts.map +1 -1
  47. package/dist/app/builder/Workspace.js +58 -8
  48. package/dist/app/builder/Workspace.js.map +1 -1
  49. package/dist/app/builder/builder-model.d.ts +146 -3
  50. package/dist/app/builder/builder-model.d.ts.map +1 -1
  51. package/dist/app/builder/builder-model.js +196 -5
  52. package/dist/app/builder/builder-model.js.map +1 -1
  53. package/dist/app/builder/labels.d.ts +13 -62
  54. package/dist/app/builder/labels.d.ts.map +1 -1
  55. package/dist/app/builder/labels.js +9 -58
  56. package/dist/app/builder/labels.js.map +1 -1
  57. package/dist/app/builder/reveal.d.ts +10 -0
  58. package/dist/app/builder/reveal.d.ts.map +1 -0
  59. package/dist/app/builder/reveal.js +10 -0
  60. package/dist/app/builder/reveal.js.map +1 -0
  61. package/dist/app/builder/status-chip.d.ts.map +1 -1
  62. package/dist/app/builder/status-chip.js +3 -2
  63. package/dist/app/builder/status-chip.js.map +1 -1
  64. package/dist/app/builder/use-builder-catalogue.d.ts +5 -1
  65. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  66. package/dist/app/builder/use-builder-catalogue.js +50 -17
  67. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  68. package/dist/app/builder/use-builder-session.d.ts +30 -0
  69. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  70. package/dist/app/builder/use-builder-session.js +58 -0
  71. package/dist/app/builder/use-builder-session.js.map +1 -0
  72. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  73. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  74. package/dist/app/builder/use-display-resolver.js +16 -0
  75. package/dist/app/builder/use-display-resolver.js.map +1 -0
  76. package/dist/app/builder/use-save-dashboard.d.ts +3 -1
  77. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
  78. package/dist/app/builder/use-save-dashboard.js +3 -1
  79. package/dist/app/builder/use-save-dashboard.js.map +1 -1
  80. package/dist/app/index.d.ts +8 -3
  81. package/dist/app/index.d.ts.map +1 -1
  82. package/dist/app/index.js +8 -2
  83. package/dist/app/index.js.map +1 -1
  84. package/dist/app/layout-builder.d.ts +18 -25
  85. package/dist/app/layout-builder.d.ts.map +1 -1
  86. package/dist/app/layout-builder.js +84 -78
  87. package/dist/app/layout-builder.js.map +1 -1
  88. package/dist/app/mcp-app-view-chrome.d.ts +103 -20
  89. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  90. package/dist/app/mcp-app-view-chrome.js +125 -40
  91. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  92. package/dist/app/mcp-app-view-states.d.ts +23 -0
  93. package/dist/app/mcp-app-view-states.d.ts.map +1 -0
  94. package/dist/app/mcp-app-view-states.js +55 -0
  95. package/dist/app/mcp-app-view-states.js.map +1 -0
  96. package/dist/app/mcp-app-view.d.ts +72 -5
  97. package/dist/app/mcp-app-view.d.ts.map +1 -1
  98. package/dist/app/mcp-app-view.js +241 -141
  99. package/dist/app/mcp-app-view.js.map +1 -1
  100. package/dist/app/mcp-toolkit-app.d.ts +8 -2
  101. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  102. package/dist/app/mcp-toolkit-app.js +33 -4
  103. package/dist/app/mcp-toolkit-app.js.map +1 -1
  104. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  105. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  106. package/dist/app/mcp-use-host-bridge.js +51 -22
  107. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  108. package/dist/app/model-context.d.ts +71 -0
  109. package/dist/app/model-context.d.ts.map +1 -0
  110. package/dist/app/model-context.js +101 -0
  111. package/dist/app/model-context.js.map +1 -0
  112. package/dist/app/use-build-mode-focus.d.ts +33 -0
  113. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  114. package/dist/app/use-build-mode-focus.js +59 -0
  115. package/dist/app/use-build-mode-focus.js.map +1 -0
  116. package/dist/app/use-view-actions.d.ts +31 -0
  117. package/dist/app/use-view-actions.d.ts.map +1 -0
  118. package/dist/app/use-view-actions.js +81 -0
  119. package/dist/app/use-view-actions.js.map +1 -0
  120. package/dist/app/use-view-lifecycle.d.ts +42 -0
  121. package/dist/app/use-view-lifecycle.d.ts.map +1 -0
  122. package/dist/app/use-view-lifecycle.js +81 -0
  123. package/dist/app/use-view-lifecycle.js.map +1 -0
  124. package/dist/app/view-lifecycle.d.ts +185 -0
  125. package/dist/app/view-lifecycle.d.ts.map +1 -0
  126. package/dist/app/view-lifecycle.js +207 -0
  127. package/dist/app/view-lifecycle.js.map +1 -0
  128. package/dist/app/widget-fixture.d.ts +10 -3
  129. package/dist/app/widget-fixture.d.ts.map +1 -1
  130. package/dist/app/widget-fixture.js +21 -8
  131. package/dist/app/widget-fixture.js.map +1 -1
  132. package/dist/app/widget-props-sheet.d.ts +6 -10
  133. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  134. package/dist/app/widget-props-sheet.js +25 -20
  135. package/dist/app/widget-props-sheet.js.map +1 -1
  136. package/dist/app/widget-renderer.d.ts +41 -1
  137. package/dist/app/widget-renderer.d.ts.map +1 -1
  138. package/dist/app/widget-renderer.js +82 -31
  139. package/dist/app/widget-renderer.js.map +1 -1
  140. package/dist/components/DrillButton.d.ts +9 -6
  141. package/dist/components/DrillButton.d.ts.map +1 -1
  142. package/dist/components/DrillButton.js +9 -6
  143. package/dist/components/DrillButton.js.map +1 -1
  144. package/dist/components/EmptyState.d.ts +33 -0
  145. package/dist/components/EmptyState.d.ts.map +1 -0
  146. package/dist/components/EmptyState.js +19 -0
  147. package/dist/components/EmptyState.js.map +1 -0
  148. package/dist/components/ErrorState.d.ts +53 -0
  149. package/dist/components/ErrorState.d.ts.map +1 -0
  150. package/dist/components/ErrorState.js +77 -0
  151. package/dist/components/ErrorState.js.map +1 -0
  152. package/dist/components/FilterBar.d.ts +16 -5
  153. package/dist/components/FilterBar.d.ts.map +1 -1
  154. package/dist/components/FilterBar.js +13 -5
  155. package/dist/components/FilterBar.js.map +1 -1
  156. package/dist/components/GroupCard.d.ts.map +1 -1
  157. package/dist/components/GroupCard.js +2 -1
  158. package/dist/components/GroupCard.js.map +1 -1
  159. package/dist/components/KpiGrid.d.ts +70 -18
  160. package/dist/components/KpiGrid.d.ts.map +1 -1
  161. package/dist/components/KpiGrid.js +112 -23
  162. package/dist/components/KpiGrid.js.map +1 -1
  163. package/dist/components/ListFooter.d.ts +32 -4
  164. package/dist/components/ListFooter.d.ts.map +1 -1
  165. package/dist/components/ListFooter.js +35 -6
  166. package/dist/components/ListFooter.js.map +1 -1
  167. package/dist/components/PagedListFooter.d.ts +57 -0
  168. package/dist/components/PagedListFooter.d.ts.map +1 -0
  169. package/dist/components/PagedListFooter.js +27 -0
  170. package/dist/components/PagedListFooter.js.map +1 -0
  171. package/dist/components/Pills.d.ts +36 -12
  172. package/dist/components/Pills.d.ts.map +1 -1
  173. package/dist/components/Pills.js +29 -8
  174. package/dist/components/Pills.js.map +1 -1
  175. package/dist/components/SectionHeading.d.ts +3 -1
  176. package/dist/components/SectionHeading.d.ts.map +1 -1
  177. package/dist/components/SectionHeading.js +6 -2
  178. package/dist/components/SectionHeading.js.map +1 -1
  179. package/dist/components/Toast.d.ts +43 -0
  180. package/dist/components/Toast.d.ts.map +1 -0
  181. package/dist/components/Toast.js +106 -0
  182. package/dist/components/Toast.js.map +1 -0
  183. package/dist/components/WidgetHeader.d.ts +34 -10
  184. package/dist/components/WidgetHeader.d.ts.map +1 -1
  185. package/dist/components/WidgetHeader.js +11 -6
  186. package/dist/components/WidgetHeader.js.map +1 -1
  187. package/dist/hooks/index.d.ts +4 -4
  188. package/dist/hooks/index.d.ts.map +1 -1
  189. package/dist/hooks/index.js +1 -1
  190. package/dist/hooks/index.js.map +1 -1
  191. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  192. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  193. package/dist/hooks/tool-result-recovery-model.js +273 -0
  194. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  195. package/dist/hooks/use-tool-query.d.ts +56 -14
  196. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  197. package/dist/hooks/use-tool-query.js +102 -25
  198. package/dist/hooks/use-tool-query.js.map +1 -1
  199. package/dist/hooks/use-tool-result-recovery.d.ts +61 -44
  200. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  201. package/dist/hooks/use-tool-result-recovery.js +62 -110
  202. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  203. package/dist/hooks/use-view-data.d.ts +55 -7
  204. package/dist/hooks/use-view-data.d.ts.map +1 -1
  205. package/dist/hooks/use-view-data.js +81 -11
  206. package/dist/hooks/use-view-data.js.map +1 -1
  207. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  208. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  209. package/dist/hooks/use-view-tool-query.js +15 -14
  210. package/dist/hooks/use-view-tool-query.js.map +1 -1
  211. package/dist/index.d.ts +27 -13
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +29 -7
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/focus-return.d.ts +61 -0
  216. package/dist/lib/focus-return.d.ts.map +1 -0
  217. package/dist/lib/focus-return.js +46 -0
  218. package/dist/lib/focus-return.js.map +1 -0
  219. package/dist/lib/focus-ring.d.ts +18 -0
  220. package/dist/lib/focus-ring.d.ts.map +1 -0
  221. package/dist/lib/focus-ring.js +18 -0
  222. package/dist/lib/focus-ring.js.map +1 -0
  223. package/dist/lib/icon-stroke.d.ts +12 -0
  224. package/dist/lib/icon-stroke.d.ts.map +1 -0
  225. package/dist/lib/icon-stroke.js +12 -0
  226. package/dist/lib/icon-stroke.js.map +1 -0
  227. package/dist/lib/parse-tool-result.d.ts +23 -1
  228. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  229. package/dist/lib/parse-tool-result.js +38 -2
  230. package/dist/lib/parse-tool-result.js.map +1 -1
  231. package/dist/lib/toast-model.d.ts +86 -0
  232. package/dist/lib/toast-model.d.ts.map +1 -0
  233. package/dist/lib/toast-model.js +102 -0
  234. package/dist/lib/toast-model.js.map +1 -0
  235. package/dist/lib/tone-utils.d.ts +95 -9
  236. package/dist/lib/tone-utils.d.ts.map +1 -1
  237. package/dist/lib/tone-utils.js +100 -9
  238. package/dist/lib/tone-utils.js.map +1 -1
  239. package/dist/lib/tool-query.d.ts +107 -0
  240. package/dist/lib/tool-query.d.ts.map +1 -0
  241. package/dist/lib/tool-query.js +182 -0
  242. package/dist/lib/tool-query.js.map +1 -0
  243. package/dist/lib/tool-result-observer.d.ts +108 -0
  244. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  245. package/dist/lib/tool-result-observer.js +158 -0
  246. package/dist/lib/tool-result-observer.js.map +1 -0
  247. package/dist/lib/typography.d.ts +37 -0
  248. package/dist/lib/typography.d.ts.map +1 -0
  249. package/dist/lib/typography.js +41 -0
  250. package/dist/lib/typography.js.map +1 -0
  251. package/dist/lib/utils.d.ts.map +1 -1
  252. package/dist/lib/utils.js +11 -1
  253. package/dist/lib/utils.js.map +1 -1
  254. package/dist/locale/index.d.ts +3 -1
  255. package/dist/locale/index.d.ts.map +1 -1
  256. package/dist/locale/index.js +2 -1
  257. package/dist/locale/index.js.map +1 -1
  258. package/dist/locale/locale-provider.d.ts +42 -1
  259. package/dist/locale/locale-provider.d.ts.map +1 -1
  260. package/dist/locale/locale-provider.js +44 -3
  261. package/dist/locale/locale-provider.js.map +1 -1
  262. package/dist/locale/message-types.d.ts +336 -0
  263. package/dist/locale/message-types.d.ts.map +1 -0
  264. package/dist/locale/message-types.js +7 -0
  265. package/dist/locale/message-types.js.map +1 -0
  266. package/dist/locale/messages-de.d.ts +9 -0
  267. package/dist/locale/messages-de.d.ts.map +1 -0
  268. package/dist/locale/messages-de.js +185 -0
  269. package/dist/locale/messages-de.js.map +1 -0
  270. package/dist/locale/messages-en.d.ts +8 -0
  271. package/dist/locale/messages-en.d.ts.map +1 -0
  272. package/dist/locale/messages-en.js +184 -0
  273. package/dist/locale/messages-en.js.map +1 -0
  274. package/dist/locale/toolkit-messages.d.ts +57 -0
  275. package/dist/locale/toolkit-messages.d.ts.map +1 -0
  276. package/dist/locale/toolkit-messages.js +103 -0
  277. package/dist/locale/toolkit-messages.js.map +1 -0
  278. package/dist/primitives/alert.d.ts +1 -1
  279. package/dist/primitives/alert.d.ts.map +1 -1
  280. package/dist/primitives/alert.js +6 -2
  281. package/dist/primitives/alert.js.map +1 -1
  282. package/dist/primitives/badge.d.ts +1 -1
  283. package/dist/primitives/badge.d.ts.map +1 -1
  284. package/dist/primitives/badge.js +19 -6
  285. package/dist/primitives/badge.js.map +1 -1
  286. package/dist/primitives/button.d.ts +1 -1
  287. package/dist/primitives/button.d.ts.map +1 -1
  288. package/dist/primitives/button.js +12 -8
  289. package/dist/primitives/button.js.map +1 -1
  290. package/dist/primitives/card.js +4 -4
  291. package/dist/primitives/card.js.map +1 -1
  292. package/dist/primitives/dialog.d.ts +7 -3
  293. package/dist/primitives/dialog.d.ts.map +1 -1
  294. package/dist/primitives/dialog.js +22 -8
  295. package/dist/primitives/dialog.js.map +1 -1
  296. package/dist/primitives/input.d.ts.map +1 -1
  297. package/dist/primitives/input.js +4 -1
  298. package/dist/primitives/input.js.map +1 -1
  299. package/dist/primitives/scroll-area.js +1 -1
  300. package/dist/primitives/scroll-area.js.map +1 -1
  301. package/dist/primitives/select.d.ts.map +1 -1
  302. package/dist/primitives/select.js +6 -3
  303. package/dist/primitives/select.js.map +1 -1
  304. package/dist/primitives/sheet.d.ts +3 -1
  305. package/dist/primitives/sheet.d.ts.map +1 -1
  306. package/dist/primitives/sheet.js +17 -7
  307. package/dist/primitives/sheet.js.map +1 -1
  308. package/dist/primitives/skeleton.js +1 -1
  309. package/dist/primitives/skeleton.js.map +1 -1
  310. package/dist/primitives/switch.d.ts.map +1 -1
  311. package/dist/primitives/switch.js +2 -1
  312. package/dist/primitives/switch.js.map +1 -1
  313. package/dist/primitives/tabs.d.ts.map +1 -1
  314. package/dist/primitives/tabs.js +5 -3
  315. package/dist/primitives/tabs.js.map +1 -1
  316. package/dist/providers/query-provider.d.ts +32 -1
  317. package/dist/providers/query-provider.d.ts.map +1 -1
  318. package/dist/providers/query-provider.js +53 -4
  319. package/dist/providers/query-provider.js.map +1 -1
  320. package/dist/render-cases-primitives.js +1 -1
  321. package/dist/render-cases-primitives.js.map +1 -1
  322. package/dist/render-cases.d.ts.map +1 -1
  323. package/dist/render-cases.js +48 -6
  324. package/dist/render-cases.js.map +1 -1
  325. package/dist/render-effects.d.ts +42 -0
  326. package/dist/render-effects.d.ts.map +1 -0
  327. package/dist/render-effects.js +81 -0
  328. package/dist/render-effects.js.map +1 -0
  329. package/dist/render-scan.d.ts +25 -0
  330. package/dist/render-scan.d.ts.map +1 -0
  331. package/dist/render-scan.js +66 -0
  332. package/dist/render-scan.js.map +1 -0
  333. package/dist/theme/cd-tokens.d.ts +66 -0
  334. package/dist/theme/cd-tokens.d.ts.map +1 -0
  335. package/dist/theme/cd-tokens.js +64 -0
  336. package/dist/theme/cd-tokens.js.map +1 -0
  337. package/dist/theme/create-theme.d.ts +214 -39
  338. package/dist/theme/create-theme.d.ts.map +1 -1
  339. package/dist/theme/create-theme.js +204 -13
  340. package/dist/theme/create-theme.js.map +1 -1
  341. package/dist/theme/fonts.d.ts +38 -0
  342. package/dist/theme/fonts.d.ts.map +1 -0
  343. package/dist/theme/fonts.js +56 -0
  344. package/dist/theme/fonts.js.map +1 -0
  345. package/dist/theme/host-fonts.d.ts +39 -0
  346. package/dist/theme/host-fonts.d.ts.map +1 -0
  347. package/dist/theme/host-fonts.js +44 -0
  348. package/dist/theme/host-fonts.js.map +1 -0
  349. package/dist/theme/index.d.ts +8 -1
  350. package/dist/theme/index.d.ts.map +1 -1
  351. package/dist/theme/index.js +5 -1
  352. package/dist/theme/index.js.map +1 -1
  353. package/dist/theme/portal-container.d.ts +16 -0
  354. package/dist/theme/portal-container.d.ts.map +1 -0
  355. package/dist/theme/portal-container.js +19 -0
  356. package/dist/theme/portal-container.js.map +1 -0
  357. package/dist/theme/presets.d.ts +24 -9
  358. package/dist/theme/presets.d.ts.map +1 -1
  359. package/dist/theme/presets.js +44 -11
  360. package/dist/theme/presets.js.map +1 -1
  361. package/dist/theme/theme-provider.d.ts +27 -9
  362. package/dist/theme/theme-provider.d.ts.map +1 -1
  363. package/dist/theme/theme-provider.js +42 -20
  364. package/dist/theme/theme-provider.js.map +1 -1
  365. package/package.json +11 -5
  366. package/src/app/adapt-data-widget.tsx +2 -42
  367. package/src/app/builder/BuilderBanners.tsx +26 -13
  368. package/src/app/builder/CanvasRow.tsx +79 -32
  369. package/src/app/builder/CatalogueSheet.tsx +58 -35
  370. package/src/app/builder/CellPropsSheet.tsx +17 -2
  371. package/src/app/builder/DiscardDialog.tsx +59 -0
  372. package/src/app/builder/EmptyHint.tsx +16 -11
  373. package/src/app/builder/LayoutSurface.tsx +207 -67
  374. package/src/app/builder/PipelineStrip.tsx +265 -156
  375. package/src/app/builder/SaveDialog.tsx +25 -14
  376. package/src/app/builder/Toolbar.tsx +154 -57
  377. package/src/app/builder/Workspace.tsx +226 -56
  378. package/src/app/builder/builder-feedback.test.tsx +75 -0
  379. package/src/app/builder/builder-model.test.ts +5 -0
  380. package/src/app/builder/builder-model.ts +346 -6
  381. package/src/app/builder/builder-model.ux.test.ts +456 -0
  382. package/src/app/builder/builder-ux.test.tsx +844 -0
  383. package/src/app/builder/labels.ts +15 -121
  384. package/src/app/builder/reveal.ts +10 -0
  385. package/src/app/builder/status-chip.tsx +8 -4
  386. package/src/app/builder/use-builder-catalogue.ts +62 -19
  387. package/src/app/builder/use-builder-session.ts +68 -0
  388. package/src/app/builder/use-display-resolver.ts +24 -0
  389. package/src/app/builder/use-save-dashboard.ts +5 -0
  390. package/src/app/index.ts +20 -3
  391. package/src/app/layout-builder.tsx +193 -186
  392. package/src/app/mcp-app-view-chrome.test.tsx +487 -14
  393. package/src/app/mcp-app-view-chrome.tsx +328 -100
  394. package/src/app/mcp-app-view-states.tsx +140 -0
  395. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  396. package/src/app/mcp-app-view.test.tsx +478 -0
  397. package/src/app/mcp-app-view.tsx +447 -190
  398. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  399. package/src/app/mcp-toolkit-app.tsx +39 -8
  400. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  401. package/src/app/mcp-use-host-bridge.tsx +67 -24
  402. package/src/app/model-context.test.tsx +116 -0
  403. package/src/app/model-context.tsx +125 -0
  404. package/src/app/use-build-mode-focus.ts +62 -0
  405. package/src/app/use-view-actions.ts +89 -0
  406. package/src/app/use-view-lifecycle.ts +122 -0
  407. package/src/app/view-lifecycle.test.ts +443 -0
  408. package/src/app/view-lifecycle.ts +334 -0
  409. package/src/app/widget-fixture.effects.test.tsx +51 -0
  410. package/src/app/widget-fixture.render.test.tsx +81 -0
  411. package/src/app/widget-fixture.tsx +34 -11
  412. package/src/app/widget-props-sheet.tsx +67 -39
  413. package/src/app/widget-renderer.render.test.tsx +131 -0
  414. package/src/app/widget-renderer.tsx +136 -38
  415. package/src/components/DrillButton.tsx +24 -10
  416. package/src/components/EmptyState.tsx +83 -0
  417. package/src/components/ErrorState.tsx +204 -0
  418. package/src/components/FilterBar.test.tsx +82 -0
  419. package/src/components/FilterBar.tsx +56 -27
  420. package/src/components/GroupCard.tsx +3 -2
  421. package/src/components/KpiGrid.test.tsx +279 -0
  422. package/src/components/KpiGrid.tsx +329 -91
  423. package/src/components/ListFooter.test.tsx +116 -0
  424. package/src/components/ListFooter.tsx +110 -12
  425. package/src/components/PagedListFooter.test.tsx +140 -0
  426. package/src/components/PagedListFooter.tsx +107 -0
  427. package/src/components/Pills.test.tsx +77 -0
  428. package/src/components/Pills.tsx +78 -16
  429. package/src/components/SectionHeading.tsx +9 -3
  430. package/src/components/Toast.tsx +206 -0
  431. package/src/components/WidgetHeader.test.tsx +78 -0
  432. package/src/components/WidgetHeader.tsx +63 -16
  433. package/src/components/feedback-states.test.tsx +314 -0
  434. package/src/components/tone-figures.test.tsx +58 -0
  435. package/src/fonts.css +19 -0
  436. package/src/globals.css +9 -165
  437. package/src/hooks/index.ts +6 -3
  438. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  439. package/src/hooks/tool-result-recovery-model.ts +418 -0
  440. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  441. package/src/hooks/use-tool-query.ts +156 -33
  442. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  443. package/src/hooks/use-tool-result-recovery.test.ts +28 -5
  444. package/src/hooks/use-tool-result-recovery.ts +101 -131
  445. package/src/hooks/use-view-data.test.tsx +169 -0
  446. package/src/hooks/use-view-data.ts +131 -10
  447. package/src/hooks/use-view-tool-query.ts +25 -19
  448. package/src/i18n-coverage.test.tsx +425 -0
  449. package/src/index.ts +102 -13
  450. package/src/lib/focus-return.test.ts +288 -0
  451. package/src/lib/focus-return.ts +96 -0
  452. package/src/lib/focus-ring.ts +20 -0
  453. package/src/lib/icon-stroke.test.ts +59 -0
  454. package/src/lib/icon-stroke.ts +13 -0
  455. package/src/lib/parse-tool-result.test.ts +46 -1
  456. package/src/lib/parse-tool-result.ts +40 -2
  457. package/src/lib/toast-model.test.ts +233 -0
  458. package/src/lib/toast-model.ts +165 -0
  459. package/src/lib/tone-utils.test.ts +125 -16
  460. package/src/lib/tone-utils.ts +144 -15
  461. package/src/lib/tool-query.test.ts +569 -0
  462. package/src/lib/tool-query.ts +253 -0
  463. package/src/lib/tool-result-observer.test.ts +296 -0
  464. package/src/lib/tool-result-observer.ts +211 -0
  465. package/src/lib/typography.test.ts +45 -0
  466. package/src/lib/typography.ts +47 -0
  467. package/src/lib/utils.ts +12 -1
  468. package/src/locale/index.ts +14 -1
  469. package/src/locale/locale-provider.test.tsx +87 -0
  470. package/src/locale/locale-provider.tsx +90 -3
  471. package/src/locale/message-types.ts +337 -0
  472. package/src/locale/messages-de.ts +198 -0
  473. package/src/locale/messages-en.ts +196 -0
  474. package/src/locale/toolkit-messages.test.ts +349 -0
  475. package/src/locale/toolkit-messages.ts +143 -0
  476. package/src/no-hardcoded-copy.test.ts +432 -0
  477. package/src/primitives/alert.tsx +7 -2
  478. package/src/primitives/badge.tsx +19 -7
  479. package/src/primitives/button.tsx +13 -8
  480. package/src/primitives/card.tsx +4 -4
  481. package/src/primitives/dialog.tsx +42 -8
  482. package/src/primitives/input.tsx +5 -3
  483. package/src/primitives/scroll-area.tsx +1 -1
  484. package/src/primitives/select.tsx +7 -4
  485. package/src/primitives/sheet.tsx +33 -6
  486. package/src/primitives/skeleton.tsx +1 -1
  487. package/src/primitives/switch.tsx +4 -2
  488. package/src/primitives/tabs.tsx +9 -6
  489. package/src/providers/query-provider.test.tsx +154 -0
  490. package/src/providers/query-provider.tsx +66 -5
  491. package/src/render-cases-primitives.tsx +1 -1
  492. package/src/render-cases.tsx +82 -6
  493. package/src/render-effects.tsx +96 -0
  494. package/src/render-scan.ts +75 -0
  495. package/src/theme/cd-tokens.generated.css +48 -0
  496. package/src/theme/cd-tokens.test.ts +109 -0
  497. package/src/theme/cd-tokens.ts +67 -0
  498. package/src/theme/create-theme.test.ts +253 -5
  499. package/src/theme/create-theme.ts +341 -41
  500. package/src/theme/fonts.test.ts +85 -0
  501. package/src/theme/fonts.ts +72 -0
  502. package/src/theme/host-fonts.test.ts +135 -0
  503. package/src/theme/host-fonts.ts +52 -0
  504. package/src/theme/index.ts +15 -1
  505. package/src/theme/portal-container.ts +20 -0
  506. package/src/theme/presets.ts +44 -11
  507. package/src/theme/theme-css.test.ts +619 -0
  508. package/src/theme/theme-provider.test.tsx +82 -0
  509. package/src/theme/theme-provider.tsx +60 -24
  510. package/src/theme.css +344 -0
  511. package/ui-catalog.json +783 -49
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The plain logic behind `useToolQuery`, `useViewToolQuery`, `useViewData`
3
+ * and `useToolMutation` (toolkit#176 K18): the effective cache key and its
4
+ * collision guard, the opt-in retry policy for tool errors, the TanStack
5
+ * options a tool query runs with, and per-call mutation callbacks that fire
6
+ * for EVERY call. No React, no `mcp-use/react`, so it is unit-tested without
7
+ * a renderer.
8
+ */
9
+ import {
10
+ hashKey,
11
+ type MutateOptions,
12
+ type MutationFunctionContext,
13
+ type QueryClient,
14
+ type UseQueryOptions,
15
+ } from "@tanstack/react-query"
16
+ import { isToolError, parseToolResult } from "./parse-tool-result.js"
17
+
18
+ /** A `(name, args)` tool caller — the shape of `useCallTool()` and `HostBridge.callTool`. */
19
+ export type ToolCaller = (name: string, args: object) => Promise<unknown>
20
+
21
+ /**
22
+ * TanStack query options a tool query passes through: everything except the
23
+ * key and the fetcher, which the hooks build from the tool and its args
24
+ * (`retry`, `retryDelay`, `staleTime`, `placeholderData` / `keepPreviousData`,
25
+ * `initialData` + `initialDataUpdatedAt`, `enabled`, `select`, …).
26
+ */
27
+ export type UseToolQueryOptions<TData = unknown, TSelected = TData> = Omit<
28
+ UseQueryOptions<TData, Error, TSelected>,
29
+ "queryKey" | "queryFn"
30
+ >
31
+
32
+ /**
33
+ * Neither an `AppQueryProvider` `callTool` nor a `HostBridgeProvider` is in
34
+ * reach, so the query cannot call any tool. Never retried: nothing changes
35
+ * until the mount does.
36
+ */
37
+ export class NoToolTransportError extends Error {
38
+ constructor(toolName: string) {
39
+ super(
40
+ `[mcp-toolkit] cannot call "${toolName}": no tool transport. Mount the widget under ` +
41
+ "AppQueryProvider with callTool, or a HostBridgeProvider.",
42
+ )
43
+ this.name = "NoToolTransportError"
44
+ }
45
+ }
46
+
47
+ /** Retries {@link shouldRetryToolCall} grants a failed transport (TanStack's own default count). */
48
+ export const TOOL_QUERY_RETRIES = 3
49
+
50
+ /**
51
+ * A retry policy for tool queries: up to {@link TOOL_QUERY_RETRIES} retries
52
+ * for a failed transport, none for a tool's own `isError` answer
53
+ * (`isToolError`) or a missing transport — repeating a deterministic answer
54
+ * only repeats it and holds the error back about 7 s.
55
+ *
56
+ * Opt-in in 2.x (the hooks keep the client's `retry`, TanStack's 3 by
57
+ * default, because a tool may also answer a transient failure with
58
+ * `isError`): pass `retry: shouldRetryToolCall` to a hook, or set it as the
59
+ * client's default (`queryClient.setDefaultOptions`, or an `AppQueryProvider`
60
+ * `client` of your own).
61
+ */
62
+ export function shouldRetryToolCall(failureCount: number, error: unknown): boolean {
63
+ if (isToolError(error) || error instanceof NoToolTransportError) return false
64
+ return failureCount < TOOL_QUERY_RETRIES
65
+ }
66
+
67
+ /**
68
+ * The effective cache key of a tool query (toolkit#176 K18).
69
+ *
70
+ * - 2.x default: the caller's key, then the args (`[...queryKey, args]`), so
71
+ * two callers sharing a key with different args get separate entries.
72
+ * - `toolNameInKey`: the caller's key, the tool name, then the args
73
+ * (`[...queryKey, toolName, args]`), so two tools never share an entry,
74
+ * whatever key their callers pass. Opt-in in 2.x (`AppQueryProvider` /
75
+ * `McpAppView` `toolNameInQueryKey`), the default from the next major.
76
+ *
77
+ * Build exact keys (`getQueryData`, `setQueryData`, `cancelQueries` with
78
+ * `exact: true`) with this helper, or with `useToolQueryKey`, which reads the
79
+ * provider's setting, rather than by hand: code that goes through them keeps
80
+ * working when the default changes. Prefix invalidation by `queryKey`
81
+ * matches either way.
82
+ */
83
+ export function toolQueryKey(
84
+ queryKey: readonly unknown[],
85
+ toolName: string,
86
+ args: object,
87
+ toolNameInKey = false,
88
+ ): unknown[] {
89
+ return toolNameInKey ? [...queryKey, toolName, args] : [...queryKey, args]
90
+ }
91
+
92
+ /**
93
+ * The tool that last claimed each cache key, per client — the guard against
94
+ * two tools sharing one entry (their answers would overwrite each other).
95
+ */
96
+ const keyOwners = new WeakMap<QueryClient, Map<string, string>>()
97
+
98
+ /**
99
+ * Claim the cache key `keyId` (`hashKey` of a {@link toolQueryKey}) for
100
+ * `toolName` on `client`. Returns the tool that held it
101
+ * before when that is ANOTHER tool — a collision the hooks report — else
102
+ * `null`. The latest claim wins, so each collision is reported once per
103
+ * switch.
104
+ */
105
+ export function claimToolQueryKey(
106
+ client: QueryClient,
107
+ keyId: string,
108
+ toolName: string,
109
+ ): string | null {
110
+ const owners = keyOwners.get(client) ?? new Map<string, string>()
111
+ keyOwners.set(client, owners)
112
+ const previous = owners.get(keyId)
113
+ owners.set(keyId, toolName)
114
+ return previous !== undefined && previous !== toolName ? previous : null
115
+ }
116
+
117
+ /** The console line for a {@link claimToolQueryKey} collision. */
118
+ export function toolKeyCollisionMessage(keyId: string, toolName: string, previous: string): string {
119
+ return (
120
+ `[mcp-toolkit] the query key ${keyId} is used by "${previous}" and "${toolName}": ` +
121
+ "both share one cache entry and overwrite each other's answer. Give each tool its own queryKey."
122
+ )
123
+ }
124
+
125
+ /** How {@link toolQueryOptions} decodes a result and builds the key. */
126
+ export interface ToolQueryBuild<TData> {
127
+ /** Decodes the tool result. Default: `parseToolResult` (structured-first). */
128
+ decode?: (result: unknown) => TData
129
+ /** Put the tool name into the key (see {@link toolQueryKey}). Default `false`. */
130
+ toolNameInKey?: boolean
131
+ }
132
+
133
+ /**
134
+ * The TanStack options of a tool query: the caller's options, the
135
+ * {@link toolQueryKey} and a fetcher that calls `toolName` and decodes the
136
+ * result. `retry` stays the caller's, else the client's default; only a
137
+ * missing transport is never retried. Without a transport the fetcher fails
138
+ * with {@link NoToolTransportError} instead of leaving the query idle.
139
+ */
140
+ export function toolQueryOptions<TData, TSelected = TData>(
141
+ queryKey: readonly unknown[],
142
+ toolName: string,
143
+ args: object,
144
+ callTool: ToolCaller | undefined,
145
+ opts?: UseToolQueryOptions<TData, TSelected>,
146
+ build: ToolQueryBuild<TData> = {},
147
+ ): UseQueryOptions<TData, Error, TSelected> {
148
+ const decode = build.decode ?? ((result: unknown) => parseToolResult<TData>(result))
149
+ return {
150
+ ...opts,
151
+ // An omitted `retry` must stay omitted: an explicit `undefined` would
152
+ // shadow the client's default.
153
+ ...(callTool ? null : { retry: false }),
154
+ queryKey: toolQueryKey(queryKey, toolName, args, build.toolNameInKey),
155
+ queryFn: async () => {
156
+ if (!callTool) throw new NoToolTransportError(toolName)
157
+ return decode(await callTool(toolName, args))
158
+ },
159
+ }
160
+ }
161
+
162
+ /**
163
+ * The content of the payload last seeded per query key, per client: a
164
+ * remounted or re-rendered OLD payload must not roll a refetched answer
165
+ * back, while a payload not seeded before is a new delivery and wins.
166
+ */
167
+ const lastSeeded = new WeakMap<QueryClient, Map<string, string>>()
168
+
169
+ function contentOf(value: unknown): string | undefined {
170
+ try {
171
+ return JSON.stringify(value)
172
+ } catch {
173
+ return undefined
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Seed `queryKey` with host-delivered `seed`: written when the entry is
179
+ * empty, or when the seed is a payload this client has not seeded for the
180
+ * key before (a new delivery). Returns whether the cache was written.
181
+ */
182
+ export function seedToolQuery(
183
+ client: QueryClient,
184
+ queryKey: readonly unknown[],
185
+ seed: unknown,
186
+ updatedAt?: number,
187
+ ): boolean {
188
+ const keyId = hashKey(queryKey)
189
+ const seeded = lastSeeded.get(client) ?? new Map<string, string>()
190
+ lastSeeded.set(client, seeded)
191
+ const content = contentOf(seed)
192
+ const delivered = content === undefined || seeded.get(keyId) !== content
193
+ if (content !== undefined) seeded.set(keyId, content)
194
+ if (!delivered && client.getQueryData(queryKey) !== undefined) return false
195
+ client.setQueryData(queryKey, seed, updatedAt === undefined ? undefined : { updatedAt })
196
+ return true
197
+ }
198
+
199
+ /** Callbacks of one `mutate` call, typed as TanStack's per-call options. */
200
+ export type PerCallCallbacks<TData, TVariables> = MutateOptions<TData, Error, TVariables, unknown>
201
+
202
+ function settle(run: () => void): void {
203
+ try {
204
+ run()
205
+ } catch (err) {
206
+ // Surface a throwing callback as an unhandled rejection, as TanStack
207
+ // does, without skipping the callbacks after it.
208
+ void Promise.reject(err instanceof Error ? err : new Error(String(err)))
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Fire one call's per-call `onSuccess` / `onError` / `onSettled` once `call`
214
+ * settles — for EVERY call. TanStack's own `mutate` keeps only the latest
215
+ * call's callbacks, so two quick calls (retrying two incidents in a row)
216
+ * would confirm only the second. `context()` is read when the call settles:
217
+ * `null` (the component unmounted) skips the callbacks, as TanStack does.
218
+ * A rejected `call` is handled here, never left unhandled.
219
+ */
220
+ export function settleEachCall<TData, TVariables>(
221
+ call: Promise<TData>,
222
+ variables: TVariables,
223
+ callbacks: PerCallCallbacks<TData, TVariables> | undefined,
224
+ context: () => MutationFunctionContext | null,
225
+ ): void {
226
+ call.then(
227
+ (data) => {
228
+ const ctx = callbacks ? context() : null
229
+ if (!callbacks || !ctx) return
230
+ settle(() => callbacks.onSuccess?.(data, variables, undefined, ctx))
231
+ settle(() => callbacks.onSettled?.(data, null, variables, undefined, ctx))
232
+ },
233
+ (error: Error) => {
234
+ const ctx = callbacks ? context() : null
235
+ if (!callbacks || !ctx) return
236
+ settle(() => callbacks.onError?.(error, variables, undefined, ctx))
237
+ settle(() => callbacks.onSettled?.(undefined, error, variables, undefined, ctx))
238
+ },
239
+ )
240
+ }
241
+
242
+ /**
243
+ * Invalidate the query keys (prefixes) a mutation names: matching ACTIVE
244
+ * queries refetch now, inactive ones are only marked stale and refetch when
245
+ * they are next used — never a refetch of every inactive cached page.
246
+ */
247
+ export async function invalidateToolQueries(
248
+ client: QueryClient,
249
+ keys: readonly (readonly unknown[])[] | undefined,
250
+ ): Promise<void> {
251
+ if (!keys) return
252
+ await Promise.all(keys.map((queryKey) => client.invalidateQueries({ queryKey })))
253
+ }
@@ -0,0 +1,296 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import {
3
+ classifyHostMessage,
4
+ classifyToolResult,
5
+ createToolResultObserver,
6
+ EMPTY_TOOL_OBSERVATION,
7
+ installToolResultObserver,
8
+ reduceToolObservation,
9
+ type HostLifecycleMessage,
10
+ type ToolObservation,
11
+ } from "./tool-result-observer.js"
12
+
13
+ /**
14
+ * The passive observer behind the stripped-result recovery (toolkit#176 K13,
15
+ * N21): it must tell a stripped result from a structured one and from "not
16
+ * yet", read only the COMPLETE tool input, and never act on foreign traffic.
17
+ */
18
+
19
+ const rpc = (method: string, params?: Record<string, unknown>) => ({
20
+ jsonrpc: "2.0",
21
+ method,
22
+ ...(params ? { params } : {}),
23
+ })
24
+
25
+ /** The host's answer to `ui/initialize`, as ext-apps sends it. */
26
+ const HANDSHAKE = {
27
+ jsonrpc: "2.0",
28
+ id: 1,
29
+ result: {
30
+ protocolVersion: "2026-01-26",
31
+ hostInfo: { name: "host", version: "1" },
32
+ hostCapabilities: { serverTools: {} },
33
+ hostContext: { toolInfo: { tool: { name: "render-view" } } },
34
+ },
35
+ }
36
+
37
+ describe("classifyToolResult (mirrors the mcp-use 2.x latch)", () => {
38
+ it("an isError result is an error, whatever else it carries", () => {
39
+ expect(classifyToolResult({ isError: true })).toBe("error")
40
+ expect(classifyToolResult({ isError: true, structuredContent: { a: 1 } })).toBe("error")
41
+ })
42
+
43
+ it("a result with structuredContent — even null — is structured", () => {
44
+ expect(classifyToolResult({ structuredContent: { a: 1 } })).toBe("structured")
45
+ expect(classifyToolResult({ structuredContent: null })).toBe("structured")
46
+ })
47
+
48
+ it("a content-only result is the stripped one claude.ai sends", () => {
49
+ expect(classifyToolResult({ content: [{ type: "text", text: "Rendered." }] })).toBe("stripped")
50
+ expect(classifyToolResult({})).toBe("stripped")
51
+ expect(classifyToolResult({ isError: false })).toBe("stripped")
52
+ })
53
+ })
54
+
55
+ describe("classifyHostMessage", () => {
56
+ it("recognises the host's ui/initialize answer by its capabilities or protocol version", () => {
57
+ expect(classifyHostMessage(HANDSHAKE)).toEqual({ kind: "handshake" })
58
+ expect(
59
+ classifyHostMessage({ jsonrpc: "2.0", id: 7, result: { protocolVersion: "x" } }),
60
+ ).toEqual({ kind: "handshake" })
61
+ expect(
62
+ classifyHostMessage({ jsonrpc: "2.0", id: 7, result: { hostCapabilities: {} } }),
63
+ ).toEqual({ kind: "handshake" })
64
+ })
65
+
66
+ it("ignores every other response (a tools/call answer, an error, a bare result)", () => {
67
+ expect(
68
+ classifyHostMessage({ jsonrpc: "2.0", id: 2, result: { structuredContent: {} } }),
69
+ ).toBeNull()
70
+ expect(classifyHostMessage({ jsonrpc: "2.0", id: 2, error: { code: 1 } })).toBeNull()
71
+ expect(classifyHostMessage({ jsonrpc: "2.0", id: 2, result: "x" })).toBeNull()
72
+ expect(
73
+ classifyHostMessage({ jsonrpc: "2.0", result: { protocolVersion: "x" } }),
74
+ "a response needs an id",
75
+ ).toBeNull()
76
+ expect(
77
+ classifyHostMessage({ jsonrpc: "2.0", id: 3, result: { protocolVersion: 1 } }),
78
+ ).toBeNull()
79
+ })
80
+
81
+ it("reads the complete tool input, never the partial stream", () => {
82
+ expect(
83
+ classifyHostMessage(rpc("ui/notifications/tool-input", { arguments: { id: 1 } })),
84
+ ).toEqual({ kind: "tool-input", arguments: { id: 1 } })
85
+ expect(
86
+ classifyHostMessage(rpc("ui/notifications/tool-input-partial", { arguments: { id: 1 } })),
87
+ ).toBeNull()
88
+ })
89
+
90
+ it("a tool input without usable arguments is an empty argument set", () => {
91
+ expect(classifyHostMessage(rpc("ui/notifications/tool-input"))).toEqual({
92
+ kind: "tool-input",
93
+ arguments: {},
94
+ })
95
+ expect(
96
+ classifyHostMessage(rpc("ui/notifications/tool-input", { arguments: ["not", "a", "map"] })),
97
+ ).toEqual({ kind: "tool-input", arguments: {} })
98
+ })
99
+
100
+ it("classifies the tool result and the cancellation", () => {
101
+ expect(classifyHostMessage(rpc("ui/notifications/tool-result", { content: [] }))).toEqual({
102
+ kind: "tool-result",
103
+ result: "stripped",
104
+ })
105
+ expect(
106
+ classifyHostMessage(rpc("ui/notifications/tool-result", { structuredContent: {} })),
107
+ ).toEqual({ kind: "tool-result", result: "structured" })
108
+ expect(classifyHostMessage(rpc("ui/notifications/tool-result"))).toEqual({
109
+ kind: "tool-result",
110
+ result: "stripped",
111
+ })
112
+ expect(classifyHostMessage(rpc("ui/notifications/tool-cancelled"))).toEqual({
113
+ kind: "tool-cancelled",
114
+ })
115
+ })
116
+
117
+ it("ignores foreign traffic and other methods", () => {
118
+ expect(classifyHostMessage(null)).toBeNull()
119
+ expect(classifyHostMessage("ui/notifications/tool-result")).toBeNull()
120
+ expect(classifyHostMessage([HANDSHAKE])).toBeNull()
121
+ expect(classifyHostMessage({ method: "ui/notifications/tool-result" })).toBeNull()
122
+ expect(classifyHostMessage({ ...HANDSHAKE, jsonrpc: "1.0" })).toBeNull()
123
+ expect(classifyHostMessage(rpc("ui/notifications/host-context-changed", {}))).toBeNull()
124
+ expect(classifyHostMessage(rpc("ui/resource-teardown", {}))).toBeNull()
125
+ })
126
+ })
127
+
128
+ describe("reduceToolObservation", () => {
129
+ const fold = (...messages: HostLifecycleMessage[]): ToolObservation =>
130
+ messages.reduce(reduceToolObservation, EMPTY_TOOL_OBSERVATION)
131
+
132
+ it("starts empty", () => {
133
+ expect(EMPTY_TOOL_OBSERVATION).toEqual({
134
+ sawHandshake: false,
135
+ completeInput: undefined,
136
+ result: "none",
137
+ cancelled: false,
138
+ })
139
+ })
140
+
141
+ it("records the handshake, the latest complete input and the result", () => {
142
+ expect(
143
+ fold(
144
+ { kind: "handshake" },
145
+ { kind: "tool-input", arguments: { a: 1 } },
146
+ { kind: "tool-input", arguments: { a: 2 } },
147
+ { kind: "tool-result", result: "stripped" },
148
+ ),
149
+ ).toEqual({ sawHandshake: true, completeInput: { a: 2 }, result: "stripped", cancelled: false })
150
+ })
151
+
152
+ it("settles once, like the mcp-use latch: nothing changes a delivered call", () => {
153
+ const settled = fold(
154
+ { kind: "tool-input", arguments: { a: 1 } },
155
+ { kind: "tool-result", result: "structured" },
156
+ )
157
+ expect(reduceToolObservation(settled, { kind: "tool-input", arguments: { a: 9 } })).toBe(
158
+ settled,
159
+ )
160
+ expect(reduceToolObservation(settled, { kind: "tool-result", result: "stripped" })).toBe(
161
+ settled,
162
+ )
163
+ expect(reduceToolObservation(settled, { kind: "tool-cancelled" })).toBe(settled)
164
+ })
165
+
166
+ it("a cancellation settles the call too", () => {
167
+ const cancelled = fold({ kind: "tool-cancelled" })
168
+ expect(cancelled.cancelled).toBe(true)
169
+ expect(reduceToolObservation(cancelled, { kind: "tool-result", result: "stripped" })).toBe(
170
+ cancelled,
171
+ )
172
+ })
173
+
174
+ it("a repeated handshake is the same state; a late one still counts after a result", () => {
175
+ const shaken = fold({ kind: "handshake" })
176
+ expect(reduceToolObservation(shaken, { kind: "handshake" })).toBe(shaken)
177
+ const late = fold({ kind: "tool-result", result: "stripped" }, { kind: "handshake" })
178
+ expect(late).toMatchObject({ sawHandshake: true, result: "stripped" })
179
+ })
180
+ })
181
+
182
+ describe("createToolResultObserver", () => {
183
+ const post = (target: EventTarget, data: unknown, source: unknown = null) => {
184
+ const event = new MessageEvent("message", { data })
185
+ Object.defineProperty(event, "source", { value: source })
186
+ target.dispatchEvent(event)
187
+ }
188
+
189
+ it("folds the messages it accepts and notifies only on a change", () => {
190
+ const target = new EventTarget()
191
+ const observer = createToolResultObserver(target)
192
+ const listener = vi.fn()
193
+ observer.subscribe(listener)
194
+ post(target, HANDSHAKE)
195
+ post(target, rpc("ui/notifications/tool-input", { arguments: { id: 7 } }))
196
+ post(target, rpc("ui/notifications/tool-input-partial", { arguments: { id: 8 } }))
197
+ post(target, { unrelated: true })
198
+ post(target, rpc("ui/notifications/tool-result", { content: [] }))
199
+ post(target, rpc("ui/notifications/tool-result", { structuredContent: {} }))
200
+ expect(observer.getSnapshot()).toEqual({
201
+ sawHandshake: true,
202
+ completeInput: { id: 7 },
203
+ result: "stripped",
204
+ cancelled: false,
205
+ })
206
+ expect(listener).toHaveBeenCalledTimes(3)
207
+ })
208
+
209
+ it("drops what the filter refuses (another frame's traffic)", () => {
210
+ const target = new EventTarget()
211
+ const host = { name: "host" }
212
+ const observer = createToolResultObserver(target, (event) => event.source === host)
213
+ post(target, HANDSHAKE, { name: "intruder" })
214
+ post(target, rpc("ui/notifications/tool-result", { content: [] }), null)
215
+ expect(observer.getSnapshot()).toBe(EMPTY_TOOL_OBSERVATION)
216
+ post(target, HANDSHAKE, host)
217
+ expect(observer.getSnapshot().sawHandshake).toBe(true)
218
+ })
219
+
220
+ it("unsubscribes a listener, and dispose stops listening but keeps the last snapshot", () => {
221
+ const target = new EventTarget()
222
+ const observer = createToolResultObserver(target)
223
+ const kept = vi.fn()
224
+ const dropped = vi.fn()
225
+ observer.subscribe(kept)
226
+ const unsubscribe = observer.subscribe(dropped)
227
+ unsubscribe()
228
+ post(target, HANDSHAKE)
229
+ expect(kept).toHaveBeenCalledTimes(1)
230
+ expect(dropped).not.toHaveBeenCalled()
231
+ observer.dispose()
232
+ post(target, rpc("ui/notifications/tool-result", { content: [] }))
233
+ expect(observer.getSnapshot()).toMatchObject({ sawHandshake: true, result: "none" })
234
+ expect(kept).toHaveBeenCalledTimes(1)
235
+ })
236
+ })
237
+
238
+ describe("installToolResultObserver", () => {
239
+ it("is null outside a framed browser document (no host to observe)", () => {
240
+ expect(installToolResultObserver()).toBeNull()
241
+ })
242
+
243
+ it("is null in a top-level window (no host frames the view)", async () => {
244
+ vi.resetModules()
245
+ const target = new EventTarget()
246
+ vi.stubGlobal("window", Object.assign(target, { parent: target }))
247
+ try {
248
+ const fresh = await import("./tool-result-observer.js")
249
+ expect(fresh.installToolResultObserver()).toBeNull()
250
+ } finally {
251
+ vi.unstubAllGlobals()
252
+ vi.resetModules()
253
+ }
254
+ })
255
+
256
+ it("listens from the moment the bundle evaluates, before any explicit install", async () => {
257
+ vi.resetModules()
258
+ const target = new EventTarget()
259
+ const parent = { name: "host" }
260
+ vi.stubGlobal("window", Object.assign(target, { parent }))
261
+ try {
262
+ const fresh = await import("./tool-result-observer.js")
263
+ post(target, HANDSHAKE, parent)
264
+ expect(fresh.installToolResultObserver()?.getSnapshot().sawHandshake).toBe(true)
265
+ } finally {
266
+ vi.unstubAllGlobals()
267
+ vi.resetModules()
268
+ }
269
+ })
270
+
271
+ it("installs once in a framed document and listens to the parent only", async () => {
272
+ vi.resetModules()
273
+ const target = new EventTarget()
274
+ const parent = { name: "host" }
275
+ vi.stubGlobal("window", Object.assign(target, { parent }))
276
+ try {
277
+ const fresh = await import("./tool-result-observer.js")
278
+ const observer = fresh.installToolResultObserver()
279
+ expect(observer).not.toBeNull()
280
+ expect(fresh.installToolResultObserver(), "idempotent").toBe(observer)
281
+ post(target, HANDSHAKE, { name: "other" })
282
+ expect(observer?.getSnapshot().sawHandshake).toBe(false)
283
+ post(target, HANDSHAKE, parent)
284
+ expect(observer?.getSnapshot().sawHandshake).toBe(true)
285
+ } finally {
286
+ vi.unstubAllGlobals()
287
+ vi.resetModules()
288
+ }
289
+ })
290
+
291
+ function post(target: EventTarget, data: unknown, source: unknown) {
292
+ const event = new MessageEvent("message", { data })
293
+ Object.defineProperty(event, "source", { value: source })
294
+ target.dispatchEvent(event)
295
+ }
296
+ })