@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
@@ -1,41 +1,55 @@
1
- import { useEffect, useRef, useState } from "react"
2
- import { parseToolResult } from "../lib/parse-tool-result.js"
1
+ import { useEffect, useState, useSyncExternalStore } from "react"
2
+ import { installToolResultObserver, type ToolResultObserver } from "../lib/tool-result-observer.js"
3
+ import {
4
+ createRecoveryController,
5
+ type RecoveryPolicy,
6
+ type ToolResultRecoveryState,
7
+ } from "./tool-result-recovery-model.js"
8
+
9
+ export { decodeRecoveredResult } from "./tool-result-recovery-model.js"
10
+ export type {
11
+ RecoveryBlockReason,
12
+ RecoveryPolicy,
13
+ ToolResultRecoveryState,
14
+ ToolSafety,
15
+ } from "./tool-result-recovery-model.js"
3
16
 
4
17
  /**
5
18
  * Recovery for hosts that deliver the `ui/notifications/tool-result`
6
19
  * notification without `structuredContent` (observed on claude.ai and Claude
7
- * Desktop, which forward only the model-facing `content` blocks). Since
8
- * mcp-use 2.x such a result is not even observable: the `useToolContext`
9
- * latch ignores content-only successes, so the tool status stays `"pending"`
10
- * forever and the shell would idle on its loading skeleton.
20
+ * Desktop, which forward only the model-facing `content` blocks). mcp-use
21
+ * 2.x's `useToolContext` latch ignores such a content-only result, so its
22
+ * status stays `"pending"` forever and the shell would idle on its skeleton.
11
23
  *
12
- * Because the stripped notification is invisible, readiness is inferred two
13
- * ways: the hard `resultReady` signal (the latch fired but the payload is
14
- * invalid), or a grace timer — once the originating tool is known and no
15
- * result has latched within
16
- * {@link ToolResultRecoveryOptions.assumeStrippedAfterMs}, the result is
17
- * assumed delivered-but-stripped. Either way the hook re-executes the
18
- * originating tool once via the host bridge's official `callTool` — tool-call
19
- * RESPONSES carry `structuredContent` intact on those hosts — and exposes the
20
- * decoded payload. Spec-conforming hosts (e.g. the mcp-use inspector) deliver
21
- * a valid payload up front, so the recovery never fires there.
24
+ * The hook re-executes the originating tool once via the host bridge's
25
+ * `callTool` — tool-call RESPONSES carry `structuredContent` intact on those
26
+ * hosts — and exposes the decoded payload. When it does so (toolkit#176):
22
27
  *
23
- * A call the host cancelled is never recovered: since mcp-use 2.7.1 the
24
- * cancellation latches as `status: "error"` (a `ToolCancelledError`), which
25
- * would otherwise count as a ready-but-invalid result — re-executing it would
26
- * override the user's decision to stop the call.
28
+ * - **Only when the host stripped the result.** A passive observer of the
29
+ * host's lifecycle notifications (`installToolResultObserver`) sees the
30
+ * content-only result itself, so the recovery fires as it arrives, and a
31
+ * slow tool on a compliant host is never re-run (K13): no result has
32
+ * arrived because none was sent. Where the observer cannot vouch for that
33
+ * (no browser frame, or installed after the handshake), the 2.x grace timer
34
+ * ({@link ToolResultRecoveryOptions.assumeStrippedAfterMs}) still infers a
35
+ * stripped result, once the call's arguments arrived.
36
+ * - **With the call's complete arguments** (N21): the observer's complete
37
+ * `tool-input`, never a partial stream; an equal re-delivery is the same run.
38
+ * - **Never for a cancelled or failed call:** re-running repeats the failure
39
+ * or overrides the user's stop; the shell renders that state and offers an
40
+ * explicit retry.
41
+ * - **Only for a tool the policy allows** ({@link ToolResultRecoveryOptions.policy}):
42
+ * by default never one the host reports as writing (`readOnlyHint: false`
43
+ * or `destructiveHint: true` in `hostContext.toolInfo.tool.annotations`).
44
+ * A blocked recovery is reported (`blocked`) so the view can show its error.
27
45
  *
28
- * Assumes the originating tool is a read-only render pipeline (true for all
29
- * toolkit `render-view`/`show_*` tools) — the re-execution must be safe to
30
- * repeat, and may rarely run concurrently with the original invocation when
31
- * the grace timer beats a slow but conforming host.
46
+ * The decision and its timer live in `createRecoveryController`; this hook
47
+ * only binds it to React.
32
48
  */
33
49
 
34
50
  /**
35
- * Default grace period before a silent latch counts as a stripped result.
36
- * Long enough for a conforming host to latch a typical render tool first,
37
- * short enough that users on stripping hosts are not left staring at the
38
- * skeleton noticeably.
51
+ * Default grace period before a silent latch counts as a stripped result —
52
+ * used only where the transport observer cannot vouch that nothing arrived.
39
53
  */
40
54
  export const DEFAULT_ASSUME_STRIPPED_AFTER_MS = 2500
41
55
 
@@ -43,8 +57,7 @@ export interface ToolResultRecoveryOptions {
43
57
  /**
44
58
  * The latch fired (`useToolContext().status !== "pending"`) — a structured
45
59
  * result, a tool error, or a host cancellation arrived. On hosts that strip
46
- * `structuredContent` this never turns true; the grace timer below covers
47
- * that case.
60
+ * `structuredContent` this never turns true.
48
61
  */
49
62
  resultReady: boolean
50
63
  /**
@@ -53,12 +66,17 @@ export interface ToolResultRecoveryOptions {
53
66
  * attempt already in flight is dropped. Defaults to `false`.
54
67
  */
55
68
  cancelled?: boolean
56
- /** The payload the host delivered (`useToolContext().toolOutput`). */
57
- props: unknown
58
69
  /**
59
- * Validates a candidate payload. MUST be referentially stable (module-level
60
- * function or memoised) — it is an effect dependency.
70
+ * The originating call latched a tool error (`useToolContext().status ===
71
+ * "error"` without a cancellation): an `isError` result or a failed call.
72
+ * Suppresses the recovery exactly like {@link cancelled} — the shell shows
73
+ * the error instead of silently re-running a call that just failed
74
+ * (toolkit#176). Defaults to `false`.
61
75
  */
76
+ errored?: boolean
77
+ /** The payload the host delivered (`useToolContext().toolOutput`). */
78
+ props: unknown
79
+ /** Validates a candidate payload. */
62
80
  isValid: (value: unknown) => boolean
63
81
  /** The tool arguments from `ui/notifications/tool-input` (`useToolContext().toolInput`). */
64
82
  toolInput: Record<string, unknown> | null | undefined
@@ -68,23 +86,21 @@ export interface ToolResultRecoveryOptions {
68
86
  callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>
69
87
  /**
70
88
  * Grace period in ms after which a silent latch counts as a stripped
71
- * result. Armed once the originating tool is known while `resultReady` is
72
- * still false; re-armed on every `toolInput` update, because streaming
73
- * arguments mean the invocation may not have finished yet. Defaults to
89
+ * result. Applies only where the transport observer cannot vouch that no
90
+ * result arrived (see above); restarts for new arguments. Defaults to
74
91
  * {@link DEFAULT_ASSUME_STRIPPED_AFTER_MS}.
75
92
  */
76
93
  assumeStrippedAfterMs?: number
94
+ /** Which tools may be re-executed on their own. Default `"auto"`. */
95
+ policy?: RecoveryPolicy
96
+ /**
97
+ * The transport observer. Defaults to the document's
98
+ * (`installToolResultObserver()`); `null` disables it, leaving the grace
99
+ * timer to decide alone (the 2.x behaviour).
100
+ */
101
+ observer?: ToolResultObserver | null
77
102
  }
78
103
 
79
- export interface ToolResultRecoveryState<T> {
80
- /** The recovered payload, `null` until the re-execution resolved (or when it failed). */
81
- data: T | null
82
- /** `true` when the single recovery attempt failed — the shell keeps its skeleton, no retry loop. */
83
- failed: boolean
84
- }
85
-
86
- const IDLE: ToolResultRecoveryState<never> = { data: null, failed: false }
87
-
88
104
  /**
89
105
  * Resolves the originating tool name from the host context's `toolInfo`.
90
106
  * Reads the SEP-1865 shape first (`toolInfo.tool.name`), then the flattened
@@ -108,20 +124,19 @@ export function resolveRecoveryToolName(hostContext: unknown): string | undefine
108
124
  }
109
125
 
110
126
  /**
111
- * Decodes a re-executed tool result and validates it. Exposed for tests;
112
- * the hook uses it on the `callTool` response.
127
+ * Whether a latched or assumed-stripped result may be recovered at all: the
128
+ * call was neither cancelled nor failed. Kept for existing imports; the full
129
+ * rule (observer, policy, arguments) is `decideRecovery`.
130
+ * @deprecated Use `decideRecovery` from the recovery model.
113
131
  */
114
- export function decodeRecoveredResult<T>(
115
- result: unknown,
116
- isValid: (value: unknown) => boolean,
117
- ): T | null {
118
- let decoded: unknown
119
- try {
120
- decoded = parseToolResult<unknown>(result)
121
- } catch {
122
- return null
123
- }
124
- return decoded !== null && isValid(decoded) ? (decoded as T) : null
132
+ export function isRecoveryReady(state: {
133
+ resultReady: boolean
134
+ assumedReady: boolean
135
+ cancelled: boolean
136
+ errored: boolean
137
+ }): boolean {
138
+ if (state.cancelled || state.errored) return false
139
+ return state.resultReady || state.assumedReady
125
140
  }
126
141
 
127
142
  export function useToolResultRecovery<T>(
@@ -130,86 +145,41 @@ export function useToolResultRecovery<T>(
130
145
  const {
131
146
  resultReady,
132
147
  cancelled = false,
148
+ errored = false,
133
149
  props,
134
150
  isValid,
135
151
  toolInput,
136
152
  hostContext,
137
153
  callTool,
138
154
  assumeStrippedAfterMs = DEFAULT_ASSUME_STRIPPED_AFTER_MS,
155
+ policy = "auto",
156
+ observer,
139
157
  } = options
140
- const [state, setState] = useState<ToolResultRecoveryState<T>>(IDLE)
141
- // Single-flight: the promise ref survives StrictMode's dev-mode
142
- // effect re-run (cleanup + setup on the same instance), so the re-run
143
- // re-subscribes to the SAME in-flight call instead of firing a second one.
144
- const flightRef = useRef<Promise<ToolResultRecoveryState<T>> | null>(null)
145
- const flightInputRef = useRef<unknown>(null)
146
-
147
- const toolName = resolveRecoveryToolName(hostContext)
148
-
149
- // A stripped result is unobservable (the 2.x latch ignores content-only
150
- // results), so infer it: once a recovery could fire but the latch has been
151
- // silent for the grace period, treat the result as delivered-but-stripped.
152
- // Re-armed on every toolInput identity change — streaming arguments mean
153
- // the invocation may still be running, so the grace period starts over.
154
- // The re-arm is a render-phase reset (not an effect) so the cascading-
155
- // render lint stays satisfied; the timer effect below restarts anyway
156
- // because toolInput is one of its dependencies.
157
- const [assumedReady, setAssumedReady] = useState(false)
158
- const [prevTimerInput, setPrevTimerInput] = useState<unknown>(toolInput)
159
- if (prevTimerInput !== toolInput) {
160
- setPrevTimerInput(toolInput)
161
- if (assumedReady) setAssumedReady(false)
162
- }
158
+ // One controller per mount; it survives StrictMode's effect re-run, so an
159
+ // in-flight call is never fired twice.
160
+ const [controller] = useState(() =>
161
+ createRecoveryController<T>({
162
+ observer: observer === undefined ? installToolResultObserver() : observer,
163
+ }),
164
+ )
163
165
  useEffect(() => {
164
- if (resultReady || cancelled || toolName === undefined) return
165
- const timer = setTimeout(() => setAssumedReady(true), assumeStrippedAfterMs)
166
- return () => clearTimeout(timer)
167
- }, [resultReady, cancelled, toolName, toolInput, assumeStrippedAfterMs])
168
-
169
- // A cancelled call never counts as ready, so the effect below invalidates
170
- // any in-flight attempt the same way it does for a superseded run.
171
- const ready = !cancelled && (resultReady || assumedReady)
172
-
166
+ controller.start()
167
+ return () => controller.stop()
168
+ }, [controller])
173
169
  useEffect(() => {
174
- // A new tool run (result no longer ready, or fresh tool-input identity)
175
- // invalidates any previous recovery attempt and re-arms the single-flight.
176
- if (flightRef.current && (!ready || flightInputRef.current !== toolInput)) {
177
- flightRef.current = null
178
- setState((prev) => (prev.data === null && !prev.failed ? prev : IDLE))
179
- }
180
- // Never fire on conforming hosts (valid payload) or before the
181
- // originating tool is known.
182
- if (!ready || isValid(props) || toolName === undefined) return
183
-
184
- if (!flightRef.current) {
185
- flightInputRef.current = toolInput
186
- flightRef.current = (async (): Promise<ToolResultRecoveryState<T>> => {
187
- try {
188
- const result = await callTool(toolName, toolInput ?? {})
189
- const data = decodeRecoveredResult<T>(result, isValid)
190
- if (data !== null) return { data, failed: false }
191
- console.warn(
192
- `[mcp-toolkit] tool-result recovery: re-executing "${toolName}" returned no usable payload`,
193
- )
194
- return { data: null, failed: true }
195
- } catch (err) {
196
- console.warn(`[mcp-toolkit] tool-result recovery for "${toolName}" failed:`, err)
197
- return { data: null, failed: true }
198
- }
199
- })()
200
- }
201
-
202
- const flight = flightRef.current
203
- let stale = false
204
- void flight.then((outcome) => {
205
- // Drop the outcome when this subscription was superseded or the flight
206
- // was invalidated by a newer tool run.
207
- if (!stale && flightRef.current === flight) setState(outcome)
170
+ controller.update({
171
+ resultReady,
172
+ cancelled,
173
+ errored,
174
+ props,
175
+ isValid,
176
+ toolInput,
177
+ hostContext,
178
+ callTool,
179
+ policy,
180
+ graceMs: assumeStrippedAfterMs,
181
+ resolveToolName: resolveRecoveryToolName,
208
182
  })
209
- return () => {
210
- stale = true
211
- }
212
- }, [ready, props, isValid, toolName, toolInput, callTool])
213
-
214
- return state
183
+ })
184
+ return useSyncExternalStore(controller.subscribe, controller.getState, controller.getState)
215
185
  }
@@ -0,0 +1,169 @@
1
+ import { QueryClient } from "@tanstack/react-query"
2
+ import { renderToStaticMarkup } from "react-dom/server"
3
+ import { describe, expect, it, vi } from "vitest"
4
+ import { toolQueryKey } from "../lib/tool-query.js"
5
+ import { AppQueryProvider } from "../providers/query-provider.js"
6
+ import { useViewData, type UseViewDataOptions, type ViewDataResult } from "./use-view-data.js"
7
+
8
+ type Demo = { n: number }
9
+ type CallTool = (name: string, args: object) => Promise<unknown>
10
+
11
+ const KEY = ["use-view-data.test"] as const
12
+ const ARGS = { id: 1 }
13
+
14
+ /** Renders a probe once (SSR, the house path) and hands back the hook's result. */
15
+ function probe(
16
+ initialData: Demo | null,
17
+ ready: boolean,
18
+ callTool: CallTool,
19
+ key: string,
20
+ options?: UseViewDataOptions<Demo> & { client?: QueryClient; toolNameInQueryKey?: boolean },
21
+ ): ViewDataResult<Demo> {
22
+ let result: ViewDataResult<Demo> | undefined
23
+ const { client, toolNameInQueryKey, ...hookOptions } = options ?? {}
24
+ function Probe() {
25
+ result = useViewData<Demo>(initialData, [...KEY, key], "demo_data", ARGS, ready, hookOptions)
26
+ return null
27
+ }
28
+ renderToStaticMarkup(
29
+ <AppQueryProvider callTool={callTool} client={client} toolNameInQueryKey={toolNameInQueryKey}>
30
+ <Probe />
31
+ </AppQueryProvider>,
32
+ )
33
+ if (!result) throw new Error("probe did not render")
34
+ return result
35
+ }
36
+
37
+ describe("useViewData", () => {
38
+ it("returns host-pushed data verbatim without fetching", () => {
39
+ const callTool = vi.fn<CallTool>(() => Promise.resolve({}))
40
+ const result = probe({ n: 1 }, true, callTool, "pushed")
41
+ expect(result.data).toEqual({ n: 1 })
42
+ expect(result.loading).toBe(false)
43
+ expect(result.error).toBeNull()
44
+ expect(callTool).not.toHaveBeenCalled()
45
+ })
46
+
47
+ it("is not loading before it is ready", () => {
48
+ const result = probe(null, false, vi.fn<CallTool>(), "not-ready")
49
+ expect(result).toMatchObject({ data: null, loading: false, error: null })
50
+ })
51
+
52
+ it("exposes the query's progress: failureCount, failureReason, isFetching, isPlaceholderData", () => {
53
+ const result = probe(null, false, vi.fn<CallTool>(), "progress")
54
+ expect(result).toMatchObject({
55
+ failureCount: 0,
56
+ failureReason: null,
57
+ isFetching: false,
58
+ isPlaceholderData: false,
59
+ })
60
+ })
61
+
62
+ it("refetch calls the tool again: the retry an error state offers", async () => {
63
+ const callTool = vi.fn<CallTool>(() => Promise.resolve({ structuredContent: { n: 2 } }))
64
+ const result = probe(null, false, callTool, "refetch")
65
+ expect(callTool).not.toHaveBeenCalled()
66
+ result.refetch()
67
+ await vi.waitFor(() => {
68
+ expect(callTool).toHaveBeenCalledWith("demo_data", { id: 1 })
69
+ })
70
+ })
71
+
72
+ it("enabled: false gates the feed next to ready; a ready feed without data is loading", () => {
73
+ const client = new QueryClient()
74
+ expect(probe(null, true, vi.fn<CallTool>(), "gated", { client, enabled: false }).loading).toBe(
75
+ false,
76
+ )
77
+ expect(probe(null, true, vi.fn<CallTool>(), "open", { client }).loading).toBe(true)
78
+ })
79
+
80
+ it("a function gate that says no idles like enabled: false, never an endless skeleton", () => {
81
+ const client = new QueryClient()
82
+ const closed = probe(null, true, vi.fn<CallTool>(), "fn-closed", {
83
+ client,
84
+ enabled: () => false,
85
+ })
86
+ expect(closed).toMatchObject({ data: null, loading: false, error: null })
87
+ const open = probe(null, true, vi.fn<CallTool>(), "fn-open", { client, enabled: () => true })
88
+ expect(open.loading).toBe(true)
89
+ // Not ready keeps it off whatever the gate says.
90
+ expect(
91
+ probe(null, false, vi.fn<CallTool>(), "fn-not-ready", { client, enabled: () => true })
92
+ .loading,
93
+ ).toBe(false)
94
+ })
95
+
96
+ it("passes TanStack's options through: initialData-free placeholderData shows meanwhile", () => {
97
+ const client = new QueryClient()
98
+ const result = probe(null, true, vi.fn<CallTool>(), "placeholder", {
99
+ client,
100
+ placeholderData: { n: 0 },
101
+ })
102
+ expect(result).toMatchObject({ data: { n: 0 }, isPlaceholderData: true, loading: false })
103
+ })
104
+ })
105
+
106
+ describe('useViewData initialDataMode: "seed" (the live standalone view)', () => {
107
+ const cacheKey = (key: string) => toolQueryKey([...KEY, key], "demo_data", ARGS)
108
+
109
+ it("seeds the feed's cache entry with the host's payload and returns it", () => {
110
+ const client = new QueryClient()
111
+ const result = probe({ n: 1 }, true, vi.fn<CallTool>(), "seeded", {
112
+ client,
113
+ initialDataMode: "seed",
114
+ initialDataUpdatedAt: 1234,
115
+ })
116
+ expect(result).toMatchObject({ data: { n: 1 }, loading: false })
117
+ expect(client.getQueryData(cacheKey("seeded"))).toEqual({ n: 1 })
118
+ expect(client.getQueryState(cacheKey("seeded"))?.dataUpdatedAt).toBe(1234)
119
+ })
120
+
121
+ it("with toolNameInQueryKey the seed lands under the key carrying the tool name", () => {
122
+ const client = new QueryClient()
123
+ const result = probe({ n: 1 }, true, vi.fn<CallTool>(), "tool-key", {
124
+ client,
125
+ initialDataMode: "seed",
126
+ toolNameInQueryKey: true,
127
+ })
128
+ expect(result.data).toEqual({ n: 1 })
129
+ expect(client.getQueryData([...KEY, "tool-key", "demo_data", ARGS])).toEqual({ n: 1 })
130
+ expect(client.getQueryData(cacheKey("tool-key")), "never the 2.x key").toBeUndefined()
131
+ })
132
+
133
+ it("static mode never writes the cache", () => {
134
+ const client = new QueryClient()
135
+ probe({ n: 1 }, true, vi.fn<CallTool>(), "static", { client })
136
+ expect(client.getQueryData(cacheKey("static"))).toBeUndefined()
137
+ })
138
+
139
+ it("the cached answer wins over the seed it came from once the feed answered anew", () => {
140
+ const client = new QueryClient()
141
+ probe({ n: 1 }, true, vi.fn<CallTool>(), "refetched", { client, initialDataMode: "seed" })
142
+ client.setQueryData(cacheKey("refetched"), { n: 2 })
143
+ const remounted = probe({ n: 1 }, true, vi.fn<CallTool>(), "refetched", {
144
+ client,
145
+ initialDataMode: "seed",
146
+ initialDataUpdatedAt: () => 99,
147
+ })
148
+ expect(remounted.data, "an old payload never rolls the answer back").toEqual({ n: 2 })
149
+ const delivered = probe({ n: 3 }, true, vi.fn<CallTool>(), "refetched", {
150
+ client,
151
+ initialDataMode: "seed",
152
+ })
153
+ expect(delivered.data, "a new delivery wins").toEqual({ n: 3 })
154
+ })
155
+
156
+ it("refetch over a seeded entry reads the feed again", async () => {
157
+ const client = new QueryClient()
158
+ const callTool = vi.fn<CallTool>(() => Promise.resolve({ structuredContent: { n: 2 } }))
159
+ const result = probe({ n: 1 }, true, callTool, "seed-refetch", {
160
+ client,
161
+ initialDataMode: "seed",
162
+ })
163
+ result.refetch()
164
+ await vi.waitFor(() => {
165
+ expect(client.getQueryData(cacheKey("seed-refetch"))).toEqual({ n: 2 })
166
+ })
167
+ expect(callTool).toHaveBeenCalledWith("demo_data", ARGS)
168
+ })
169
+ })
@@ -1,22 +1,74 @@
1
- import { useToolQuery } from "./use-tool-query.js"
1
+ import { useCallback, useState } from "react"
2
+ import { hashKey, useQueryClient } from "@tanstack/react-query"
3
+ import { seedToolQuery, type UseToolQueryOptions } from "../lib/tool-query.js"
4
+ import { useToolQuery, useToolQueryKey } from "./use-tool-query.js"
2
5
 
3
6
  export interface ViewDataResult<T> {
4
7
  data: T | null
5
8
  /** True while self-fetching with no data yet — the first paint of a cockpit cell. */
6
9
  loading: boolean
7
10
  error: Error | null
11
+ /**
12
+ * Fetch `tool` again now: the retry of an error state
13
+ * (`<ErrorState onRetry={refetch} … />`) or a manual refresh — a new read
14
+ * even while a failed fetch still waits on its retry backoff. In `"static"`
15
+ * mode host-pushed data, when present, stays what `data` returns.
16
+ */
17
+ refetch: () => void
18
+ /** Failed attempts of the current fetch (0 once it succeeds). */
19
+ failureCount: number
20
+ /**
21
+ * The current fetch's last failed attempt — set from the first failure on,
22
+ * while the query still retries (`error` waits for the last retry).
23
+ */
24
+ failureReason: Error | null
25
+ /** A fetch is in flight (the first one or a refetch). */
26
+ isFetching: boolean
27
+ /**
28
+ * `data` is a placeholder (`placeholderData`, e.g. `keepPreviousData`):
29
+ * the previous args' answer while the current args' fetch runs or failed.
30
+ */
31
+ isPlaceholderData: boolean
32
+ }
33
+
34
+ /**
35
+ * How {@link useViewData} treats host-pushed `initialData`.
36
+ *
37
+ * - `"static"` (default): returned verbatim; the feed query stays off while
38
+ * it is present.
39
+ * - `"seed"`: it becomes the feed query's cached answer, and the query stays
40
+ * live (once `ready`): an invalidation after a write, a `refetch` or a
41
+ * retry re-reads the feed. A fresh seed is not refetched on mount (it is
42
+ * younger than `staleTime`); a payload delivered anew (content not seeded
43
+ * for the key before) replaces the cached answer, while a re-rendered or
44
+ * remounted old one never rolls a refetched answer back.
45
+ */
46
+ export type ViewDataMode = "static" | "seed"
47
+
48
+ /**
49
+ * Options of {@link useViewData}: TanStack's query options for the feed
50
+ * (`retry`, `retryDelay`, `staleTime`, `placeholderData` /
51
+ * `keepPreviousData`, `enabled` as an extra gate next to `ready`, …),
52
+ * plus how host data seeds it.
53
+ */
54
+ export type UseViewDataOptions<T> = Omit<UseToolQueryOptions<T>, "initialData" | "select"> & {
55
+ initialDataMode?: ViewDataMode
8
56
  }
9
57
 
10
58
  /**
11
59
  * The seam that lets one widget component serve both modes without a duplicate UI.
12
60
  *
13
61
  * Standalone (a `*_show_*` widget) the agent's tool result is handed in via
14
- * `initialData` (from the view's stepData, e.g. through `adaptDataWidget`) and
15
- * returned verbatim. Embedded in a self-fetching cockpit only scope params are
16
- * passed, so the component fetches `tool` with `args` under `key` — disabled
17
- * unless data is absent AND `ready` (i.e. the required id/key is present).
18
- * Sibling widgets sharing `key` dedupe to a single call, so a composed view of
19
- * N widgets over one data feed fetches once.
62
+ * `initialData` (from the view's stepData, e.g. through `adaptDataWidget`).
63
+ * Embedded in a self-fetching cockpit only scope params are passed, so the
64
+ * component fetches `tool` with `args` under `key`, gated on `ready` (i.e.
65
+ * the required id/key is present). Sibling widgets sharing `key` and `args`
66
+ * dedupe to a single call, so a composed view of N widgets over one data
67
+ * feed fetches once.
68
+ *
69
+ * By default host data is returned verbatim and switches the query off;
70
+ * `initialDataMode: "seed"` keeps the query live over it instead (see
71
+ * {@link ViewDataMode}). `options` passes TanStack's query options through.
20
72
  */
21
73
  export function useViewData<T>(
22
74
  initialData: T | null | undefined,
@@ -24,8 +76,77 @@ export function useViewData<T>(
24
76
  tool: string,
25
77
  args: Record<string, unknown>,
26
78
  ready: boolean,
79
+ options?: UseViewDataOptions<T>,
27
80
  ): ViewDataResult<T> {
28
- const query = useToolQuery<T>([...key], tool, args, { enabled: !initialData && ready })
29
- const data = initialData ?? query.data ?? null
30
- return { data, loading: !data && ready && !query.isError, error: query.error ?? null }
81
+ const { initialDataMode = "static", enabled, ...queryOptions } = options ?? {}
82
+ const seeding = initialDataMode === "seed"
83
+ const cacheKey = useStableKey(useToolQueryKey(key, tool, args))
84
+ useSeed(seeding ? initialData : null, cacheKey, queryOptions.initialDataUpdatedAt)
85
+
86
+ const active = ready && enabled !== false
87
+ const fetches = active && (seeding || !initialData)
88
+ const query = useToolQuery<T>([...key], tool, args, {
89
+ ...queryOptions,
90
+ enabled: typeof enabled === "function" ? (q) => fetches && enabled(q) : fetches,
91
+ })
92
+ const data = seeding ? (query.data ?? initialData) : (initialData ?? query.data)
93
+ const refetch = useFreshRefetch(cacheKey, query.refetch)
94
+ return {
95
+ data: data ?? null,
96
+ // The query's own verdict: a function `enabled` that returns false idles
97
+ // like `enabled: false`, never an endless skeleton.
98
+ loading: !data && query.isEnabled && !query.isError,
99
+ error: query.error,
100
+ refetch,
101
+ failureCount: query.failureCount,
102
+ failureReason: query.failureReason,
103
+ isFetching: query.isFetching,
104
+ isPlaceholderData: query.isPlaceholderData,
105
+ }
106
+ }
107
+
108
+ /** `key`, kept by content: callers build `key`/`args` anew every render. */
109
+ function useStableKey(key: unknown[]): unknown[] {
110
+ const id = hashKey(key)
111
+ const [stable, setStable] = useState({ id, key })
112
+ if (stable.id !== id) setStable({ id, key })
113
+ return stable.id === id ? stable.key : key
114
+ }
115
+
116
+ /**
117
+ * `"seed"` mode: write the host's payload into the feed's cache entry in the
118
+ * render phase, before the query's observer reads the cache — what TanStack's
119
+ * own `initialData` does (see {@link seedToolQuery} for when it wins over a
120
+ * cached answer). The last processed seed skips the content check while the
121
+ * same object comes in render after render.
122
+ */
123
+ function useSeed(
124
+ seed: unknown,
125
+ cacheKey: unknown[],
126
+ updatedAt: number | (() => number | undefined) | undefined,
127
+ ): void {
128
+ const client = useQueryClient()
129
+ const keyId = hashKey(cacheKey)
130
+ const [seen, setSeen] = useState<{ seed: unknown; keyId: string } | null>(null)
131
+ if (seed == null || (seen?.seed === seed && seen.keyId === keyId)) return
132
+ const at = typeof updatedAt === "function" ? updatedAt() : updatedAt
133
+ const written = seedToolQuery(client, cacheKey, seed, at)
134
+ // Only a real change re-renders: an equal new object per render must not loop.
135
+ if (written || seen?.keyId !== keyId) setSeen({ seed, keyId })
136
+ }
137
+
138
+ /**
139
+ * A refetch that reads anew at once. TanStack restarts an in-flight fetch only
140
+ * over data (`cancelRefetch`); a key without data joins the running retry
141
+ * chain and waits out its backoff. So cancel that chain first, then read.
142
+ */
143
+ function useFreshRefetch(cacheKey: unknown[], refetchQuery: () => Promise<unknown>): () => void {
144
+ const client = useQueryClient()
145
+ return useCallback(() => {
146
+ if (client.getQueryData(cacheKey) !== undefined) {
147
+ void refetchQuery()
148
+ return
149
+ }
150
+ void client.cancelQueries({ queryKey: cacheKey, exact: true }).then(() => refetchQuery())
151
+ }, [client, cacheKey, refetchQuery])
31
152
  }