@miragon/mcp-toolkit-ui 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (387) hide show
  1. package/README.md +7 -4
  2. package/dist/app/adapt-data-widget.d.ts +1 -22
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -1
  4. package/dist/app/adapt-data-widget.js +1 -35
  5. package/dist/app/adapt-data-widget.js.map +1 -1
  6. package/dist/app/builder/BuilderBanners.js +1 -1
  7. package/dist/app/builder/BuilderBanners.js.map +1 -1
  8. package/dist/app/builder/CanvasRow.d.ts +4 -2
  9. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  10. package/dist/app/builder/CanvasRow.js +21 -7
  11. package/dist/app/builder/CanvasRow.js.map +1 -1
  12. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  13. package/dist/app/builder/CatalogueSheet.js +11 -4
  14. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  15. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  16. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.js +10 -3
  18. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  19. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  20. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  21. package/dist/app/builder/DiscardDialog.js +18 -0
  22. package/dist/app/builder/DiscardDialog.js.map +1 -0
  23. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  24. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  25. package/dist/app/builder/LayoutSurface.js +74 -23
  26. package/dist/app/builder/LayoutSurface.js.map +1 -1
  27. package/dist/app/builder/PipelineStrip.d.ts +2 -1
  28. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  29. package/dist/app/builder/PipelineStrip.js +26 -13
  30. package/dist/app/builder/PipelineStrip.js.map +1 -1
  31. package/dist/app/builder/SaveDialog.d.ts +9 -1
  32. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  33. package/dist/app/builder/SaveDialog.js +8 -2
  34. package/dist/app/builder/SaveDialog.js.map +1 -1
  35. package/dist/app/builder/Toolbar.d.ts +12 -4
  36. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  37. package/dist/app/builder/Toolbar.js +18 -7
  38. package/dist/app/builder/Toolbar.js.map +1 -1
  39. package/dist/app/builder/Workspace.d.ts +23 -3
  40. package/dist/app/builder/Workspace.d.ts.map +1 -1
  41. package/dist/app/builder/Workspace.js +58 -8
  42. package/dist/app/builder/Workspace.js.map +1 -1
  43. package/dist/app/builder/builder-model.d.ts +143 -1
  44. package/dist/app/builder/builder-model.d.ts.map +1 -1
  45. package/dist/app/builder/builder-model.js +191 -1
  46. package/dist/app/builder/builder-model.js.map +1 -1
  47. package/dist/app/builder/reveal.d.ts +10 -0
  48. package/dist/app/builder/reveal.d.ts.map +1 -0
  49. package/dist/app/builder/reveal.js +10 -0
  50. package/dist/app/builder/reveal.js.map +1 -0
  51. package/dist/app/builder/use-builder-catalogue.d.ts +4 -1
  52. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  53. package/dist/app/builder/use-builder-catalogue.js +44 -17
  54. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  55. package/dist/app/builder/use-builder-session.d.ts +30 -0
  56. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  57. package/dist/app/builder/use-builder-session.js +58 -0
  58. package/dist/app/builder/use-builder-session.js.map +1 -0
  59. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  60. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  61. package/dist/app/builder/use-display-resolver.js +16 -0
  62. package/dist/app/builder/use-display-resolver.js.map +1 -0
  63. package/dist/app/index.d.ts +8 -3
  64. package/dist/app/index.d.ts.map +1 -1
  65. package/dist/app/index.js +8 -2
  66. package/dist/app/index.js.map +1 -1
  67. package/dist/app/layout-builder.d.ts +13 -25
  68. package/dist/app/layout-builder.d.ts.map +1 -1
  69. package/dist/app/layout-builder.js +75 -81
  70. package/dist/app/layout-builder.js.map +1 -1
  71. package/dist/app/mcp-app-view-chrome.d.ts +12 -3
  72. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  73. package/dist/app/mcp-app-view-chrome.js +21 -5
  74. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  75. package/dist/app/mcp-app-view-states.d.ts +11 -3
  76. package/dist/app/mcp-app-view-states.d.ts.map +1 -1
  77. package/dist/app/mcp-app-view-states.js +29 -8
  78. package/dist/app/mcp-app-view-states.js.map +1 -1
  79. package/dist/app/mcp-app-view.d.ts +33 -1
  80. package/dist/app/mcp-app-view.d.ts.map +1 -1
  81. package/dist/app/mcp-app-view.js +181 -127
  82. package/dist/app/mcp-app-view.js.map +1 -1
  83. package/dist/app/mcp-toolkit-app.d.ts +6 -1
  84. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  85. package/dist/app/mcp-toolkit-app.js +8 -1
  86. package/dist/app/mcp-toolkit-app.js.map +1 -1
  87. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  88. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  89. package/dist/app/mcp-use-host-bridge.js +51 -22
  90. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  91. package/dist/app/model-context.d.ts +71 -0
  92. package/dist/app/model-context.d.ts.map +1 -0
  93. package/dist/app/model-context.js +101 -0
  94. package/dist/app/model-context.js.map +1 -0
  95. package/dist/app/use-build-mode-focus.d.ts +33 -0
  96. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  97. package/dist/app/use-build-mode-focus.js +59 -0
  98. package/dist/app/use-build-mode-focus.js.map +1 -0
  99. package/dist/app/use-view-actions.d.ts +31 -0
  100. package/dist/app/use-view-actions.d.ts.map +1 -0
  101. package/dist/app/use-view-actions.js +81 -0
  102. package/dist/app/use-view-actions.js.map +1 -0
  103. package/dist/app/use-view-lifecycle.d.ts +4 -0
  104. package/dist/app/use-view-lifecycle.d.ts.map +1 -1
  105. package/dist/app/use-view-lifecycle.js +2 -0
  106. package/dist/app/use-view-lifecycle.js.map +1 -1
  107. package/dist/app/view-lifecycle.d.ts +80 -4
  108. package/dist/app/view-lifecycle.d.ts.map +1 -1
  109. package/dist/app/view-lifecycle.js +90 -3
  110. package/dist/app/view-lifecycle.js.map +1 -1
  111. package/dist/app/widget-fixture.d.ts +10 -3
  112. package/dist/app/widget-fixture.d.ts.map +1 -1
  113. package/dist/app/widget-fixture.js +21 -8
  114. package/dist/app/widget-fixture.js.map +1 -1
  115. package/dist/app/widget-props-sheet.d.ts +3 -1
  116. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  117. package/dist/app/widget-props-sheet.js +22 -10
  118. package/dist/app/widget-props-sheet.js.map +1 -1
  119. package/dist/app/widget-renderer.js +1 -1
  120. package/dist/app/widget-renderer.js.map +1 -1
  121. package/dist/components/DrillButton.d.ts +9 -6
  122. package/dist/components/DrillButton.d.ts.map +1 -1
  123. package/dist/components/DrillButton.js +8 -6
  124. package/dist/components/DrillButton.js.map +1 -1
  125. package/dist/components/EmptyState.d.ts.map +1 -1
  126. package/dist/components/EmptyState.js +4 -2
  127. package/dist/components/EmptyState.js.map +1 -1
  128. package/dist/components/ErrorState.d.ts +5 -2
  129. package/dist/components/ErrorState.d.ts.map +1 -1
  130. package/dist/components/ErrorState.js +25 -8
  131. package/dist/components/ErrorState.js.map +1 -1
  132. package/dist/components/FilterBar.d.ts +15 -4
  133. package/dist/components/FilterBar.d.ts.map +1 -1
  134. package/dist/components/FilterBar.js +11 -6
  135. package/dist/components/FilterBar.js.map +1 -1
  136. package/dist/components/KpiGrid.d.ts +66 -21
  137. package/dist/components/KpiGrid.d.ts.map +1 -1
  138. package/dist/components/KpiGrid.js +101 -34
  139. package/dist/components/KpiGrid.js.map +1 -1
  140. package/dist/components/ListFooter.d.ts +31 -4
  141. package/dist/components/ListFooter.d.ts.map +1 -1
  142. package/dist/components/ListFooter.js +30 -11
  143. package/dist/components/ListFooter.js.map +1 -1
  144. package/dist/components/PagedListFooter.d.ts +57 -0
  145. package/dist/components/PagedListFooter.d.ts.map +1 -0
  146. package/dist/components/PagedListFooter.js +27 -0
  147. package/dist/components/PagedListFooter.js.map +1 -0
  148. package/dist/components/Pills.d.ts +27 -7
  149. package/dist/components/Pills.d.ts.map +1 -1
  150. package/dist/components/Pills.js +21 -6
  151. package/dist/components/Pills.js.map +1 -1
  152. package/dist/components/SectionHeading.d.ts +3 -1
  153. package/dist/components/SectionHeading.d.ts.map +1 -1
  154. package/dist/components/SectionHeading.js +6 -2
  155. package/dist/components/SectionHeading.js.map +1 -1
  156. package/dist/components/Toast.d.ts +9 -3
  157. package/dist/components/Toast.d.ts.map +1 -1
  158. package/dist/components/Toast.js +31 -16
  159. package/dist/components/Toast.js.map +1 -1
  160. package/dist/components/WidgetHeader.d.ts +34 -10
  161. package/dist/components/WidgetHeader.d.ts.map +1 -1
  162. package/dist/components/WidgetHeader.js +11 -6
  163. package/dist/components/WidgetHeader.js.map +1 -1
  164. package/dist/hooks/index.d.ts +4 -4
  165. package/dist/hooks/index.d.ts.map +1 -1
  166. package/dist/hooks/index.js +1 -1
  167. package/dist/hooks/index.js.map +1 -1
  168. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  169. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  170. package/dist/hooks/tool-result-recovery-model.js +273 -0
  171. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  172. package/dist/hooks/use-tool-query.d.ts +56 -14
  173. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  174. package/dist/hooks/use-tool-query.js +102 -25
  175. package/dist/hooks/use-tool-query.js.map +1 -1
  176. package/dist/hooks/use-tool-result-recovery.d.ts +47 -52
  177. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  178. package/dist/hooks/use-tool-result-recovery.js +58 -120
  179. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  180. package/dist/hooks/use-view-data.d.ts +52 -10
  181. package/dist/hooks/use-view-data.d.ts.map +1 -1
  182. package/dist/hooks/use-view-data.js +81 -16
  183. package/dist/hooks/use-view-data.js.map +1 -1
  184. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  185. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  186. package/dist/hooks/use-view-tool-query.js +15 -14
  187. package/dist/hooks/use-view-tool-query.js.map +1 -1
  188. package/dist/index.d.ts +17 -10
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +18 -6
  191. package/dist/index.js.map +1 -1
  192. package/dist/lib/focus-return.d.ts +61 -0
  193. package/dist/lib/focus-return.d.ts.map +1 -0
  194. package/dist/lib/focus-return.js +46 -0
  195. package/dist/lib/focus-return.js.map +1 -0
  196. package/dist/lib/parse-tool-result.d.ts +23 -1
  197. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  198. package/dist/lib/parse-tool-result.js +38 -2
  199. package/dist/lib/parse-tool-result.js.map +1 -1
  200. package/dist/lib/toast-model.d.ts +41 -1
  201. package/dist/lib/toast-model.d.ts.map +1 -1
  202. package/dist/lib/toast-model.js +67 -1
  203. package/dist/lib/toast-model.js.map +1 -1
  204. package/dist/lib/tone-utils.d.ts +58 -6
  205. package/dist/lib/tone-utils.d.ts.map +1 -1
  206. package/dist/lib/tone-utils.js +66 -4
  207. package/dist/lib/tone-utils.js.map +1 -1
  208. package/dist/lib/tool-query.d.ts +107 -0
  209. package/dist/lib/tool-query.d.ts.map +1 -0
  210. package/dist/lib/tool-query.js +182 -0
  211. package/dist/lib/tool-query.js.map +1 -0
  212. package/dist/lib/tool-result-observer.d.ts +108 -0
  213. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  214. package/dist/lib/tool-result-observer.js +158 -0
  215. package/dist/lib/tool-result-observer.js.map +1 -0
  216. package/dist/lib/typography.d.ts +37 -0
  217. package/dist/lib/typography.d.ts.map +1 -0
  218. package/dist/lib/typography.js +41 -0
  219. package/dist/lib/typography.js.map +1 -0
  220. package/dist/lib/utils.d.ts.map +1 -1
  221. package/dist/lib/utils.js +11 -1
  222. package/dist/lib/utils.js.map +1 -1
  223. package/dist/locale/index.d.ts +1 -1
  224. package/dist/locale/index.d.ts.map +1 -1
  225. package/dist/locale/message-types.d.ts +117 -4
  226. package/dist/locale/message-types.d.ts.map +1 -1
  227. package/dist/locale/messages-de.d.ts.map +1 -1
  228. package/dist/locale/messages-de.js +44 -10
  229. package/dist/locale/messages-de.js.map +1 -1
  230. package/dist/locale/messages-en.d.ts.map +1 -1
  231. package/dist/locale/messages-en.js +44 -10
  232. package/dist/locale/messages-en.js.map +1 -1
  233. package/dist/locale/toolkit-messages.d.ts +19 -1
  234. package/dist/locale/toolkit-messages.d.ts.map +1 -1
  235. package/dist/locale/toolkit-messages.js +54 -1
  236. package/dist/locale/toolkit-messages.js.map +1 -1
  237. package/dist/primitives/alert.d.ts +1 -1
  238. package/dist/primitives/alert.d.ts.map +1 -1
  239. package/dist/primitives/alert.js +2 -0
  240. package/dist/primitives/alert.js.map +1 -1
  241. package/dist/primitives/badge.d.ts +1 -1
  242. package/dist/primitives/badge.d.ts.map +1 -1
  243. package/dist/primitives/badge.js +9 -4
  244. package/dist/primitives/badge.js.map +1 -1
  245. package/dist/primitives/button.d.ts +1 -1
  246. package/dist/primitives/button.js +1 -1
  247. package/dist/primitives/button.js.map +1 -1
  248. package/dist/primitives/dialog.d.ts +1 -1
  249. package/dist/primitives/dialog.d.ts.map +1 -1
  250. package/dist/primitives/dialog.js +10 -3
  251. package/dist/primitives/dialog.js.map +1 -1
  252. package/dist/primitives/select.js +1 -1
  253. package/dist/primitives/select.js.map +1 -1
  254. package/dist/primitives/sheet.d.ts +1 -1
  255. package/dist/primitives/sheet.d.ts.map +1 -1
  256. package/dist/primitives/sheet.js +6 -2
  257. package/dist/primitives/sheet.js.map +1 -1
  258. package/dist/providers/query-provider.d.ts +32 -1
  259. package/dist/providers/query-provider.d.ts.map +1 -1
  260. package/dist/providers/query-provider.js +53 -4
  261. package/dist/providers/query-provider.js.map +1 -1
  262. package/dist/render-cases-primitives.js +1 -1
  263. package/dist/render-cases-primitives.js.map +1 -1
  264. package/dist/render-cases.d.ts.map +1 -1
  265. package/dist/render-cases.js +30 -6
  266. package/dist/render-cases.js.map +1 -1
  267. package/dist/render-effects.d.ts +42 -0
  268. package/dist/render-effects.d.ts.map +1 -0
  269. package/dist/render-effects.js +81 -0
  270. package/dist/render-effects.js.map +1 -0
  271. package/dist/theme/create-theme.d.ts +19 -1
  272. package/dist/theme/create-theme.d.ts.map +1 -1
  273. package/dist/theme/create-theme.js +15 -0
  274. package/dist/theme/create-theme.js.map +1 -1
  275. package/package.json +5 -4
  276. package/src/app/adapt-data-widget.tsx +2 -42
  277. package/src/app/builder/BuilderBanners.tsx +1 -1
  278. package/src/app/builder/CanvasRow.tsx +63 -17
  279. package/src/app/builder/CatalogueSheet.tsx +50 -29
  280. package/src/app/builder/CellPropsSheet.tsx +17 -2
  281. package/src/app/builder/DiscardDialog.tsx +59 -0
  282. package/src/app/builder/LayoutSurface.tsx +206 -66
  283. package/src/app/builder/PipelineStrip.tsx +253 -153
  284. package/src/app/builder/SaveDialog.tsx +14 -2
  285. package/src/app/builder/Toolbar.tsx +144 -59
  286. package/src/app/builder/Workspace.tsx +226 -58
  287. package/src/app/builder/builder-model.ts +341 -2
  288. package/src/app/builder/builder-model.ux.test.ts +456 -0
  289. package/src/app/builder/builder-ux.test.tsx +844 -0
  290. package/src/app/builder/reveal.ts +10 -0
  291. package/src/app/builder/use-builder-catalogue.ts +56 -19
  292. package/src/app/builder/use-builder-session.ts +68 -0
  293. package/src/app/builder/use-display-resolver.ts +24 -0
  294. package/src/app/index.ts +12 -2
  295. package/src/app/layout-builder.tsx +178 -191
  296. package/src/app/mcp-app-view-chrome.test.tsx +141 -1
  297. package/src/app/mcp-app-view-chrome.tsx +56 -6
  298. package/src/app/mcp-app-view-states.tsx +51 -11
  299. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  300. package/src/app/mcp-app-view.test.tsx +222 -2
  301. package/src/app/mcp-app-view.tsx +267 -117
  302. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  303. package/src/app/mcp-toolkit-app.tsx +8 -1
  304. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  305. package/src/app/mcp-use-host-bridge.tsx +67 -24
  306. package/src/app/model-context.test.tsx +116 -0
  307. package/src/app/model-context.tsx +125 -0
  308. package/src/app/use-build-mode-focus.ts +62 -0
  309. package/src/app/use-view-actions.ts +89 -0
  310. package/src/app/use-view-lifecycle.ts +6 -0
  311. package/src/app/view-lifecycle.test.ts +216 -0
  312. package/src/app/view-lifecycle.ts +165 -5
  313. package/src/app/widget-fixture.effects.test.tsx +51 -0
  314. package/src/app/widget-fixture.render.test.tsx +81 -0
  315. package/src/app/widget-fixture.tsx +34 -11
  316. package/src/app/widget-props-sheet.tsx +57 -16
  317. package/src/app/widget-renderer.tsx +1 -1
  318. package/src/components/DrillButton.tsx +23 -10
  319. package/src/components/EmptyState.tsx +6 -4
  320. package/src/components/ErrorState.tsx +31 -9
  321. package/src/components/FilterBar.test.tsx +82 -0
  322. package/src/components/FilterBar.tsx +52 -26
  323. package/src/components/KpiGrid.test.tsx +279 -0
  324. package/src/components/KpiGrid.tsx +297 -90
  325. package/src/components/ListFooter.test.tsx +116 -0
  326. package/src/components/ListFooter.tsx +102 -15
  327. package/src/components/PagedListFooter.test.tsx +140 -0
  328. package/src/components/PagedListFooter.tsx +107 -0
  329. package/src/components/Pills.test.tsx +77 -0
  330. package/src/components/Pills.tsx +63 -10
  331. package/src/components/SectionHeading.tsx +9 -3
  332. package/src/components/Toast.tsx +37 -19
  333. package/src/components/WidgetHeader.test.tsx +78 -0
  334. package/src/components/WidgetHeader.tsx +63 -16
  335. package/src/components/feedback-states.test.tsx +98 -9
  336. package/src/components/tone-figures.test.tsx +8 -4
  337. package/src/hooks/index.ts +6 -3
  338. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  339. package/src/hooks/tool-result-recovery-model.ts +418 -0
  340. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  341. package/src/hooks/use-tool-query.ts +156 -33
  342. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  343. package/src/hooks/use-tool-result-recovery.test.ts +3 -4
  344. package/src/hooks/use-tool-result-recovery.ts +84 -142
  345. package/src/hooks/use-view-data.test.tsx +118 -9
  346. package/src/hooks/use-view-data.ts +128 -18
  347. package/src/hooks/use-view-tool-query.ts +25 -19
  348. package/src/i18n-coverage.test.tsx +104 -2
  349. package/src/index.ts +48 -11
  350. package/src/lib/focus-return.test.ts +288 -0
  351. package/src/lib/focus-return.ts +96 -0
  352. package/src/lib/parse-tool-result.test.ts +46 -1
  353. package/src/lib/parse-tool-result.ts +40 -2
  354. package/src/lib/toast-model.test.ts +166 -1
  355. package/src/lib/toast-model.ts +104 -2
  356. package/src/lib/tone-utils.test.ts +86 -10
  357. package/src/lib/tone-utils.ts +89 -8
  358. package/src/lib/tool-query.test.ts +569 -0
  359. package/src/lib/tool-query.ts +253 -0
  360. package/src/lib/tool-result-observer.test.ts +296 -0
  361. package/src/lib/tool-result-observer.ts +211 -0
  362. package/src/lib/typography.test.ts +45 -0
  363. package/src/lib/typography.ts +47 -0
  364. package/src/lib/utils.ts +12 -1
  365. package/src/locale/index.ts +1 -0
  366. package/src/locale/message-types.ts +118 -2
  367. package/src/locale/messages-de.ts +49 -10
  368. package/src/locale/messages-en.ts +49 -10
  369. package/src/locale/toolkit-messages.test.ts +133 -0
  370. package/src/locale/toolkit-messages.ts +63 -2
  371. package/src/primitives/alert.tsx +3 -0
  372. package/src/primitives/badge.tsx +9 -4
  373. package/src/primitives/button.tsx +1 -1
  374. package/src/primitives/dialog.tsx +10 -1
  375. package/src/primitives/select.tsx +1 -1
  376. package/src/primitives/sheet.tsx +7 -0
  377. package/src/providers/query-provider.test.tsx +154 -0
  378. package/src/providers/query-provider.tsx +66 -5
  379. package/src/render-cases-primitives.tsx +1 -1
  380. package/src/render-cases.tsx +38 -6
  381. package/src/render-effects.tsx +96 -0
  382. package/src/theme/cd-tokens.test.ts +3 -1
  383. package/src/theme/create-theme.test.ts +10 -0
  384. package/src/theme/create-theme.ts +28 -0
  385. package/src/theme/theme-css.test.ts +51 -0
  386. package/src/theme.css +31 -1
  387. package/ui-catalog.json +448 -41
@@ -0,0 +1,180 @@
1
+ // @vitest-environment happy-dom
2
+ import { useEffect, type ReactNode } from "react"
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
4
+ import { flushEffects, hookProbe, renderWithEffects, type EffectRender } from "../render-effects.js"
5
+ import { useHostBridge, type HostBridge } from "./host-bridge.js"
6
+ import { McpUseHostBridgeProvider, useViewResultSink } from "./mcp-use-host-bridge.js"
7
+ import { useHostModelContext } from "./model-context.js"
8
+
9
+ /**
10
+ * `McpUseHostBridgeProvider`'s wiring with its effects running (toolkit#176
11
+ * K15, K20, N29): `getWidgetData()` returns the payload the view reports it
12
+ * shows, the bridge's `setModelContext` is one line of the view's native
13
+ * model-context tree, and `useHostModelContext` lines come and go with their
14
+ * component. The mcp-use view hooks are the only mock.
15
+ */
16
+
17
+ const host = vi.hoisted(
18
+ (): {
19
+ tool: { status: string; toolOutput?: unknown }
20
+ useViewState: ReturnType<typeof vi.fn> | null
21
+ } => ({ tool: { status: "pending" }, useViewState: null }),
22
+ )
23
+
24
+ vi.mock(import("mcp-use/react"), async (importOriginal) => {
25
+ const actual = await importOriginal()
26
+ const { createElement } = await import("react")
27
+ host.useViewState = vi.fn()
28
+ const ModelContext = ({ content }: { content: string }) =>
29
+ // Not a <meta>: React 19 hoists those into <head>.
30
+ createElement("output", { "data-model-context": content })
31
+ const stable = {
32
+ dynamic: { callTool: () => Promise.resolve({}) },
33
+ followUp: () => Promise.resolve(),
34
+ openExternal: () => Promise.resolve(),
35
+ }
36
+ return {
37
+ ...actual,
38
+ ModelContext: ModelContext as unknown as typeof actual.ModelContext,
39
+ useDynamicTool: (() => stable.dynamic) as unknown as typeof actual.useDynamicTool,
40
+ useSendFollowUp: (() => stable.followUp) as unknown as typeof actual.useSendFollowUp,
41
+ useOpenExternal: (() => stable.openExternal) as unknown as typeof actual.useOpenExternal,
42
+ useToolContext: (() => host.tool) as unknown as typeof actual.useToolContext,
43
+ useViewTheme: (() => "light") as unknown as typeof actual.useViewTheme,
44
+ useHostContext: (() => ({ isAvailable: true })) as unknown as typeof actual.useHostContext,
45
+ useViewState: host.useViewState as unknown as typeof actual.useViewState,
46
+ }
47
+ })
48
+
49
+ const PAYLOAD = { title: "Incidents", context: { keys: {} }, layout: { rows: [] } }
50
+
51
+ /** Reports `payload` the way McpAppView reports what it shows. */
52
+ function Reporter({ payload }: { payload: unknown }) {
53
+ const report = useViewResultSink()
54
+ useEffect(() => {
55
+ report?.(payload)
56
+ }, [payload, report])
57
+ return null
58
+ }
59
+
60
+ /** Calls the bridge's setModelContext with `text` on every change. */
61
+ function BridgeLine({ text }: { text: string }) {
62
+ const bridge = useHostBridge()
63
+ useEffect(() => {
64
+ bridge.setModelContext?.(text)
65
+ }, [bridge, text])
66
+ return null
67
+ }
68
+
69
+ function Line({ content }: { content: string | null }) {
70
+ useHostModelContext(content)
71
+ return null
72
+ }
73
+
74
+ const lines = (view: EffectRender) =>
75
+ [...view.container.querySelectorAll("[data-model-context]")].map((node) =>
76
+ node.getAttribute("data-model-context"),
77
+ )
78
+
79
+ let mounted: EffectRender | null = null
80
+
81
+ async function render(children: ReactNode) {
82
+ mounted = await renderWithEffects(<McpUseHostBridgeProvider>{children}</McpUseHostBridgeProvider>)
83
+ return mounted
84
+ }
85
+
86
+ const rerender = (view: EffectRender, children: ReactNode) =>
87
+ view.rerender(<McpUseHostBridgeProvider>{children}</McpUseHostBridgeProvider>)
88
+
89
+ beforeEach(() => {
90
+ host.tool = { status: "pending", toolOutput: undefined }
91
+ })
92
+
93
+ afterEach(async () => {
94
+ await mounted?.unmount()
95
+ mounted = null
96
+ })
97
+
98
+ describe("McpUseHostBridgeProvider getWidgetData (K15)", () => {
99
+ const bridgeProbe = () => hookProbe((): HostBridge => useHostBridge())
100
+
101
+ it("returns the payload the view reports while the host's latch is still pending", async () => {
102
+ const probe = bridgeProbe()
103
+ const view = await render(<probe.Probe />)
104
+ expect(probe.current().getWidgetData()).toBeNull()
105
+ await rerender(
106
+ view,
107
+ <>
108
+ <probe.Probe />
109
+ <Reporter payload={PAYLOAD} />
110
+ </>,
111
+ )
112
+ await flushEffects()
113
+ expect(probe.current().getWidgetData()).toEqual(PAYLOAD)
114
+ })
115
+
116
+ it("prefers the reported payload over an unusable latched output (structuredContent: null)", async () => {
117
+ host.tool = { status: "ready", toolOutput: null }
118
+ const probe = bridgeProbe()
119
+ const view = await render(<probe.Probe />)
120
+ expect(probe.current().getWidgetData()).toBeNull()
121
+ await rerender(
122
+ view,
123
+ <>
124
+ <probe.Probe />
125
+ <Reporter payload={PAYLOAD} />
126
+ </>,
127
+ )
128
+ await flushEffects()
129
+ expect(probe.current().getWidgetData()).toEqual(PAYLOAD)
130
+ })
131
+
132
+ it("without a report, the latched output once ready", async () => {
133
+ host.tool = { status: "ready", toolOutput: PAYLOAD }
134
+ const probe = bridgeProbe()
135
+ await render(<probe.Probe />)
136
+ expect(probe.current().getWidgetData()).toEqual(PAYLOAD)
137
+ })
138
+ })
139
+
140
+ describe("McpUseHostBridgeProvider model context (K20, N29)", () => {
141
+ it("the bridge's setModelContext is one native line: set, replaced, removed when blank", async () => {
142
+ const view = await render(<BridgeLine text="Viewing 3 incidents" />)
143
+ await flushEffects()
144
+ expect(lines(view)).toEqual(["Viewing 3 incidents"])
145
+ await rerender(view, <BridgeLine text="Viewing 2 incidents" />)
146
+ await flushEffects()
147
+ expect(lines(view), "last writer wins: still one line").toEqual(["Viewing 2 incidents"])
148
+ await rerender(view, <BridgeLine text="" />)
149
+ await flushEffects()
150
+ expect(lines(view)).toEqual([])
151
+ })
152
+
153
+ it("never seeds the widgets' view state (N29)", async () => {
154
+ await render(<BridgeLine text="Viewing 3 incidents" />)
155
+ await flushEffects()
156
+ expect(host.useViewState).not.toHaveBeenCalled()
157
+ })
158
+
159
+ it("useHostModelContext joins the tree on mount and leaves it on unmount or when blank", async () => {
160
+ const view = await render(
161
+ <>
162
+ <Line content="Filter: open" />
163
+ <Line content="Selected: #42" />
164
+ </>,
165
+ )
166
+ expect(lines(view)).toEqual(["Filter: open", "Selected: #42"])
167
+ await rerender(
168
+ view,
169
+ <>
170
+ <Line content="Filter: open" />
171
+ <Line content=" " />
172
+ </>,
173
+ )
174
+ expect(lines(view), "a blank line is removed").toEqual(["Filter: open"])
175
+ await rerender(view, <Line content="Filter: open" />)
176
+ expect(lines(view)).toEqual(["Filter: open"])
177
+ await rerender(view, null)
178
+ expect(lines(view), "unmounted, its line is gone").toEqual([])
179
+ })
180
+ })
@@ -14,24 +14,17 @@ import {
14
14
  useOpenExternal,
15
15
  useSendFollowUp,
16
16
  useToolContext,
17
- useViewState,
18
17
  useViewTheme,
19
18
  } from "mcp-use/react"
20
19
  import { HostBridgeProvider, toHostBridge, type HostBridge } from "./host-bridge.js"
21
-
22
- /**
23
- * `true` inside an mcp-use 2.x view — i.e. under a
24
- * {@link McpUseHostBridgeProvider}, which itself only renders under a
25
- * `bootstrapView`-mounted tree. Components that want to use `mcp-use/react`
26
- * primitives (which throw outside a view) branch on this instead of probing
27
- * mcp-use internals; see `adaptDataWidget`'s ModelContext handling.
28
- */
29
- const McpUseViewContext = createContext(false)
30
-
31
- /** Whether the subtree runs inside an mcp-use view (see {@link McpUseViewContext}). */
32
- export function useIsInsideMcpUseView(): boolean {
33
- return useContext(McpUseViewContext)
34
- }
20
+ import {
21
+ McpUseViewContext,
22
+ ModelContextChannelContext,
23
+ ModelContextLines,
24
+ modelContextLine,
25
+ withModelContextLine,
26
+ type ModelContextChannel,
27
+ } from "./model-context.js"
35
28
 
36
29
  type DynamicCallTool = (name: string, args: Record<string, unknown>) => Promise<unknown>
37
30
  type BoundCall = (args: Record<string, unknown>) => Promise<unknown>
@@ -99,6 +92,34 @@ function useDynamicToolPool(): { callTool: DynamicCallTool; binders: ReactNode }
99
92
  return { callTool, binders }
100
93
  }
101
94
 
95
+ /** The line of the bridge's own `setModelContext` (last writer wins). */
96
+ const BRIDGE_LINE = "host-bridge"
97
+
98
+ /**
99
+ * Where the view reports the payload it shows (the host's, a stripped-result
100
+ * recovery's, the user's retry or refresh), so `HostBridge.getWidgetData()`
101
+ * returns what the view shows (toolkit#176 K15): the bridge, not one
102
+ * component's local state, holds the view's data.
103
+ */
104
+ const ViewResultSinkContext = createContext<((payload: unknown) => void) | null>(null)
105
+
106
+ /**
107
+ * The sink for the payload a view shows, or `null` outside a
108
+ * {@link McpUseHostBridgeProvider}. `McpAppView` reports through it.
109
+ */
110
+ export function useViewResultSink(): ((payload: unknown) => void) | null {
111
+ return useContext(ViewResultSinkContext)
112
+ }
113
+
114
+ /**
115
+ * What `getWidgetData()` returns: the payload the view reported it shows,
116
+ * else the rendering tool's output once the call latched `"ready"`.
117
+ */
118
+ function bridgeOutput(shown: unknown, tool: { status: string; toolOutput?: unknown }): unknown {
119
+ if (shown !== undefined) return shown
120
+ return tool.status === "ready" ? tool.toolOutput : undefined
121
+ }
122
+
102
123
  /**
103
124
  * Install the `mcp-use` {@link HostBridge} (plus the view-scope flag) for a
104
125
  * subtree inside an mcp-use 2.x view.
@@ -112,10 +133,16 @@ function useDynamicToolPool(): { callTool: DynamicCallTool; binders: ReactNode }
112
133
  * - `sendFollowup` / `openExternal` — `useSendFollowUp` / `useOpenExternal`;
113
134
  * while the bridge is not connected (local `file://` preview), `openExternal`
114
135
  * throws into `toHostBridge`'s documented `window.open` fallback.
115
- * - `setModelContext` — writes `{ __model_context }` into the view state,
116
- * which hosts receive via `ui/update-model-context` (ext-apps) or
117
- * `widgetState` (ChatGPT). The key is a toolkit-owned convention since 2.x.
118
- * - `getWidgetData` — the rendering tool's `structuredContent` once ready.
136
+ * - `setModelContext` — one line of the view's model-context tree (an mcp-use
137
+ * `ModelContext` node beside the ones `HostModelContext` and
138
+ * `useHostModelContext` register), which hosts receive via
139
+ * `ui/update-model-context` (ext-apps) or `widgetState` (ChatGPT). The
140
+ * provider does not seed mcp-use's view state, so a widget's own
141
+ * `useViewState(default)` keeps its default (N29).
142
+ * - `getWidgetData` — the payload the view reports it shows
143
+ * ({@link useViewResultSink}): the host's, or the one it got by re-running
144
+ * the tool when the host stripped it or kept back an unusable one. Without
145
+ * a report, the rendering tool's `structuredContent` once ready.
119
146
  *
120
147
  * Must render inside a `bootstrapView`-mounted tree (the mcp-use hooks throw
121
148
  * otherwise) — `McpToolkitApp` does this for the standard bundle; use it
@@ -128,9 +155,17 @@ export function McpUseHostBridgeProvider({ children }: { children: ReactNode }):
128
155
  const tool = useToolContext()
129
156
  const theme = useViewTheme()
130
157
  const { isAvailable } = useHostContext()
131
- const [, setViewState] = useViewState<Record<string, unknown>>({})
158
+ const [lines, setLines] = useState<ReadonlyMap<string, string>>(() => new Map())
159
+ const channel = useMemo<ModelContextChannel>(
160
+ () => ({ set: (id, content) => setLines((prev) => withModelContextLine(prev, id, content)) }),
161
+ [],
162
+ )
163
+ // The payload the view reports it shows wins: after a latch the view could
164
+ // not render (`structuredContent: null` latches "ready"), the host's own
165
+ // output is not what the user sees.
166
+ const [shown, setShown] = useState<unknown>(undefined)
132
167
 
133
- const output = tool.status === "ready" ? tool.toolOutput : undefined
168
+ const output = bridgeOutput(shown, tool)
134
169
 
135
170
  const bridge = useMemo<HostBridge>(
136
171
  () =>
@@ -147,18 +182,26 @@ export function McpUseHostBridgeProvider({ children }: { children: ReactNode }):
147
182
  },
148
183
  output,
149
184
  theme,
185
+ // toHostBridge writes model context as `{ __model_context }`: it
186
+ // becomes the bridge's line of the one channel, never view state.
150
187
  setState: (state) => {
151
- setViewState((prev) => ({ ...prev, ...state }))
188
+ const text = state.__model_context
189
+ if (typeof text === "string") channel.set(BRIDGE_LINE, modelContextLine(text))
152
190
  return Promise.resolve()
153
191
  },
154
192
  }),
155
- [callTool, sendFollowUp, openExternal, output, theme, isAvailable, setViewState],
193
+ [callTool, sendFollowUp, openExternal, output, theme, isAvailable, channel],
156
194
  )
157
195
 
158
196
  return (
159
197
  <McpUseViewContext.Provider value={true}>
160
198
  {binders}
161
- <HostBridgeProvider bridge={bridge}>{children}</HostBridgeProvider>
199
+ <ModelContextLines lines={lines} />
200
+ <ModelContextChannelContext.Provider value={channel}>
201
+ <ViewResultSinkContext.Provider value={setShown}>
202
+ <HostBridgeProvider bridge={bridge}>{children}</HostBridgeProvider>
203
+ </ViewResultSinkContext.Provider>
204
+ </ModelContextChannelContext.Provider>
162
205
  </McpUseViewContext.Provider>
163
206
  )
164
207
  }
@@ -0,0 +1,116 @@
1
+ import type { ReactNode } from "react"
2
+ import { renderToStaticMarkup } from "react-dom/server"
3
+ import { describe, expect, it, vi } from "vitest"
4
+ import {
5
+ HostModelContext,
6
+ McpUseViewContext,
7
+ ModelContextLines,
8
+ modelContextLine,
9
+ useIsInsideMcpUseView,
10
+ withModelContextLine,
11
+ } from "./model-context.js"
12
+
13
+ // The native node, made visible (it throws outside a bootstrapView mount).
14
+ vi.mock(import("mcp-use/react"), async (importOriginal) => {
15
+ const actual = await importOriginal()
16
+ const { createElement, Fragment } = await import("react")
17
+ const ModelContext = ({ content, children }: { content: string; children?: ReactNode }) =>
18
+ createElement(
19
+ Fragment,
20
+ null,
21
+ createElement("meta", { "data-model-context": content }),
22
+ children,
23
+ )
24
+ return { ...actual, ModelContext: ModelContext as unknown as typeof actual.ModelContext }
25
+ })
26
+
27
+ /**
28
+ * The view's ONE model-context channel (toolkit#176 K20, N29): every line
29
+ * the view reports joins the native mcp-use tree inside a view; outside one
30
+ * it falls back to the bridge.
31
+ */
32
+ describe("withModelContextLine", () => {
33
+ it("sets, replaces and removes a line, keeping the order of first registration", () => {
34
+ const one = withModelContextLine(new Map(), "a", "Viewing 3 incidents")
35
+ const two = withModelContextLine(one, "b", "Filter: open")
36
+ const replaced = withModelContextLine(two, "a", "Viewing 2 incidents")
37
+ expect([...replaced]).toEqual([
38
+ ["a", "Viewing 2 incidents"],
39
+ ["b", "Filter: open"],
40
+ ])
41
+ expect([...withModelContextLine(replaced, "a", null)]).toEqual([["b", "Filter: open"]])
42
+ })
43
+
44
+ it("returns the same map when nothing changes (no re-render)", () => {
45
+ const lines = withModelContextLine(new Map(), "a", "x")
46
+ expect(withModelContextLine(lines, "a", "x")).toBe(lines)
47
+ expect(withModelContextLine(lines, "missing", null)).toBe(lines)
48
+ })
49
+ })
50
+
51
+ describe("modelContextLine", () => {
52
+ it("a blank line is nothing to report", () => {
53
+ expect(modelContextLine("Viewing 3 incidents")).toBe("Viewing 3 incidents")
54
+ expect(modelContextLine(" ")).toBeNull()
55
+ expect(modelContextLine("")).toBeNull()
56
+ expect(modelContextLine(null)).toBeNull()
57
+ expect(modelContextLine(undefined)).toBeNull()
58
+ })
59
+ })
60
+
61
+ describe("HostModelContext", () => {
62
+ it("inside a view: the native node, its children nested below it", () => {
63
+ const html = renderToStaticMarkup(
64
+ <McpUseViewContext.Provider value={true}>
65
+ <HostModelContext content="Viewing 3 incidents">
66
+ <span>board</span>
67
+ </HostModelContext>
68
+ </McpUseViewContext.Provider>,
69
+ )
70
+ expect(html).toBe('<meta data-model-context="Viewing 3 incidents"/><span>board</span>')
71
+ })
72
+
73
+ it("reports one line without children", () => {
74
+ const html = renderToStaticMarkup(
75
+ <McpUseViewContext.Provider value={true}>
76
+ <HostModelContext content="The view could not be shown." />
77
+ </McpUseViewContext.Provider>,
78
+ )
79
+ expect(html).toBe('<meta data-model-context="The view could not be shown."/>')
80
+ })
81
+
82
+ it("outside a view: no native node, the children render (the bridge gets the line)", () => {
83
+ const html = renderToStaticMarkup(
84
+ <HostModelContext content="Viewing 3 incidents">
85
+ <span>board</span>
86
+ </HostModelContext>,
87
+ )
88
+ expect(html).toBe("<span>board</span>")
89
+ })
90
+ })
91
+
92
+ describe("ModelContextLines / useIsInsideMcpUseView", () => {
93
+ it("renders one native node per channel line", () => {
94
+ const lines = new Map([
95
+ ["a", "Viewing 3 incidents"],
96
+ ["host-bridge", "Selected: INC-7"],
97
+ ])
98
+ expect(renderToStaticMarkup(<ModelContextLines lines={lines} />)).toBe(
99
+ '<meta data-model-context="Viewing 3 incidents"/><meta data-model-context="Selected: INC-7"/>',
100
+ )
101
+ })
102
+
103
+ it("reports the view scope", () => {
104
+ function Probe() {
105
+ return <i>{String(useIsInsideMcpUseView())}</i>
106
+ }
107
+ expect(renderToStaticMarkup(<Probe />)).toBe("<i>false</i>")
108
+ expect(
109
+ renderToStaticMarkup(
110
+ <McpUseViewContext.Provider value={true}>
111
+ <Probe />
112
+ </McpUseViewContext.Provider>,
113
+ ),
114
+ ).toBe("<i>true</i>")
115
+ })
116
+ })
@@ -0,0 +1,125 @@
1
+ import { createContext, useContext, useEffect, useId, type ReactNode } from "react"
2
+ import { ModelContext } from "mcp-use/react"
3
+ import { useHostBridgeOrNull } from "./host-bridge.js"
4
+
5
+ /**
6
+ * The view's model context: what the model reads about the view on its next
7
+ * turn (`ui/update-model-context`). The one module that builds on mcp-use's
8
+ * native `ModelContext`, which throws outside a `bootstrapView` mount; every
9
+ * other component reports through {@link HostModelContext} or
10
+ * {@link useHostModelContext} (toolkit#176 K20).
11
+ */
12
+
13
+ /**
14
+ * `true` inside an mcp-use 2.x view — i.e. under a `McpUseHostBridgeProvider`,
15
+ * which itself only renders under a `bootstrapView`-mounted tree. Components
16
+ * that want to use `mcp-use/react` primitives (which throw outside a view)
17
+ * branch on this instead of probing mcp-use internals. Exported for the
18
+ * provider and for tests that render a view-scoped subtree.
19
+ */
20
+ export const McpUseViewContext = createContext(false)
21
+
22
+ /** Whether the subtree runs inside an mcp-use view (see {@link McpUseViewContext}). */
23
+ export function useIsInsideMcpUseView(): boolean {
24
+ return useContext(McpUseViewContext)
25
+ }
26
+
27
+ /**
28
+ * Model-context reporting that works on every host — the host-portable
29
+ * replacement for rendering `mcp-use/react`'s `ModelContext` directly.
30
+ *
31
+ * Inside an mcp-use view it renders the native `ModelContext`, which nests:
32
+ * the lines of its children hang below its own in the one description the
33
+ * host receives. Everywhere else — ChatGPT via `createChatGptHostBridge`, the
34
+ * `WidgetFixtureHost` harness, standalone apps — the description flows
35
+ * through the bridge's `setModelContext` instead (last-writer-wins; fine for
36
+ * the single-widget mounts those hosts render).
37
+ *
38
+ * Widgets that self-fetch and report their own context (cockpit views like
39
+ * `TasksBoard`) must use this instead of importing `ModelContext` from
40
+ * `mcp-use/react` — the direct import crashes the widget in every non-view
41
+ * mount, starting with the widget playground. Without `children` it reports
42
+ * one line where it is rendered.
43
+ */
44
+ export function HostModelContext({
45
+ content,
46
+ children,
47
+ }: {
48
+ content: string
49
+ children?: ReactNode
50
+ }): ReactNode {
51
+ const insideView = useIsInsideMcpUseView()
52
+ if (insideView) return <ModelContext content={content}>{children}</ModelContext>
53
+ return <BridgeModelContext content={content}>{children}</BridgeModelContext>
54
+ }
55
+
56
+ function BridgeModelContext({ content, children }: { content: string; children?: ReactNode }) {
57
+ const bridge = useHostBridgeOrNull()
58
+ const setModelContext = bridge?.setModelContext
59
+ useEffect(() => {
60
+ setModelContext?.(content)
61
+ }, [setModelContext, content])
62
+ return children
63
+ }
64
+
65
+ /**
66
+ * The view's ONE model-context channel: lines registered here render as
67
+ * root-level native `ModelContext` nodes ({@link ModelContextLines}), the
68
+ * tree `HostModelContext` nodes join too, so the host receives everything
69
+ * the view reports in one description. `McpUseHostBridgeProvider` owns it.
70
+ */
71
+ export interface ModelContextChannel {
72
+ /** Set (`content`) or remove (`null`) the line registered under `id`. */
73
+ set: (id: string, content: string | null) => void
74
+ }
75
+
76
+ export const ModelContextChannelContext = createContext<ModelContextChannel | null>(null)
77
+
78
+ /** `lines` with `id` set to `content` (or removed for `null`); the same map when nothing changes. */
79
+ export function withModelContextLine(
80
+ lines: ReadonlyMap<string, string>,
81
+ id: string,
82
+ content: string | null,
83
+ ): ReadonlyMap<string, string> {
84
+ if (content === null ? !lines.has(id) : lines.get(id) === content) return lines
85
+ const next = new Map(lines)
86
+ if (content === null) next.delete(id)
87
+ else next.set(id, content)
88
+ return next
89
+ }
90
+
91
+ /** A line of model context, or `null` for a blank one (nothing to report). */
92
+ export function modelContextLine(content: string | null | undefined): string | null {
93
+ return content?.trim() ? content : null
94
+ }
95
+
96
+ /** Renders the channel's lines as native `ModelContext` nodes (inside a view only). */
97
+ export function ModelContextLines({ lines }: { lines: ReadonlyMap<string, string> }): ReactNode {
98
+ return [...lines].map(([id, content]) => <ModelContext key={id} content={content} />)
99
+ }
100
+
101
+ /**
102
+ * Report one line of model context for as long as the calling component is
103
+ * mounted: what the user is looking at, a filter, an error the user sees.
104
+ * The channel helper for components with nothing to wrap
105
+ * ({@link HostModelContext} is the wrapping form).
106
+ *
107
+ * Inside an mcp-use view the line joins the view's one model-context tree and
108
+ * leaves it on unmount or when `content` turns blank. Elsewhere (the ChatGPT
109
+ * bridge, the fixture harness, standalone apps) it goes through the bridge's
110
+ * `setModelContext`, last writer wins.
111
+ */
112
+ export function useHostModelContext(content: string | null | undefined): void {
113
+ const channel = useContext(ModelContextChannelContext)
114
+ const setModelContext = useHostBridgeOrNull()?.setModelContext
115
+ const id = useId()
116
+ const line = modelContextLine(content)
117
+ useEffect(() => {
118
+ if (channel) {
119
+ channel.set(id, line)
120
+ return () => channel.set(id, null)
121
+ }
122
+ if (line !== null) setModelContext?.(line)
123
+ return undefined
124
+ }, [channel, id, line, setModelContext])
125
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef, type RefObject } from "react"
2
+
3
+ /** Where focus goes when build mode opens: the builder's title. */
4
+ export const BUILDER_FOCUS_TARGET = '[data-slot="builder-title"]'
5
+ /** Where focus goes when build mode closes (Apply or Discard): the edit button that opened it. */
6
+ export const VIEW_FOCUS_TARGET = '[data-slot="view-edit"]'
7
+
8
+ /**
9
+ * The selector focus moves to when build mode switches, or `null` when it
10
+ * did not switch (the first render included). Pure, so the hand-off is
11
+ * unit-testable without a DOM.
12
+ */
13
+ export function buildModeFocusTarget(previous: boolean, current: boolean): string | null {
14
+ if (previous === current) return null
15
+ return current ? BUILDER_FOCUS_TARGET : VIEW_FOCUS_TARGET
16
+ }
17
+
18
+ /**
19
+ * Where focus goes when a `LayoutBuilder` mounts, by its reset `generation`:
20
+ * after a reset (Discard without `onExit`, > 0) the builder's title, because
21
+ * the Discard button that had the focus went with the old builder; on the
22
+ * first mount nowhere (McpAppView's build-mode switch handles that one).
23
+ */
24
+ export function builderMountFocusTarget(generation: number): string | null {
25
+ return generation > 0 ? BUILDER_FOCUS_TARGET : null
26
+ }
27
+
28
+ /**
29
+ * Focuses the element `target` selects inside the returned ref's element once
30
+ * it mounts; `null` focuses nothing. `LayoutBuilder` passes
31
+ * {@link builderMountFocusTarget} of its reset generation.
32
+ */
33
+ export function useFocusOnMount<T extends HTMLElement>(target: string | null): RefObject<T | null> {
34
+ const ref = useRef<T>(null)
35
+ useEffect(() => {
36
+ if (target) ref.current?.querySelector<HTMLElement>(target)?.focus()
37
+ }, [target])
38
+ return ref
39
+ }
40
+
41
+ /**
42
+ * Moves focus across the build-mode switch: the button that opened the
43
+ * builder disappears with the toolbar, so focus lands on the builder's title;
44
+ * Apply and Discard unmount the builder, so focus returns to the edit button.
45
+ * Focus that already sits outside the view (another frame, the host) stays
46
+ * where it is.
47
+ */
48
+ export function useBuildModeFocus(
49
+ buildMode: boolean,
50
+ frameRef: RefObject<HTMLElement | null>,
51
+ ): void {
52
+ const previous = useRef(buildMode)
53
+ useEffect(() => {
54
+ const target = buildModeFocusTarget(previous.current, buildMode)
55
+ previous.current = buildMode
56
+ const frame = frameRef.current
57
+ if (!target || !frame) return
58
+ const active = frame.ownerDocument.activeElement
59
+ if (active && active !== frame.ownerDocument.body && !frame.contains(active)) return
60
+ frame.querySelector<HTMLElement>(target)?.focus()
61
+ }, [buildMode, frameRef])
62
+ }