@miragon/mcp-toolkit-ui 0.10.1

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 (320) hide show
  1. package/README.md +36 -0
  2. package/dist/app/adapt-data-widget.d.ts +33 -0
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -0
  4. package/dist/app/adapt-data-widget.js +35 -0
  5. package/dist/app/adapt-data-widget.js.map +1 -0
  6. package/dist/app/builder/CatalogueSheet.d.ts +13 -0
  7. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
  8. package/dist/app/builder/CatalogueSheet.js +33 -0
  9. package/dist/app/builder/CatalogueSheet.js.map +1 -0
  10. package/dist/app/builder/PipelineStrip.d.ts +22 -0
  11. package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
  12. package/dist/app/builder/PipelineStrip.js +31 -0
  13. package/dist/app/builder/PipelineStrip.js.map +1 -0
  14. package/dist/app/builder/PreviewPane.d.ts +13 -0
  15. package/dist/app/builder/PreviewPane.d.ts.map +1 -0
  16. package/dist/app/builder/PreviewPane.js +21 -0
  17. package/dist/app/builder/PreviewPane.js.map +1 -0
  18. package/dist/app/builder/SaveDialog.d.ts +14 -0
  19. package/dist/app/builder/SaveDialog.d.ts.map +1 -0
  20. package/dist/app/builder/SaveDialog.js +14 -0
  21. package/dist/app/builder/SaveDialog.js.map +1 -0
  22. package/dist/app/builder/Toolbar.d.ts +17 -0
  23. package/dist/app/builder/Toolbar.d.ts.map +1 -0
  24. package/dist/app/builder/Toolbar.js +15 -0
  25. package/dist/app/builder/Toolbar.js.map +1 -0
  26. package/dist/app/builder/Workspace.d.ts +28 -0
  27. package/dist/app/builder/Workspace.d.ts.map +1 -0
  28. package/dist/app/builder/Workspace.js +117 -0
  29. package/dist/app/builder/Workspace.js.map +1 -0
  30. package/dist/app/builder/builder-model.d.ts +86 -0
  31. package/dist/app/builder/builder-model.d.ts.map +1 -0
  32. package/dist/app/builder/builder-model.js +174 -0
  33. package/dist/app/builder/builder-model.js.map +1 -0
  34. package/dist/app/builder/builder-reducer.d.ts +71 -0
  35. package/dist/app/builder/builder-reducer.d.ts.map +1 -0
  36. package/dist/app/builder/builder-reducer.js +201 -0
  37. package/dist/app/builder/builder-reducer.js.map +1 -0
  38. package/dist/app/builder/labels.d.ts +78 -0
  39. package/dist/app/builder/labels.d.ts.map +1 -0
  40. package/dist/app/builder/labels.js +71 -0
  41. package/dist/app/builder/labels.js.map +1 -0
  42. package/dist/app/builder/status-chip.d.ts +10 -0
  43. package/dist/app/builder/status-chip.d.ts.map +1 -0
  44. package/dist/app/builder/status-chip.js +16 -0
  45. package/dist/app/builder/status-chip.js.map +1 -0
  46. package/dist/app/host-bridge.d.ts +211 -0
  47. package/dist/app/host-bridge.d.ts.map +1 -0
  48. package/dist/app/host-bridge.js +247 -0
  49. package/dist/app/host-bridge.js.map +1 -0
  50. package/dist/app/index.d.ts +16 -0
  51. package/dist/app/index.d.ts.map +1 -0
  52. package/dist/app/index.js +18 -0
  53. package/dist/app/index.js.map +1 -0
  54. package/dist/app/layout-builder.d.ts +48 -0
  55. package/dist/app/layout-builder.d.ts.map +1 -0
  56. package/dist/app/layout-builder.js +418 -0
  57. package/dist/app/layout-builder.js.map +1 -0
  58. package/dist/app/mcp-app-view.d.ts +43 -0
  59. package/dist/app/mcp-app-view.d.ts.map +1 -0
  60. package/dist/app/mcp-app-view.js +169 -0
  61. package/dist/app/mcp-app-view.js.map +1 -0
  62. package/dist/app/mcp-toolkit-app.d.ts +18 -0
  63. package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
  64. package/dist/app/mcp-toolkit-app.js +22 -0
  65. package/dist/app/mcp-toolkit-app.js.map +1 -0
  66. package/dist/app/use-host-actions.d.ts +57 -0
  67. package/dist/app/use-host-actions.d.ts.map +1 -0
  68. package/dist/app/use-host-actions.js +42 -0
  69. package/dist/app/use-host-actions.js.map +1 -0
  70. package/dist/app/widget-fixture.d.ts +180 -0
  71. package/dist/app/widget-fixture.d.ts.map +1 -0
  72. package/dist/app/widget-fixture.js +273 -0
  73. package/dist/app/widget-fixture.js.map +1 -0
  74. package/dist/app/widget-props-sheet.d.ts +20 -0
  75. package/dist/app/widget-props-sheet.d.ts.map +1 -0
  76. package/dist/app/widget-props-sheet.js +87 -0
  77. package/dist/app/widget-props-sheet.js.map +1 -0
  78. package/dist/app/widget-renderer.d.ts +87 -0
  79. package/dist/app/widget-renderer.d.ts.map +1 -0
  80. package/dist/app/widget-renderer.js +105 -0
  81. package/dist/app/widget-renderer.js.map +1 -0
  82. package/dist/components/DrillButton.d.ts +22 -0
  83. package/dist/components/DrillButton.d.ts.map +1 -0
  84. package/dist/components/DrillButton.js +13 -0
  85. package/dist/components/DrillButton.js.map +1 -0
  86. package/dist/components/FilterBar.d.ts +28 -0
  87. package/dist/components/FilterBar.d.ts.map +1 -0
  88. package/dist/components/FilterBar.js +12 -0
  89. package/dist/components/FilterBar.js.map +1 -0
  90. package/dist/components/GridLayout.d.ts +26 -0
  91. package/dist/components/GridLayout.d.ts.map +1 -0
  92. package/dist/components/GridLayout.js +31 -0
  93. package/dist/components/GridLayout.js.map +1 -0
  94. package/dist/components/GroupCard.d.ts +21 -0
  95. package/dist/components/GroupCard.d.ts.map +1 -0
  96. package/dist/components/GroupCard.js +24 -0
  97. package/dist/components/GroupCard.js.map +1 -0
  98. package/dist/components/KpiGrid.d.ts +43 -0
  99. package/dist/components/KpiGrid.d.ts.map +1 -0
  100. package/dist/components/KpiGrid.js +35 -0
  101. package/dist/components/KpiGrid.js.map +1 -0
  102. package/dist/components/ListFooter.d.ts +22 -0
  103. package/dist/components/ListFooter.d.ts.map +1 -0
  104. package/dist/components/ListFooter.js +13 -0
  105. package/dist/components/ListFooter.js.map +1 -0
  106. package/dist/components/Pills.d.ts +25 -0
  107. package/dist/components/Pills.d.ts.map +1 -0
  108. package/dist/components/Pills.js +17 -0
  109. package/dist/components/Pills.js.map +1 -0
  110. package/dist/components/SectionHeading.d.ts +16 -0
  111. package/dist/components/SectionHeading.d.ts.map +1 -0
  112. package/dist/components/SectionHeading.js +10 -0
  113. package/dist/components/SectionHeading.js.map +1 -0
  114. package/dist/components/WidgetHeader.d.ts +21 -0
  115. package/dist/components/WidgetHeader.d.ts.map +1 -0
  116. package/dist/components/WidgetHeader.js +11 -0
  117. package/dist/components/WidgetHeader.js.map +1 -0
  118. package/dist/hooks/index.d.ts +11 -0
  119. package/dist/hooks/index.d.ts.map +1 -0
  120. package/dist/hooks/index.js +7 -0
  121. package/dist/hooks/index.js.map +1 -0
  122. package/dist/hooks/use-debounced-value.d.ts +7 -0
  123. package/dist/hooks/use-debounced-value.d.ts.map +1 -0
  124. package/dist/hooks/use-debounced-value.js +15 -0
  125. package/dist/hooks/use-debounced-value.js.map +1 -0
  126. package/dist/hooks/use-mobile.d.ts +6 -0
  127. package/dist/hooks/use-mobile.d.ts.map +1 -0
  128. package/dist/hooks/use-mobile.js +19 -0
  129. package/dist/hooks/use-mobile.js.map +1 -0
  130. package/dist/hooks/use-tool-query.d.ts +21 -0
  131. package/dist/hooks/use-tool-query.d.ts.map +1 -0
  132. package/dist/hooks/use-tool-query.js +47 -0
  133. package/dist/hooks/use-tool-query.js.map +1 -0
  134. package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
  135. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
  136. package/dist/hooks/use-tool-result-recovery.js +93 -0
  137. package/dist/hooks/use-tool-result-recovery.js.map +1 -0
  138. package/dist/hooks/use-view-data.d.ts +19 -0
  139. package/dist/hooks/use-view-data.d.ts.map +1 -0
  140. package/dist/hooks/use-view-data.js +18 -0
  141. package/dist/hooks/use-view-data.js.map +1 -0
  142. package/dist/hooks/use-view-tool-query.d.ts +18 -0
  143. package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
  144. package/dist/hooks/use-view-tool-query.js +29 -0
  145. package/dist/hooks/use-view-tool-query.js.map +1 -0
  146. package/dist/index.d.ts +47 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +56 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/lib/parse-tool-result.d.ts +39 -0
  151. package/dist/lib/parse-tool-result.d.ts.map +1 -0
  152. package/dist/lib/parse-tool-result.js +66 -0
  153. package/dist/lib/parse-tool-result.js.map +1 -0
  154. package/dist/lib/tone-utils.d.ts +18 -0
  155. package/dist/lib/tone-utils.d.ts.map +1 -0
  156. package/dist/lib/tone-utils.js +25 -0
  157. package/dist/lib/tone-utils.js.map +1 -0
  158. package/dist/lib/utils.d.ts +3 -0
  159. package/dist/lib/utils.d.ts.map +1 -0
  160. package/dist/lib/utils.js +6 -0
  161. package/dist/lib/utils.js.map +1 -0
  162. package/dist/locale/index.d.ts +3 -0
  163. package/dist/locale/index.d.ts.map +1 -0
  164. package/dist/locale/index.js +7 -0
  165. package/dist/locale/index.js.map +1 -0
  166. package/dist/locale/locale-provider.d.ts +50 -0
  167. package/dist/locale/locale-provider.d.ts.map +1 -0
  168. package/dist/locale/locale-provider.js +47 -0
  169. package/dist/locale/locale-provider.js.map +1 -0
  170. package/dist/primitives/alert.d.ts +10 -0
  171. package/dist/primitives/alert.d.ts.map +1 -0
  172. package/dist/primitives/alert.js +25 -0
  173. package/dist/primitives/alert.js.map +1 -0
  174. package/dist/primitives/badge.d.ts +8 -0
  175. package/dist/primitives/badge.d.ts.map +1 -0
  176. package/dist/primitives/badge.js +34 -0
  177. package/dist/primitives/badge.js.map +1 -0
  178. package/dist/primitives/button.d.ts +11 -0
  179. package/dist/primitives/button.d.ts.map +1 -0
  180. package/dist/primitives/button.js +36 -0
  181. package/dist/primitives/button.js.map +1 -0
  182. package/dist/primitives/card.d.ts +12 -0
  183. package/dist/primitives/card.d.ts.map +1 -0
  184. package/dist/primitives/card.js +25 -0
  185. package/dist/primitives/card.js.map +1 -0
  186. package/dist/primitives/dialog.d.ts +18 -0
  187. package/dist/primitives/dialog.d.ts.map +1 -0
  188. package/dist/primitives/dialog.js +38 -0
  189. package/dist/primitives/dialog.js.map +1 -0
  190. package/dist/primitives/input.d.ts +4 -0
  191. package/dist/primitives/input.d.ts.map +1 -0
  192. package/dist/primitives/input.js +7 -0
  193. package/dist/primitives/input.js.map +1 -0
  194. package/dist/primitives/scroll-area.d.ts +6 -0
  195. package/dist/primitives/scroll-area.d.ts.map +1 -0
  196. package/dist/primitives/scroll-area.js +11 -0
  197. package/dist/primitives/scroll-area.js.map +1 -0
  198. package/dist/primitives/select.d.ts +16 -0
  199. package/dist/primitives/select.d.ts.map +1 -0
  200. package/dist/primitives/select.js +38 -0
  201. package/dist/primitives/select.js.map +1 -0
  202. package/dist/primitives/separator.d.ts +4 -0
  203. package/dist/primitives/separator.d.ts.map +1 -0
  204. package/dist/primitives/separator.js +9 -0
  205. package/dist/primitives/separator.js.map +1 -0
  206. package/dist/primitives/sheet.d.ts +15 -0
  207. package/dist/primitives/sheet.d.ts.map +1 -0
  208. package/dist/primitives/sheet.js +41 -0
  209. package/dist/primitives/sheet.js.map +1 -0
  210. package/dist/primitives/skeleton.d.ts +3 -0
  211. package/dist/primitives/skeleton.d.ts.map +1 -0
  212. package/dist/primitives/skeleton.js +7 -0
  213. package/dist/primitives/skeleton.js.map +1 -0
  214. package/dist/primitives/switch.d.ts +5 -0
  215. package/dist/primitives/switch.d.ts.map +1 -0
  216. package/dist/primitives/switch.js +8 -0
  217. package/dist/primitives/switch.js.map +1 -0
  218. package/dist/primitives/table.d.ts +11 -0
  219. package/dist/primitives/table.d.ts.map +1 -0
  220. package/dist/primitives/table.js +28 -0
  221. package/dist/primitives/table.js.map +1 -0
  222. package/dist/primitives/tabs.d.ts +12 -0
  223. package/dist/primitives/tabs.d.ts.map +1 -0
  224. package/dist/primitives/tabs.js +29 -0
  225. package/dist/primitives/tabs.js.map +1 -0
  226. package/dist/providers/query-provider.d.ts +11 -0
  227. package/dist/providers/query-provider.d.ts.map +1 -0
  228. package/dist/providers/query-provider.js +20 -0
  229. package/dist/providers/query-provider.js.map +1 -0
  230. package/dist/theme/create-theme.d.ts +130 -0
  231. package/dist/theme/create-theme.d.ts.map +1 -0
  232. package/dist/theme/create-theme.js +65 -0
  233. package/dist/theme/create-theme.js.map +1 -0
  234. package/dist/theme/index.d.ts +7 -0
  235. package/dist/theme/index.d.ts.map +1 -0
  236. package/dist/theme/index.js +7 -0
  237. package/dist/theme/index.js.map +1 -0
  238. package/dist/theme/presets.d.ts +25 -0
  239. package/dist/theme/presets.d.ts.map +1 -0
  240. package/dist/theme/presets.js +68 -0
  241. package/dist/theme/presets.js.map +1 -0
  242. package/dist/theme/theme-provider.d.ts +69 -0
  243. package/dist/theme/theme-provider.d.ts.map +1 -0
  244. package/dist/theme/theme-provider.js +79 -0
  245. package/dist/theme/theme-provider.js.map +1 -0
  246. package/package.json +71 -0
  247. package/src/app/adapt-data-widget.tsx +51 -0
  248. package/src/app/builder/CatalogueSheet.tsx +230 -0
  249. package/src/app/builder/PipelineStrip.tsx +260 -0
  250. package/src/app/builder/PreviewPane.tsx +72 -0
  251. package/src/app/builder/SaveDialog.tsx +95 -0
  252. package/src/app/builder/Toolbar.tsx +95 -0
  253. package/src/app/builder/Workspace.tsx +483 -0
  254. package/src/app/builder/builder-model.test.ts +351 -0
  255. package/src/app/builder/builder-model.ts +213 -0
  256. package/src/app/builder/builder-reducer.test.ts +441 -0
  257. package/src/app/builder/builder-reducer.ts +289 -0
  258. package/src/app/builder/labels.ts +139 -0
  259. package/src/app/builder/status-chip.tsx +37 -0
  260. package/src/app/host-bridge.test.ts +206 -0
  261. package/src/app/host-bridge.tsx +371 -0
  262. package/src/app/index.ts +51 -0
  263. package/src/app/layout-builder.tsx +872 -0
  264. package/src/app/mcp-app-view.tsx +367 -0
  265. package/src/app/mcp-toolkit-app.tsx +25 -0
  266. package/src/app/use-host-actions.test.ts +16 -0
  267. package/src/app/use-host-actions.tsx +88 -0
  268. package/src/app/widget-fixture.test.ts +108 -0
  269. package/src/app/widget-fixture.tsx +441 -0
  270. package/src/app/widget-props-sheet.tsx +285 -0
  271. package/src/app/widget-renderer.test.ts +42 -0
  272. package/src/app/widget-renderer.tsx +200 -0
  273. package/src/components/DrillButton.tsx +37 -0
  274. package/src/components/FilterBar.tsx +68 -0
  275. package/src/components/GridLayout.tsx +49 -0
  276. package/src/components/GroupCard.tsx +52 -0
  277. package/src/components/KpiGrid.tsx +134 -0
  278. package/src/components/ListFooter.tsx +48 -0
  279. package/src/components/Pills.tsx +45 -0
  280. package/src/components/SectionHeading.tsx +26 -0
  281. package/src/components/WidgetHeader.tsx +51 -0
  282. package/src/globals.css +168 -0
  283. package/src/hooks/index.ts +17 -0
  284. package/src/hooks/use-debounced-value.ts +15 -0
  285. package/src/hooks/use-mobile.ts +24 -0
  286. package/src/hooks/use-tool-query.ts +63 -0
  287. package/src/hooks/use-tool-result-recovery.test.ts +65 -0
  288. package/src/hooks/use-tool-result-recovery.ts +144 -0
  289. package/src/hooks/use-view-data.ts +31 -0
  290. package/src/hooks/use-view-tool-query.ts +38 -0
  291. package/src/index.ts +134 -0
  292. package/src/lib/parse-tool-result.test.ts +139 -0
  293. package/src/lib/parse-tool-result.ts +89 -0
  294. package/src/lib/tone-utils.test.ts +35 -0
  295. package/src/lib/tone-utils.ts +38 -0
  296. package/src/lib/utils.ts +6 -0
  297. package/src/locale/index.ts +7 -0
  298. package/src/locale/locale-provider.tsx +81 -0
  299. package/src/primitives/alert.tsx +60 -0
  300. package/src/primitives/badge.tsx +49 -0
  301. package/src/primitives/button.tsx +62 -0
  302. package/src/primitives/card.tsx +92 -0
  303. package/src/primitives/dialog.tsx +144 -0
  304. package/src/primitives/input.tsx +21 -0
  305. package/src/primitives/scroll-area.tsx +54 -0
  306. package/src/primitives/select.tsx +173 -0
  307. package/src/primitives/separator.tsx +21 -0
  308. package/src/primitives/sheet.tsx +134 -0
  309. package/src/primitives/skeleton.tsx +13 -0
  310. package/src/primitives/switch.tsx +24 -0
  311. package/src/primitives/table.tsx +87 -0
  312. package/src/primitives/tabs.tsx +79 -0
  313. package/src/providers/query-provider.tsx +38 -0
  314. package/src/theme/create-theme.test.ts +108 -0
  315. package/src/theme/create-theme.ts +158 -0
  316. package/src/theme/index.ts +9 -0
  317. package/src/theme/presets.ts +82 -0
  318. package/src/theme/theme-provider.tsx +152 -0
  319. package/src/ui-catalog.test.ts +112 -0
  320. package/ui-catalog.json +1280 -0
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { decodeRecoveredResult, resolveRecoveryToolName } from "./use-tool-result-recovery.js"
3
+
4
+ // The hook's effect wiring (single-flight, StrictMode re-run, host-payload
5
+ // precedence) is exercised end-to-end by the consumer's Playwright host
6
+ // simulation (structuredContent keep/strip scenarios); these tests pin the
7
+ // pure decision logic.
8
+
9
+ describe("resolveRecoveryToolName", () => {
10
+ it("reads the SEP-1865 shape toolInfo.tool.name first", () => {
11
+ expect(
12
+ resolveRecoveryToolName({
13
+ toolInfo: { tool: { name: "render-view" }, name: "ignored" },
14
+ }),
15
+ ).toBe("render-view")
16
+ })
17
+
18
+ it("falls back to the flattened variants some hosts send", () => {
19
+ expect(resolveRecoveryToolName({ toolInfo: { name: "show_cockpit" } })).toBe("show_cockpit")
20
+ expect(resolveRecoveryToolName({ toolInfo: { toolName: "show_cockpit" } })).toBe("show_cockpit")
21
+ })
22
+
23
+ it("returns undefined when the tool name is missing or unusable", () => {
24
+ expect(resolveRecoveryToolName(undefined)).toBeUndefined()
25
+ expect(resolveRecoveryToolName({})).toBeUndefined()
26
+ expect(resolveRecoveryToolName({ toolInfo: null })).toBeUndefined()
27
+ expect(resolveRecoveryToolName({ toolInfo: { name: "" } })).toBeUndefined()
28
+ expect(resolveRecoveryToolName({ toolInfo: { name: 42 } })).toBeUndefined()
29
+ })
30
+ })
31
+
32
+ describe("decodeRecoveredResult", () => {
33
+ const isView = (v: unknown): boolean =>
34
+ Boolean((v as { context?: unknown; layout?: unknown } | null)?.context) &&
35
+ Boolean((v as { layout?: unknown } | null)?.layout)
36
+
37
+ it("decodes structuredContent-first and validates it", () => {
38
+ const result = {
39
+ content: [{ type: "text", text: "Cockpit rendered" }],
40
+ structuredContent: { context: { keys: {} }, layout: { rows: [] } },
41
+ }
42
+ expect(decodeRecoveredResult(result, isView)).toEqual({
43
+ context: { keys: {} },
44
+ layout: { rows: [] },
45
+ })
46
+ })
47
+
48
+ it("rejects payloads the validator refuses", () => {
49
+ const result = {
50
+ content: [{ type: "text", text: "just a summary" }],
51
+ structuredContent: { unrelated: true },
52
+ }
53
+ expect(decodeRecoveredResult(result, isView)).toBeNull()
54
+ })
55
+
56
+ it("returns null for error results instead of throwing", () => {
57
+ const result = { isError: true, content: [{ type: "text", text: "[503] engine down" }] }
58
+ expect(decodeRecoveredResult(result, isView)).toBeNull()
59
+ })
60
+
61
+ it("returns null for empty results", () => {
62
+ expect(decodeRecoveredResult(undefined, isView)).toBeNull()
63
+ expect(decodeRecoveredResult({ content: [] }, isView)).toBeNull()
64
+ })
65
+ })
@@ -0,0 +1,144 @@
1
+ import { useEffect, useRef, useState } from "react"
2
+ import { parseToolResult } from "../lib/parse-tool-result.js"
3
+
4
+ /**
5
+ * Recovery for hosts that deliver the `ui/notifications/tool-result`
6
+ * notification without `structuredContent` (observed on claude.ai and Claude
7
+ * Desktop, which forward only the model-facing `content` blocks). The
8
+ * mcp-use bridge then hands the widget an empty props object and the shell
9
+ * would idle on its loading skeleton forever.
10
+ *
11
+ * When the result is ready but the payload is invalid, the hook re-executes
12
+ * the originating tool once via the host bridge's official `callTool` —
13
+ * tool-call RESPONSES carry `structuredContent` intact on those hosts — and
14
+ * exposes the decoded payload. Spec-conforming hosts (e.g. the mcp-use
15
+ * inspector) deliver a valid payload up front, so the recovery never fires
16
+ * there.
17
+ *
18
+ * Assumes the originating tool is a read-only render pipeline (true for all
19
+ * toolkit `render-view`/`show_*` tools) — the re-execution must be safe to
20
+ * repeat.
21
+ */
22
+
23
+ export interface ToolResultRecoveryOptions {
24
+ /** `!isPending` from `useWidget` — the host reported a finished tool call. */
25
+ resultReady: boolean
26
+ /** The props the host delivered (`useWidget().props`). */
27
+ props: unknown
28
+ /**
29
+ * Validates a candidate payload. MUST be referentially stable (module-level
30
+ * function or memoised) — it is an effect dependency.
31
+ */
32
+ isValid: (value: unknown) => boolean
33
+ /** The tool arguments from `ui/notifications/tool-input` (`useWidget().toolInput`). */
34
+ toolInput: Record<string, unknown> | null | undefined
35
+ /** Raw host context (`useWidget().hostContext`); `toolInfo` names the originating tool. */
36
+ hostContext: Record<string, unknown> | null | undefined
37
+ /** The bridge's `callTool` (`useWidget().callTool`). */
38
+ callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>
39
+ }
40
+
41
+ export interface ToolResultRecoveryState<T> {
42
+ /** The recovered payload, `null` until the re-execution resolved (or when it failed). */
43
+ data: T | null
44
+ /** `true` when the single recovery attempt failed — the shell keeps its skeleton, no retry loop. */
45
+ failed: boolean
46
+ }
47
+
48
+ const IDLE: ToolResultRecoveryState<never> = { data: null, failed: false }
49
+
50
+ /**
51
+ * Resolves the originating tool name from the host context's `toolInfo`.
52
+ * Reads the SEP-1865 shape first (`toolInfo.tool.name`), then the flattened
53
+ * variants some hosts send (`toolInfo.name`, `toolInfo.toolName`).
54
+ */
55
+ export function resolveRecoveryToolName(hostContext: unknown): string | undefined {
56
+ if (!hostContext || typeof hostContext !== "object") return undefined
57
+ const info = (hostContext as Record<string, unknown>).toolInfo
58
+ if (!info || typeof info !== "object") return undefined
59
+ const rec = info as Record<string, unknown>
60
+ const tool = rec.tool
61
+ const candidates = [
62
+ tool && typeof tool === "object" ? (tool as Record<string, unknown>).name : undefined,
63
+ rec.name,
64
+ rec.toolName,
65
+ ]
66
+ for (const candidate of candidates) {
67
+ if (typeof candidate === "string" && candidate.length > 0) return candidate
68
+ }
69
+ return undefined
70
+ }
71
+
72
+ /**
73
+ * Decodes a re-executed tool result and validates it. Exposed for tests;
74
+ * the hook uses it on the `callTool` response.
75
+ */
76
+ export function decodeRecoveredResult<T>(
77
+ result: unknown,
78
+ isValid: (value: unknown) => boolean,
79
+ ): T | null {
80
+ let decoded: unknown
81
+ try {
82
+ decoded = parseToolResult<unknown>(result)
83
+ } catch {
84
+ return null
85
+ }
86
+ return decoded !== null && isValid(decoded) ? (decoded as T) : null
87
+ }
88
+
89
+ export function useToolResultRecovery<T>(
90
+ options: ToolResultRecoveryOptions,
91
+ ): ToolResultRecoveryState<T> {
92
+ const { resultReady, props, isValid, toolInput, hostContext, callTool } = options
93
+ const [state, setState] = useState<ToolResultRecoveryState<T>>(IDLE)
94
+ // Single-flight: the promise ref survives StrictMode's dev-mode
95
+ // effect re-run (cleanup + setup on the same instance), so the re-run
96
+ // re-subscribes to the SAME in-flight call instead of firing a second one.
97
+ const flightRef = useRef<Promise<ToolResultRecoveryState<T>> | null>(null)
98
+ const flightInputRef = useRef<unknown>(null)
99
+
100
+ const toolName = resolveRecoveryToolName(hostContext)
101
+
102
+ useEffect(() => {
103
+ // A new tool run (result no longer ready, or fresh tool-input identity)
104
+ // invalidates any previous recovery attempt and re-arms the single-flight.
105
+ if (flightRef.current && (!resultReady || flightInputRef.current !== toolInput)) {
106
+ flightRef.current = null
107
+ setState((prev) => (prev.data === null && !prev.failed ? prev : IDLE))
108
+ }
109
+ // Never fire on conforming hosts (valid payload) or before the
110
+ // originating tool is known.
111
+ if (!resultReady || isValid(props) || toolName === undefined) return
112
+
113
+ if (!flightRef.current) {
114
+ flightInputRef.current = toolInput
115
+ flightRef.current = (async (): Promise<ToolResultRecoveryState<T>> => {
116
+ try {
117
+ const result = await callTool(toolName, toolInput ?? {})
118
+ const data = decodeRecoveredResult<T>(result, isValid)
119
+ if (data !== null) return { data, failed: false }
120
+ console.warn(
121
+ `[mcp-toolkit] tool-result recovery: re-executing "${toolName}" returned no usable payload`,
122
+ )
123
+ return { data: null, failed: true }
124
+ } catch (err) {
125
+ console.warn(`[mcp-toolkit] tool-result recovery for "${toolName}" failed:`, err)
126
+ return { data: null, failed: true }
127
+ }
128
+ })()
129
+ }
130
+
131
+ const flight = flightRef.current
132
+ let stale = false
133
+ void flight.then((outcome) => {
134
+ // Drop the outcome when this subscription was superseded or the flight
135
+ // was invalidated by a newer tool run.
136
+ if (!stale && flightRef.current === flight) setState(outcome)
137
+ })
138
+ return () => {
139
+ stale = true
140
+ }
141
+ }, [resultReady, props, isValid, toolName, toolInput, callTool])
142
+
143
+ return state
144
+ }
@@ -0,0 +1,31 @@
1
+ import { useToolQuery } from "./use-tool-query.js"
2
+
3
+ export interface ViewDataResult<T> {
4
+ data: T | null
5
+ /** True while self-fetching with no data yet — the first paint of a cockpit cell. */
6
+ loading: boolean
7
+ error: Error | null
8
+ }
9
+
10
+ /**
11
+ * The seam that lets one widget component serve both modes without a duplicate UI.
12
+ *
13
+ * 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.
20
+ */
21
+ export function useViewData<T>(
22
+ initialData: T | null | undefined,
23
+ key: ReadonlyArray<unknown>,
24
+ tool: string,
25
+ args: Record<string, unknown>,
26
+ ready: boolean,
27
+ ): 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 }
31
+ }
@@ -0,0 +1,38 @@
1
+ import { useQuery, type UseQueryResult } from "@tanstack/react-query"
2
+ import { useCallTool } from "../providers/query-provider.js"
3
+ import { parseViewToolResult } from "../lib/parse-tool-result.js"
4
+
5
+ export interface UseViewToolQueryOptions {
6
+ enabled?: boolean
7
+ }
8
+
9
+ /**
10
+ * Self-fetch hook for `*_show_*` widget tools — the envelope-unwrapping sibling
11
+ * of {@link useToolQuery}. A widget tool's text channel carries only a model
12
+ * summary and its `structuredContent` is the full view envelope, so decoding
13
+ * with the plain {@link parseToolResult} would hand the widget the whole
14
+ * envelope; this hook decodes via {@link parseViewToolResult} (single-step →
15
+ * flat data, multi-step → keyed by step id) instead.
16
+ *
17
+ * `args` is appended to the effective query key — same queryKey-vs-args
18
+ * contract as {@link useToolQuery}, so sibling widgets sharing a query key
19
+ * collapse to a single deduped tool call.
20
+ */
21
+ export function useViewToolQuery<T = unknown>(
22
+ queryKey: unknown[],
23
+ toolName: string,
24
+ args: object,
25
+ opts?: UseViewToolQueryOptions,
26
+ ): UseQueryResult<T, Error> {
27
+ const callTool = useCallTool()
28
+
29
+ return useQuery<T, Error>({
30
+ queryKey: [...queryKey, args],
31
+ queryFn: async () => {
32
+ if (!callTool) throw new Error("callTool not available")
33
+ const result = await callTool(toolName, args)
34
+ return parseViewToolResult<T>(result)
35
+ },
36
+ enabled: !!callTool && (opts?.enabled ?? true),
37
+ })
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,134 @@
1
+ // shadcn base primitives
2
+ export {
3
+ Card,
4
+ CardHeader,
5
+ CardFooter,
6
+ CardTitle,
7
+ CardAction,
8
+ CardDescription,
9
+ CardContent,
10
+ } from "./primitives/card.js"
11
+ export { Badge, badgeVariants } from "./primitives/badge.js"
12
+ export {
13
+ Table,
14
+ TableHeader,
15
+ TableBody,
16
+ TableFooter,
17
+ TableRow,
18
+ TableHead,
19
+ TableCell,
20
+ TableCaption,
21
+ } from "./primitives/table.js"
22
+ export { Separator } from "./primitives/separator.js"
23
+ export { Skeleton } from "./primitives/skeleton.js"
24
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from "./primitives/tabs.js"
25
+ export { Button, buttonVariants } from "./primitives/button.js"
26
+ export { Input } from "./primitives/input.js"
27
+ export {
28
+ Select,
29
+ SelectContent,
30
+ SelectGroup,
31
+ SelectItem,
32
+ SelectLabel,
33
+ SelectScrollDownButton,
34
+ SelectScrollUpButton,
35
+ SelectSeparator,
36
+ SelectTrigger,
37
+ SelectValue,
38
+ } from "./primitives/select.js"
39
+ export {
40
+ Dialog,
41
+ DialogClose,
42
+ DialogContent,
43
+ DialogDescription,
44
+ DialogFooter,
45
+ DialogHeader,
46
+ DialogOverlay,
47
+ DialogPortal,
48
+ DialogTitle,
49
+ DialogTrigger,
50
+ } from "./primitives/dialog.js"
51
+ export { Alert, AlertDescription, AlertTitle } from "./primitives/alert.js"
52
+ export { Switch } from "./primitives/switch.js"
53
+ export { ScrollArea, ScrollBar } from "./primitives/scroll-area.js"
54
+ export {
55
+ Sheet,
56
+ SheetClose,
57
+ SheetContent,
58
+ SheetDescription,
59
+ SheetFooter,
60
+ SheetHeader,
61
+ SheetTitle,
62
+ SheetTrigger,
63
+ } from "./primitives/sheet.js"
64
+
65
+ // Hooks
66
+ // NOTE: the view-tool hooks (`useViewToolQuery`, `useViewData`) are exported
67
+ // from the `./hooks` subpath only — see `packages/ui/src/hooks/index.ts`.
68
+ export { useIsMobile } from "./hooks/use-mobile.js"
69
+ export { useToolQuery, useToolMutation } from "./hooks/use-tool-query.js"
70
+ export type { UseToolQueryOptions } from "./hooks/use-tool-query.js"
71
+ export { useDebouncedValue } from "./hooks/use-debounced-value.js"
72
+
73
+ // Tone token system — the semantic status tones (ToneVariant + TONE_* maps)
74
+ // behind the tone-aware components below. Pure data + class strings.
75
+ export { TONE_SOFT, TONE_DOT, TONE_TEXT } from "./lib/tone-utils.js"
76
+ export type { ToneVariant } from "./lib/tone-utils.js"
77
+
78
+ // Composed components — focused widget building blocks assembled from the
79
+ // primitives. Pure (React + primitives + cn), so they live in the root barrel.
80
+ export { GridLayout, GridItem } from "./components/GridLayout.js"
81
+ export type { GridLayoutProps, GridItemProps } from "./components/GridLayout.js"
82
+ export { KpiGrid } from "./components/KpiGrid.js"
83
+ export type { KpiGridProps, KpiGridHeader, KpiCell } from "./components/KpiGrid.js"
84
+ export { LivePill, CountPill } from "./components/Pills.js"
85
+ export type { LivePillProps, CountPillProps } from "./components/Pills.js"
86
+ export { SectionHeading } from "./components/SectionHeading.js"
87
+ export type { SectionHeadingProps } from "./components/SectionHeading.js"
88
+ export { WidgetHeader } from "./components/WidgetHeader.js"
89
+ export type { WidgetHeaderProps } from "./components/WidgetHeader.js"
90
+ export { GroupCard } from "./components/GroupCard.js"
91
+ export type { GroupCardProps } from "./components/GroupCard.js"
92
+ export { FilterBar } from "./components/FilterBar.js"
93
+ export type { FilterBarProps, FilterChip } from "./components/FilterBar.js"
94
+ export { DrillButton } from "./components/DrillButton.js"
95
+ export type { DrillButtonProps } from "./components/DrillButton.js"
96
+ export { ListFooter } from "./components/ListFooter.js"
97
+ export type { ListFooterProps } from "./components/ListFooter.js"
98
+
99
+ // Providers
100
+ export { AppQueryProvider, useCallTool, queryClient } from "./providers/query-provider.js"
101
+
102
+ // Theming / white-label token system. Browser-safe (React + DOM only, no
103
+ // `mcp-use/react`), so it belongs in this root barrel: a consumer can re-skin
104
+ // the toolkit's primitives from a single switch without pulling in the host
105
+ // runtime. See packages/ui/src/theme/ and docs/guides/white-labeling.md.
106
+ export { createTheme, ThemeProvider, useTheme, themePresets } from "./theme/index.js"
107
+ export type {
108
+ ThemeTokens,
109
+ ThemeDefinition,
110
+ ThemeVars,
111
+ CreateThemeOptions,
112
+ ThemeMode,
113
+ ThemeContextValue,
114
+ ThemeProviderProps,
115
+ ThemePresetId,
116
+ } from "./theme/index.js"
117
+
118
+ // i18n / localization. Browser-safe (React only, no `mcp-use/react`), so it
119
+ // belongs in this root barrel: a consumer wraps its widget tree in
120
+ // <LocaleProvider locale=… translator=…> and reads the active locale with
121
+ // useLocale/useTranslate. Pairs with `createTranslator` from
122
+ // `@miragon/mcp-toolkit-core`. See packages/ui/src/locale/.
123
+ export { LocaleProvider, useLocale, useTranslate } from "./locale/index.js"
124
+ export type { LocaleContextValue, LocaleProviderProps, BoundTranslate } from "./locale/index.js"
125
+
126
+ // NOTE: MCP App shell (McpAppView, WidgetRenderer) is exported from the
127
+ // `./app` subpath only. Keeping it out of the main barrel prevents consumers
128
+ // that only need primitives (e.g. an admin portal) from pulling `mcp-use/react`
129
+ // and its langchain transitive peer into their Vite bundle.
130
+
131
+ // Utils
132
+ export { cn } from "./lib/utils.js"
133
+ export { parseToolResult, parseViewToolResult } from "./lib/parse-tool-result.js"
134
+ export type { ParseToolResultOptions } from "./lib/parse-tool-result.js"
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { parseToolResult, parseViewToolResult } from "./parse-tool-result.js"
3
+
4
+ describe("parseToolResult (structuredContent-first contract)", () => {
5
+ it("prefers structuredContent over the text channel", () => {
6
+ const result = {
7
+ content: [{ type: "text", text: "Process list: 2 definitions." }],
8
+ structuredContent: { items: [1, 2], total: 2 },
9
+ }
10
+ expect(parseToolResult(result)).toEqual({ items: [1, 2], total: 2 })
11
+ })
12
+
13
+ it("prefers structuredContent even when the text channel is parseable JSON", () => {
14
+ const result = {
15
+ content: [{ type: "text", text: JSON.stringify({ fromText: true }) }],
16
+ structuredContent: { fromStructured: true },
17
+ }
18
+ expect(parseToolResult(result)).toEqual({ fromStructured: true })
19
+ })
20
+
21
+ it("falls back to JSON-decoding the first text block without structuredContent", () => {
22
+ const result = {
23
+ content: [{ type: "text", text: JSON.stringify({ items: [1, 2], total: 2 }) }],
24
+ }
25
+ expect(parseToolResult(result)).toEqual({ items: [1, 2], total: 2 })
26
+ })
27
+
28
+ it("returns non-JSON text verbatim without structuredContent", () => {
29
+ const result = { content: [{ type: "text", text: "plain status line" }] }
30
+ expect(parseToolResult<string>(result)).toBe("plain status line")
31
+ })
32
+
33
+ it("falls back to the raw result when neither channel is present", () => {
34
+ const result = { something: "else" }
35
+ expect(parseToolResult(result)).toEqual({ something: "else" })
36
+ })
37
+
38
+ it("treats structuredContent: null as absent and falls through to text", () => {
39
+ const result = {
40
+ content: [{ type: "text", text: JSON.stringify({ fromText: true }) }],
41
+ structuredContent: null,
42
+ }
43
+ expect(parseToolResult(result)).toEqual({ fromText: true })
44
+ })
45
+
46
+ it("returns a falsy-but-present structuredContent (e.g. 0) instead of falling through", () => {
47
+ const result = { content: [{ type: "text", text: "summary" }], structuredContent: 0 }
48
+ expect(parseToolResult<number>(result)).toBe(0)
49
+ })
50
+
51
+ it("throws the first text block as the error message on isError results", () => {
52
+ const result = { isError: true, content: [{ type: "text", text: "boom" }] }
53
+ expect(() => parseToolResult(result)).toThrowError("boom")
54
+ })
55
+
56
+ it("throws a generic message on isError results without content", () => {
57
+ expect(() => parseToolResult({ isError: true })).toThrowError("Tool call failed")
58
+ })
59
+ })
60
+
61
+ describe('parseToolResult with prefer: "text"', () => {
62
+ it("reads the text channel first when both are present", () => {
63
+ const result = {
64
+ content: [{ type: "text", text: JSON.stringify({ fromText: true }) }],
65
+ structuredContent: { fromStructured: true },
66
+ }
67
+ expect(parseToolResult(result, { prefer: "text" })).toEqual({ fromText: true })
68
+ })
69
+
70
+ it("falls back to structuredContent when there is no text block", () => {
71
+ const result = { structuredContent: { fromStructured: true } }
72
+ expect(parseToolResult(result, { prefer: "text" })).toEqual({ fromStructured: true })
73
+ })
74
+
75
+ it("returns non-JSON text verbatim before consulting structuredContent", () => {
76
+ const result = {
77
+ content: [{ type: "text", text: "plain line" }],
78
+ structuredContent: { fromStructured: true },
79
+ }
80
+ expect(parseToolResult<string>(result, { prefer: "text" })).toBe("plain line")
81
+ })
82
+
83
+ it("still throws on isError results", () => {
84
+ const result = { isError: true, content: [{ type: "text", text: "boom" }] }
85
+ expect(() => parseToolResult(result, { prefer: "text" })).toThrowError("boom")
86
+ })
87
+ })
88
+
89
+ describe("parseViewToolResult (show-tool envelope unwrap)", () => {
90
+ it("unwraps a single-step view envelope to the widget's flat data", () => {
91
+ const result = {
92
+ content: [{ type: "text", text: "Model summary." }],
93
+ structuredContent: {
94
+ title: "Instances",
95
+ context: {
96
+ keys: {},
97
+ stepIds: ["result"],
98
+ stepData: { result: { data: { rows: [1], total: 1 }, keys: {} } },
99
+ errors: [],
100
+ },
101
+ layout: [],
102
+ },
103
+ }
104
+ expect(parseViewToolResult(result)).toEqual({ rows: [1], total: 1 })
105
+ })
106
+
107
+ it("returns multi-step composed views keyed by step id", () => {
108
+ const result = {
109
+ structuredContent: {
110
+ context: {
111
+ stepIds: ["kpis", "result_1"],
112
+ stepData: {
113
+ kpis: { data: { totalCount: 5 } },
114
+ result_1: { data: { patterns: [] } },
115
+ },
116
+ },
117
+ layout: [],
118
+ },
119
+ }
120
+ expect(parseViewToolResult(result)).toEqual({
121
+ kpis: { totalCount: 5 },
122
+ result_1: { patterns: [] },
123
+ })
124
+ })
125
+
126
+ it("passes non-envelope results (e.g. *_data feeds) through unchanged", () => {
127
+ const data = { jobs: [], totalCount: 0 }
128
+ const result = {
129
+ content: [{ type: "text", text: JSON.stringify(data) }],
130
+ structuredContent: data,
131
+ }
132
+ expect(parseViewToolResult(result)).toEqual(data)
133
+ })
134
+
135
+ it("propagates isError results as thrown errors", () => {
136
+ const result = { isError: true, content: [{ type: "text", text: "boom" }] }
137
+ expect(() => parseViewToolResult(result)).toThrowError("boom")
138
+ })
139
+ })
@@ -0,0 +1,89 @@
1
+ import type { ViewStructuredContent } from "@miragon/mcp-toolkit-core"
2
+
3
+ export interface ParseToolResultOptions {
4
+ /**
5
+ * Which channel to read first when both are present.
6
+ *
7
+ * - `"structured"` (default): `structuredContent`-first. Since the widget-tool
8
+ * "text-channel diet" the text block carries only a short model-facing
9
+ * summary, while the data payload lives in `structuredContent` (the view
10
+ * builders and `*_data` feeds always set it). The text channel remains a
11
+ * fallback for plain results from other servers.
12
+ * - `"text"`: text-first (the pre-0.4 behaviour) — JSON-decode the first text
13
+ * block, then fall back to `structuredContent`. Kept for callers that
14
+ * deliberately prefer a tool's human-readable text payload.
15
+ */
16
+ prefer?: "structured" | "text"
17
+ }
18
+
19
+ /**
20
+ * Decodes an MCP tool result down to its data payload.
21
+ *
22
+ * Default (`prefer: "structured"`): `isError` → throw the first text block,
23
+ * then `structuredContent` (if non-null), then JSON-decode the first text
24
+ * block (verbatim string if it isn't JSON), then the raw result.
25
+ *
26
+ * This is **structured-first** — a deliberate, observable change from the
27
+ * pre-0.4 text-first parsing: tools that return BOTH a text summary and
28
+ * `structuredContent` now yield the structured payload, not the summary
29
+ * string. `useToolQuery`/`useToolMutation` and the MCP App shell all decode
30
+ * through this one parser so the convention is uniform.
31
+ */
32
+ export function parseToolResult<T = unknown>(result: unknown, opts?: ParseToolResultOptions): T {
33
+ const r = result as
34
+ | {
35
+ content?: Array<{ text?: string }>
36
+ structuredContent?: unknown
37
+ isError?: boolean
38
+ }
39
+ | undefined
40
+ if (r?.isError) throw new Error(r.content?.[0]?.text ?? "Tool call failed")
41
+
42
+ const fromStructured = (): T | undefined =>
43
+ r?.structuredContent != null ? (r.structuredContent as T) : undefined
44
+ const fromText = (): T | undefined => {
45
+ const text = r?.content?.[0]?.text
46
+ if (text == null) return undefined
47
+ try {
48
+ return JSON.parse(text) as T
49
+ } catch {
50
+ return text as T
51
+ }
52
+ }
53
+
54
+ if (opts?.prefer === "text") {
55
+ const text = fromText()
56
+ if (text !== undefined) return text
57
+ const structured = fromStructured()
58
+ if (structured !== undefined) return structured
59
+ return r as T
60
+ }
61
+
62
+ const structured = fromStructured()
63
+ if (structured !== undefined) return structured
64
+ const text = fromText()
65
+ if (text !== undefined) return text
66
+ return r as T
67
+ }
68
+
69
+ /** Shape of the `structuredContent` envelope emitted by the view builders. */
70
+ type ViewEnvelope = Pick<ViewStructuredContent, "context">
71
+
72
+ /**
73
+ * Decodes a `*_show_*` widget-tool result down to the widget's flat data
74
+ * payload. `buildSingleWidgetView`/`buildComposedView` wrap the data in the
75
+ * `McpAppView` envelope (`context.stepData[<id>].data`); this unwraps it:
76
+ * single-step views yield the step's data directly, multi-step composed views
77
+ * yield an object keyed by step id. Non-envelope results (e.g. `*_data`
78
+ * feeds) pass through {@link parseToolResult} unchanged.
79
+ */
80
+ export function parseViewToolResult<T = unknown>(result: unknown): T {
81
+ const parsed = parseToolResult<unknown>(result)
82
+ const ctx = (parsed as ViewEnvelope | null)?.context
83
+ const stepIds = ctx?.stepIds
84
+ if (ctx?.stepData && Array.isArray(stepIds) && stepIds.length > 0) {
85
+ if (stepIds.length === 1) return ctx.stepData[stepIds[0] ?? ""]?.data as T
86
+ return Object.fromEntries(stepIds.map((id) => [id, ctx.stepData[id]?.data])) as T
87
+ }
88
+ return parsed as T
89
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { TONE_DOT, TONE_SOFT, TONE_TEXT, type ToneVariant } from "./tone-utils.js"
3
+
4
+ const ALL_TONES: ToneVariant[] = ["neutral", "info", "success", "warning", "danger"]
5
+
6
+ describe("tone-utils", () => {
7
+ it("covers every ToneVariant in each map (no missing tones)", () => {
8
+ for (const map of [TONE_SOFT, TONE_DOT, TONE_TEXT]) {
9
+ expect(Object.keys(map).sort()).toEqual([...ALL_TONES].sort())
10
+ }
11
+ })
12
+
13
+ it("maps each tone to a non-empty class string in every map", () => {
14
+ for (const tone of ALL_TONES) {
15
+ expect(TONE_SOFT[tone], `soft:${tone}`).toBeTruthy()
16
+ expect(TONE_DOT[tone], `dot:${tone}`).toBeTruthy()
17
+ expect(TONE_TEXT[tone], `text:${tone}`).toBeTruthy()
18
+ }
19
+ })
20
+
21
+ it("routes each status tone to its matching design token", () => {
22
+ expect(TONE_SOFT.danger).toBe("bg-danger-soft text-danger")
23
+ expect(TONE_SOFT.warning).toBe("bg-warning-soft text-warning")
24
+ expect(TONE_SOFT.success).toBe("bg-success-soft text-success")
25
+ expect(TONE_SOFT.info).toBe("bg-info-soft text-info")
26
+ expect(TONE_DOT.danger).toBe("bg-danger")
27
+ expect(TONE_TEXT.success).toBe("text-success")
28
+ })
29
+
30
+ it("routes the neutral tone to the shadcn muted ramp", () => {
31
+ expect(TONE_SOFT.neutral).toBe("bg-muted text-muted-foreground")
32
+ expect(TONE_DOT.neutral).toBe("bg-muted-foreground")
33
+ expect(TONE_TEXT.neutral).toBe("text-foreground")
34
+ })
35
+ })