sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,286 @@
1
+ /**
2
+ * PLAN-4 3-E (GS-10): "Copy as test". Turns a recorded session (./devtoolsActions getSession())
3
+ * into a Vitest + renderComponent test that replays it:
4
+ *
5
+ * renderComponent(C, { initialState?, drivers? }) the session's initial state (left out when
6
+ * it is the component's own initialState)
7
+ * await t.ready()
8
+ * t.simulateAction(type, data) each action caused by 'intent' | 'simulateAction' | 'behavior' | 'agent'
9
+ * await t.respond(sink, body, OK) a reply action from a makeFetchDriver source (renderComponent's
10
+ * await t.fail(sink, status, ...) HTTP fake answers the request the replayed actions send again)
11
+ * await t.settle()
12
+ * expect(t.state).toEqual(final)
13
+ *
14
+ * 'next' and 'built-in' actions are consequences (the replay produces them again), never replayed;
15
+ * nor is an intent action whose data is the component's own state (an intent over STATE.stream).
16
+ * A t.respond / t.fail that follows simulateAction calls waits for the request they send (G-218).
17
+ * Data is written as JS literals: JSON values, Date, Map, Set, NaN / Infinity / -0, bigint. A DOM
18
+ * event or element becomes a stub ({ type, key, target: { dataset, value, checked, id } }), with a
19
+ * comment. Anything else (functions, class instances, cycles) is left out with a comment.
20
+ *
21
+ * The final-state assertion is written only when the replay is complete: every state-changing
22
+ * action could be replayed with its data, no descendant instance changed the state on its own
23
+ * (a child's intent: copy the test from that instance instead), the session wasn't truncated,
24
+ * and the final state is JSON-safe. Otherwise a comment says why. A test written with the
25
+ * assertion passes unchanged (for drivers the session's app had, pass `drivers`).
26
+ */
27
+ import type {SessionRecording, SessionAction} from './devtoolsActions'
28
+ import {isElement, isEvent, deepEqual} from './devtoolsActions'
29
+
30
+ export interface CopyAsTestOptions {
31
+ /** the import line(s) for the component, e.g. "import App from './App.jsx'" (default: `import <Name> from './<Name>.js'`) */
32
+ componentImport?: string
33
+ /** the identifier the component has in the test (default: the recorded component name) */
34
+ componentName?: string
35
+ /** more import lines (drivers, helpers) */
36
+ imports?: string[]
37
+ /** drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
38
+ drivers?: Record<string, string>
39
+ /** more renderComponent options, as source code: "strict: true" */
40
+ renderOptions?: string
41
+ /** the test's name (default: '<Component> session (copied from sygnal/devtools)') */
42
+ testName?: string
43
+ /** adds a `// @vitest-environment <env>` first line (e.g. 'jsdom') */
44
+ environment?: string
45
+ }
46
+
47
+ export interface CopyAsTestResult {
48
+ /** the test file */
49
+ code: string
50
+ /** true when the test asserts the final state */
51
+ complete: boolean
52
+ /** what was left out, and why (also comments in the code) */
53
+ warnings: string[]
54
+ /** actions written as simulateAction / respond / fail */
55
+ replayed: number
56
+ }
57
+
58
+ const REPLAY = new Set(['intent', 'simulateAction', 'behavior', 'agent'])
59
+ const IDENT = /^[A-Za-z_$][\w$]*$/
60
+
61
+ class Unserializable extends Error {}
62
+
63
+ /** a single-quoted JS string literal */
64
+ const str = (s: string) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")}'`
65
+
66
+ interface Lit { code: string; stubbed: boolean }
67
+
68
+ /** `v` as a JS literal. mode 'data' turns DOM events / elements into stubs; 'state' refuses them. */
69
+ export function literal(v: any, mode: 'data' | 'state' = 'data', indent = ''): Lit {
70
+ const flag = {stubbed: false}
71
+ const code = lit(v, mode, indent, new Set(), flag)
72
+ return {code, stubbed: flag.stubbed}
73
+ }
74
+
75
+ function elementStub(el: any): any {
76
+ const out: any = {}
77
+ const ds = el.dataset && typeof el.dataset == 'object' ? {...el.dataset} : {}
78
+ if (Object.keys(ds).length) out.dataset = ds
79
+ if (el.id) out.id = el.id
80
+ const tag = String(el.tagName).toUpperCase()
81
+ if (/^(INPUT|SELECT|TEXTAREA)$/.test(tag) && typeof el.value == 'string') out.value = el.value
82
+ if (tag == 'INPUT' && /^(checkbox|radio)$/i.test(el.type || '')) out.checked = !!el.checked
83
+ return out
84
+ }
85
+
86
+ function eventStub(e: any): any {
87
+ const out: any = {type: e.type}
88
+ if (typeof e.key == 'string') out.key = e.key
89
+ if (isElement(e.target)) {
90
+ const t = elementStub(e.target)
91
+ if (Object.keys(t).length) out.target = t
92
+ }
93
+ return out
94
+ }
95
+
96
+ function lit(v: any, mode: 'data' | 'state', ind: string, seen: Set<any>, flag: {stubbed: boolean}): string {
97
+ if (v === null) return 'null'
98
+ switch (typeof v) {
99
+ case 'undefined': return 'undefined'
100
+ case 'boolean': return String(v)
101
+ case 'string': return str(v)
102
+ case 'number': return Object.is(v, -0) ? '-0' : String(v)
103
+ case 'bigint': return `${v}n`
104
+ case 'function': throw new Unserializable(`a function (${v.name || 'anonymous'})`)
105
+ case 'symbol': throw new Unserializable(`a symbol (${String(v)})`)
106
+ }
107
+ if (seen.has(v)) throw new Unserializable('a circular reference')
108
+ if (isEvent(v) || isElement(v)) {
109
+ if (mode == 'state') throw new Unserializable(isEvent(v) ? `a DOM ${v.type} event` : 'a DOM element')
110
+ flag.stubbed = true
111
+ return lit(isEvent(v) ? eventStub(v) : elementStub(v), mode, ind, seen, flag)
112
+ }
113
+ seen.add(v)
114
+ try {
115
+ const inner = ind + ' '
116
+ const list = (items: string[], open: string, close: string) => {
117
+ if (!items.length) return open + close
118
+ const one = open + (open == '{' ? ' ' : '') + items.join(', ') + (open == '{' ? ' ' : '') + close
119
+ if (one.length + ind.length <= 100 && !one.includes('\n')) return one
120
+ return open + '\n' + items.map(s => inner + s).join(',\n') + ',\n' + ind + close
121
+ }
122
+ if (Array.isArray(v)) return list(Array.from(v, x => lit(x, mode, inner, seen, flag)), '[', ']')
123
+ if (v instanceof Date) return `new Date(${str(isNaN(+v) ? 'Invalid Date' : v.toISOString())})`
124
+ if (v instanceof Map) return `new Map(${list([...v].map(([k, x]) => list([lit(k, mode, inner + ' ', seen, flag), lit(x, mode, inner + ' ', seen, flag)], '[', ']')), '[', ']')})`
125
+ if (v instanceof Set) return `new Set(${list([...v].map(x => lit(x, mode, inner, seen, flag)), '[', ']')})`
126
+ const proto = Object.getPrototypeOf(v)
127
+ // G-265: a vnode of the JSX pragma (its own constructor, flagged $p) is a plain object
128
+ if (proto !== Object.prototype && proto !== null && !v.$p) throw new Unserializable(`a ${(v.constructor && v.constructor.name) || 'class instance'}`)
129
+ const items: string[] = []
130
+ for (const k of Object.keys(v)) {
131
+ if (v[k] === undefined) continue
132
+ items.push(`${IDENT.test(k) ? k : str(k)}: ${lit(v[k], mode, inner, seen, flag)}`)
133
+ }
134
+ return list(items, '{', '}')
135
+ } finally {
136
+ seen.delete(v)
137
+ }
138
+ }
139
+
140
+ const q = str
141
+ const tryLit = (v: any, mode: 'data' | 'state', ind: string): Lit | {error: string} => {
142
+ try { return literal(v, mode, ind) } catch (e: any) {
143
+ if (e instanceof Unserializable) return {error: e.message}
144
+ throw e
145
+ }
146
+ }
147
+
148
+ /** an error reply from makeFetchDriver: { error, request, status?, body? } for the request's `error` action */
149
+ const isErrorReply = (a: SessionAction) =>
150
+ a.data && typeof a.data == 'object' && 'error' in a.data && a.data.request && typeof a.data.request == 'object' && a.data.request.error === a.type
151
+
152
+ /** The test for a recorded session (see the module comment), with what it left out. */
153
+ export function sessionToTest(rec: SessionRecording, options: CopyAsTestOptions = {}): CopyAsTestResult {
154
+ const name = options.componentName || (IDENT.test(rec.component) ? rec.component : 'Component')
155
+ const warnings: string[] = []
156
+ const incomplete: string[] = []
157
+ const body: string[] = []
158
+ const I = ' '
159
+ const drivers = options.drivers || {}
160
+ let replayed = 0
161
+
162
+ if (rec.truncated) incomplete.push('the session lost its oldest actions (clear the log, then record a shorter session)')
163
+
164
+ // render options
165
+ const opts: string[] = []
166
+ if (rec.initialState !== undefined && !deepEqual(rec.initialState, rec.definitionInitialState)) {
167
+ const l = tryLit(rec.initialState, 'state', I)
168
+ if ('error' in l) {
169
+ warnings.push(`the initial state holds ${l.error}: rendering with the component's own initialState`)
170
+ incomplete.push('the initial state could not be written')
171
+ } else opts.push(`initialState: ${l.code}`)
172
+ }
173
+ const fakeable = new Set(rec.fakeable)
174
+ const driverNames = [...new Set([...rec.drivers.filter(n => !fakeable.has(n)), ...Object.keys(drivers)])]
175
+ const given = driverNames.filter(n => drivers[n] !== undefined)
176
+ const missing = driverNames.filter(n => drivers[n] === undefined)
177
+ if (given.length) opts.push(`drivers: { ${given.map(n => `${IDENT.test(n) ? n : q(n)}: ${drivers[n]}`).join(', ')} }`)
178
+ if (missing.length) warnings.push(`the app had ${missing.length == 1 ? 'a driver' : 'drivers'} for ${missing.join(', ')}: pass ${missing.length == 1 ? 'it' : 'them'} with copyAsTest(..., { drivers: { ${missing[0]}: '...' } }) if the component reads ${missing.length == 1 ? 'it' : 'them'}`)
179
+ if (options.renderOptions) opts.push(options.renderOptions)
180
+
181
+ const names = rec.actionNames && new Set(rec.actionNames)
182
+ // (G-218: t.respond / t.fail right after queued simulateAction calls wait for the request
183
+ // those calls send, so no settle() is needed before them)
184
+ const answer = (line: string) => body.push(line)
185
+ for (const a of rec.actions) {
186
+ const changes = a.sinks.includes('STATE') || a.sinks.includes('state')
187
+ if (REPLAY.has(a.cause)) {
188
+ // an intent over the component's own STATE stream: the replay produces it again
189
+ if (a.echo) continue
190
+ // an intent action with no model entry does nothing (renderComponent can't send it)
191
+ if (names && !names.has(a.type) && a.cause != 'behavior') continue
192
+ replayed++
193
+ if (a.data === undefined) { body.push(`${I}t.simulateAction(${q(a.type)})`); continue }
194
+ const l = tryLit(a.data, 'data', I)
195
+ if ('error' in l) {
196
+ body.push(`${I}t.simulateAction(${q(a.type)}) // data left out: it held ${l.error}`)
197
+ if (changes) incomplete.push(`${a.type}'s data could not be written (${l.error})`)
198
+ continue
199
+ }
200
+ if (l.stubbed) body.push(`${I}// ${a.type}: DOM event/element data as a stub (type, key, target dataset/value/checked/id)`)
201
+ body.push(`${I}t.simulateAction(${q(a.type)}, ${l.code})`)
202
+ } else if (a.cause == 'reply') {
203
+ const sink = a.replySink
204
+ if (!sink || a.replyKind != 'fetch' || drivers[sink] !== undefined) {
205
+ body.push(`${I}// reply ${a.type}${sink ? ` from ${sink}` : ''}: not replayed (renderComponent fakes makeFetchDriver sources only)`)
206
+ if (changes) incomplete.push(`the ${a.type} reply could not be replayed`)
207
+ continue
208
+ }
209
+ if (isErrorReply(a)) {
210
+ const err = a.data.error, status = a.data.status ?? (err && err.status), errBody = a.data.body ?? (err && err.body)
211
+ if (typeof status == 'number') {
212
+ if (errBody === undefined) answer(`${I}await t.fail(${q(sink)}, ${status}, ${q(a.type)})`)
213
+ else {
214
+ const l = tryLit(errBody, 'state', I)
215
+ if ('error' in l) { answer(`${I}await t.fail(${q(sink)}, ${status}, ${q(a.type)}) // body left out: it held ${l.error}`); if (changes) incomplete.push(`the ${a.type} error body could not be written`) }
216
+ else answer(`${I}await t.fail(${q(sink)}, ${status}, { request: (r) => r.error === ${q(a.type)}, body: ${l.code} })`)
217
+ }
218
+ } else if (a.data.issues) {
219
+ body.push(`${I}// reply ${a.type}: a validation failure, not replayed`)
220
+ if (changes) incomplete.push(`the ${a.type} validation failure could not be replayed`)
221
+ continue
222
+ } else {
223
+ answer(`${I}await t.fail(${q(sink)}, new Error(${str(String(err && err.message || err))}), ${q(a.type)})`)
224
+ }
225
+ replayed++
226
+ continue
227
+ }
228
+ const l = tryLit(a.data, 'state', I)
229
+ if ('error' in l) {
230
+ body.push(`${I}// reply ${a.type} from ${sink}: its data held ${l.error}, not replayed`)
231
+ if (changes) incomplete.push(`the ${a.type} reply's data could not be written`)
232
+ continue
233
+ }
234
+ answer(`${I}await t.respond(${q(sink)}, ${l.code}, ${q(a.type)})`)
235
+ replayed++
236
+ } else if (a.type == 'RESOURCE' && a.data && (a.data.status == 'success' || a.data.status == 'error')) {
237
+ body.push(`${I}// RESOURCE ${a.data.name}: ${a.data.status}, not replayed (answer it with t.respond / t.fail)`)
238
+ if (changes) incomplete.push(`resource '${a.data.name}' was answered`)
239
+ }
240
+ // 'next' and 'built-in': consequences, produced again by the replay
241
+ }
242
+
243
+ if (rec.foreign.length) {
244
+ const list = rec.foreign.slice(0, 5).map(f => `${f.component}#${f.instance} ${f.type}`).join(', ') + (rec.foreign.length > 5 ? ', ...' : '')
245
+ incomplete.push(`child components changed the state through their own actions (${list}): copy the test from that component instance instead`)
246
+ }
247
+
248
+ let assertion: string[]
249
+ if (!incomplete.length) {
250
+ const l = tryLit(rec.finalState, 'state', I)
251
+ if ('error' in l) incomplete.push(`the final state holds ${l.error}`)
252
+ else assertion = [`${I}expect(t.state).toEqual(${l.code})`]
253
+ }
254
+ if (incomplete.length) {
255
+ warnings.push(...incomplete.map(s => 'no final-state assertion: ' + s))
256
+ assertion = [`${I}// No final-state assertion: ${incomplete.join('; ')}`]
257
+ }
258
+
259
+ const head: string[] = []
260
+ if (options.environment) head.push(`// @vitest-environment ${options.environment}`)
261
+ head.push(`// Copied from a sygnal/devtools session of ${rec.component} (${rec.actions.length} recorded actions, ${replayed} replayed)`)
262
+ for (const w of warnings) head.push(`// NOTE: ${w}`)
263
+ head.push(`import { it, expect } from 'vitest'`, `import { renderComponent } from 'sygnal'`)
264
+ head.push(...(options.imports || []))
265
+ head.push(options.componentImport || `import ${name} from './${name}.js'`)
266
+
267
+ const testName = options.testName || `${rec.component} session (copied from sygnal/devtools)`
268
+ const render = opts.length ? `renderComponent(${name}, {\n${opts.map(o => `${I}${o},`).join('\n')}\n })` : `renderComponent(${name})`
269
+ const code = [
270
+ ...head,
271
+ '',
272
+ `it(${q(testName)}, async () => {`,
273
+ ` const t = ${render}`,
274
+ ' try {',
275
+ `${I}await t.ready()`,
276
+ ...body,
277
+ `${I}await t.settle()`,
278
+ ...assertion!,
279
+ ' } finally {',
280
+ `${I}t.dispose()`,
281
+ ' }',
282
+ '})',
283
+ '',
284
+ ].join('\n')
285
+ return {code, complete: !incomplete.length, warnings, replayed}
286
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * PLAN-6 E-1: the page side of the dev MCP endpoint (`sygnal({ mcp: true })` in sygnal/vite).
3
+ * Dev only: it is part of the 'sygnal/devtools' entry, and sygnal/vite's 'virtual:sygnal/mcp'
4
+ * module (served by the dev server, never in a build) calls installMcpBridge(import.meta.hot).
5
+ *
6
+ * The protocol, over Vite's HMR channel (custom events):
7
+ * page → server 'sygnal:mcp:hello' { url, title, focused, apps, update? } on load, focus,
8
+ * when shown, and (update: true) when the apps change
9
+ * server → page 'sygnal:mcp:request' { id, tool, args }
10
+ * page → server 'sygnal:mcp:response' { id, ok: true, result } | { id, ok: false, error }
11
+ * | { id, waiting: true } (a confirm dialog is open:
12
+ * the server waits longer)
13
+ *
14
+ * The tools read the page's apps through their runtime APIs, the DevTools bridge (diagnostics,
15
+ * action log, copy as test), the diagnostics core's inspect(), and the A-1 agent layer
16
+ * (agentTools, passed in by the virtual module from the app's own 'sygnal', so there is one core).
17
+ *
18
+ * G-638: every run() of the page is served. The bridge registers a dev hook layer
19
+ * (`__SYGNAL_DIAGNOSTICS__.layers`, read by each app at its start: no core bytes), so it sees each
20
+ * app's runtime API in start order; an HMR successor (disposed and started in the same tick)
21
+ * keeps its predecessor's place. `app` (an index, or the root component's name) picks one; the
22
+ * default is the first. An app started before the bridge was installed is still found through
23
+ * window.__SYGNAL_DEVTOOLS_APP__. The hello carries the apps ({ index, component }) and is sent
24
+ * again (`update: true`, not a focus) when they change.
25
+ */
26
+ import {getDevTools} from './devtools'
27
+ import {getActions, getSession, preview} from './devtoolsActions'
28
+ import {sessionToTest} from './copyAsTest'
29
+ import {addLayer} from './devtoolsNext'
30
+
31
+ export const MCP_HELLO = 'sygnal:mcp:hello'
32
+ export const MCP_REQUEST = 'sygnal:mcp:request'
33
+ export const MCP_RESPONSE = 'sygnal:mcp:response'
34
+
35
+ export interface McpBridgeOptions {
36
+ /** agentTools from 'sygnal' (the app's core) */
37
+ agentTools?: (target: any, options?: any) => any
38
+ /**
39
+ * Consequential agent tools (A-1 `consequential: true`): 'page' asks in the page with
40
+ * window.confirm() (default), true runs them, false declines them.
41
+ */
42
+ confirm?: boolean | 'page'
43
+ /** the app to serve, alone (default: every run() of the page) */
44
+ app?: () => any
45
+ }
46
+
47
+ interface Hot {
48
+ on(event: string, cb: (data: any) => void): void
49
+ send(event: string, data?: any): void
50
+ }
51
+
52
+ class ToolError extends Error {}
53
+
54
+ const g: any = globalThis
55
+
56
+ /** JSON-safe copy (functions, DOM nodes, cycles, Maps, Sets described) */
57
+ const plain = (v: any) => (v === undefined ? null : preview(v))
58
+
59
+ // ---------------------------------------------------------------- the page's apps (G-638)
60
+
61
+ /** runtime APIs of the apps started since the bridge was installed, in start order */
62
+ const started: any[] = []
63
+ let gone = -1
64
+ let tracking = false
65
+ const listeners = new Set<() => void>()
66
+ const changed = () => listeners.forEach(f => { try { f() } catch (_) {} })
67
+ const alive = (api: any) => {
68
+ try { const r = api?.root; return !!r && !r.disposed } catch (_) { return false }
69
+ }
70
+
71
+ /** the dev hook layer: each app's runtime API at its start; its root's dispose ends it */
72
+ function appLayer(api: any) {
73
+ // an HMR swap (or any app started in the tick its predecessor ended) takes the same place
74
+ if (gone >= 0 && gone < started.length && !alive(started[gone])) started[gone] = api
75
+ else started.push(api)
76
+ gone = -1
77
+ changed()
78
+ return {
79
+ onDispose(iv: any) {
80
+ if (!iv?.isRoot) return
81
+ const i = started.indexOf(api)
82
+ if (i < 0) return
83
+ gone = i
84
+ queueMicrotask(() => {
85
+ if (gone === i) gone = -1
86
+ const j = started.indexOf(api)
87
+ if (j >= 0) started.splice(j, 1)
88
+ changed()
89
+ })
90
+ },
91
+ }
92
+ }
93
+
94
+ function trackApps() {
95
+ if (tracking) return
96
+ tracking = true
97
+ addLayer(appLayer)
98
+ }
99
+
100
+ interface AppRef { api: any; target: any }
101
+
102
+ /** the live apps of the page, the first app first */
103
+ export function pageApps(o: McpBridgeOptions = {}): AppRef[] {
104
+ if (o.app) {
105
+ const app = o.app()
106
+ return app?.__runtime && alive(app.__runtime) ? [{api: app.__runtime, target: app}] : []
107
+ }
108
+ // each runtime.api() is a new object: an app is known by its root instance (ids are page-unique)
109
+ const first = g.window?.__SYGNAL_DEVTOOLS_APP__
110
+ const firstRoot = first?.__runtime && alive(first.__runtime) ? first.__runtime.root.id : undefined
111
+ const ref = (api: any): AppRef => ({api, target: firstRoot !== undefined && api.root.id === firstRoot ? first : api})
112
+ const out = started.filter(alive).map(ref)
113
+ if (firstRoot !== undefined && !out.some(a => a.api.root.id === firstRoot)) out.unshift({api: first.__runtime, target: first})
114
+ return out
115
+ }
116
+
117
+ const appSummary = (a: AppRef, index: number) => {
118
+ let component = '?'
119
+ try { component = a.api.root.name } catch (_) {}
120
+ return {index, component}
121
+ }
122
+
123
+ const appsLine = (apps: AppRef[]) => apps.map((a, i) => `${i} ${appSummary(a, i).component}`).join(', ')
124
+
125
+ /** the app `want` names (an index, or the root component's name); the first when omitted */
126
+ function pickApp(o: McpBridgeOptions, want: any): AppRef {
127
+ const apps = pageApps(o)
128
+ if (!apps.length) throw new ToolError('no Sygnal app is running on this page (run() has not been called, or the app was disposed)')
129
+ if (want === undefined || want === null || want === '') return apps[0]
130
+ if (typeof want == 'number' || /^\d+$/.test(String(want))) {
131
+ const a = apps[Number(want)]
132
+ if (!a) throw new ToolError(`no app ${want}; apps: ${appsLine(apps)}`)
133
+ return a
134
+ }
135
+ const named = apps.filter(a => { try { return a.api.root.name === want } catch (_) { return false } })
136
+ if (!named.length) throw new ToolError(`no app ${JSON.stringify(want)}; apps: ${appsLine(apps)}`)
137
+ if (named.length > 1) throw new ToolError(`${named.length} apps have the root ${want}; pass an index (apps: ${appsLine(apps)})`)
138
+ return named[0]
139
+ }
140
+
141
+ /**
142
+ * The runtime API to use: `app`'s; without `app`, the first app, unless `component` (a name or
143
+ * an instance id; ids are unique on the page) is only in another one
144
+ */
145
+ function runtimeFor(o: McpBridgeOptions, args: any): any {
146
+ const c = args.component
147
+ const first = pickApp(o, args.app).api
148
+ if ((args.app !== undefined && args.app !== null) || c === undefined || c === null || c === '' || find(first, c).length) return first
149
+ const owners = pageApps(o).filter(a => { try { return find(a.api, c).length > 0 } catch (_) { return false } })
150
+ return owners.length == 1 ? owners[0].api : first
151
+ }
152
+
153
+ /** every instance of the app, root first (depth-first) */
154
+ function allInstances(rt: any): any[] {
155
+ const out: any[] = []
156
+ const walk = (iv: any) => {
157
+ if (!iv) return
158
+ out.push(iv)
159
+ for (const c of iv.children?.() || []) walk(c)
160
+ }
161
+ walk(rt.root)
162
+ return out
163
+ }
164
+
165
+ /** The instances `component` names: a component name, or an instance id (number or digits); root when omitted */
166
+ function find(rt: any, component: any): any[] {
167
+ if (component === undefined || component === null || component === '') return rt.root ? [rt.root] : []
168
+ const all = allInstances(rt)
169
+ if (typeof component == 'number' || /^\d+$/.test(String(component))) {
170
+ const iv = all.find(i => String(i.id) === String(component))
171
+ return iv ? [iv] : []
172
+ }
173
+ return all.filter(i => i.name === component)
174
+ }
175
+
176
+ function names(rt: any): string {
177
+ return [...new Set(allInstances(rt).map(i => i.name))].join(', ')
178
+ }
179
+
180
+ function one(rt: any, component: any, verb: string): any {
181
+ const found = find(rt, component)
182
+ if (!found.length) throw new ToolError(`no component ${JSON.stringify(component)} is shown now; components: ${names(rt) || 'none'}`)
183
+ if (found.length > 1) {
184
+ throw new ToolError(`${found.length} instances of ${component} are shown (ids ${found.map(i => i.id).join(', ')}); pass one id as component to ${verb} one`)
185
+ }
186
+ return found[0]
187
+ }
188
+
189
+ function at(value: any, path: any): any {
190
+ if (path === undefined || path === null || path === '') return value
191
+ const parts = Array.isArray(path) ? path.map(String) : String(path).split('.').filter(Boolean)
192
+ let v = value
193
+ for (const p of parts) {
194
+ if (v === null || v === undefined || typeof v != 'object') throw new ToolError(`state has no ${parts.join('.')} (stopped at ${p})`)
195
+ v = v instanceof Map ? v.get(p) : v[p]
196
+ }
197
+ return v
198
+ }
199
+
200
+ const stateOf = (iv: any) => ({component: iv.name, id: iv.id, ...(iv.kind == 'item' ? {item: true} : {}), state: plain(iv.state)})
201
+
202
+ function actionNames(iv: any): string[] {
203
+ const def = iv.def || {}
204
+ const out = def.handlers instanceof Map ? [...def.handlers.keys()] : []
205
+ const b = def.behaviorActions
206
+ if (b && typeof b == 'object') out.push(...(b instanceof Map ? [...b.keys()] : Object.keys(b)))
207
+ return [...new Set(out.map(String))]
208
+ }
209
+
210
+ const serializeAction = (a: any) => ({
211
+ seq: a.seq, type: a.type, data: preview(a.data), component: a.component, instance: a.instance, parent: a.parent,
212
+ sinks: [...a.sinks], cause: a.cause, at: a.at,
213
+ ...('after' in a ? {before: plain(a.before), after: plain(a.after)} : {}),
214
+ })
215
+
216
+ /** The tool implementations; `id` is the request id (for the confirm notice). Exported for tests. */
217
+ export function pageTools(o: McpBridgeOptions, notifyWaiting: (id: any) => void = () => {}) {
218
+ const agents = new WeakMap<object, any>()
219
+ const agentsOf = (target: any) => {
220
+ if (typeof o.agentTools != 'function') throw new ToolError('the agent layer is not available (agentTools from sygnal/ai)')
221
+ let t = agents.get(target)
222
+ if (!t) agents.set(target, t = o.agentTools(target))
223
+ return t
224
+ }
225
+ const confirmFor = (id: any) => {
226
+ const c = o.confirm === undefined ? 'page' : o.confirm
227
+ if (c !== 'page') return !!c
228
+ return async (info: any) => {
229
+ if (typeof g.window?.confirm != 'function') return false
230
+ notifyWaiting(id)
231
+ // let the notice leave before the dialog blocks the page
232
+ await new Promise(r => setTimeout(r, 30))
233
+ const input = info.input === undefined ? '' : `\n\nInput: ${JSON.stringify(info.input)}`
234
+ return !!g.window.confirm(`An agent (MCP, sygnal dev server) wants to run "${info.tool}" on ${info.component}${info.label ? ` (${info.label})` : ''}: ${info.description}${input}\n\nAllow it?`)
235
+ }
236
+ }
237
+
238
+ const tools: Record<string, (args: any, id: any) => any> = {
239
+ apps() {
240
+ return {apps: pageApps(o).map((a, i) => {
241
+ let id, instances = 0
242
+ try { id = a.api.root.id; instances = allInstances(a.api).length } catch (_) {}
243
+ return {...appSummary(a, i), id, instances, ...(i === 0 ? {default: true} : {})}
244
+ })}
245
+ },
246
+
247
+ get_state(args) {
248
+ const rt = runtimeFor(o, args)
249
+ const found = find(rt, args.component)
250
+ if (!found.length) throw new ToolError(`no component ${JSON.stringify(args.component)} is shown now; components: ${names(rt) || 'none'}`)
251
+ const withPath = (iv: any) => {
252
+ const s = stateOf(iv)
253
+ if (args.path === undefined) return s
254
+ return {...s, path: args.path, state: plain(at(iv.state, args.path))}
255
+ }
256
+ if (found.length == 1) return withPath(found[0])
257
+ return {component: args.component, instances: found.map(withPath)}
258
+ },
259
+
260
+ async dispatch(args) {
261
+ if (typeof args.action != 'string' || !args.action) throw new ToolError('dispatch needs action: the action name, e.g. "ADD"')
262
+ const rt = runtimeFor(o, args)
263
+ const iv = one(rt, args.component, 'dispatch to')
264
+ const known = actionNames(iv)
265
+ if (!known.includes(args.action) && !args.action.includes('.')) {
266
+ throw new ToolError(`${iv.name} has no action ${args.action}; its actions: ${known.join(', ') || 'none'}`)
267
+ }
268
+ const before = iv.state
269
+ rt.dispatch(iv.id, args.action, args.data, 'agent')
270
+ await rt.flushed()
271
+ const after = iv.disposed ? undefined : iv.state
272
+ return {ok: true, component: iv.name, id: iv.id, action: args.action, changed: after !== before, ...(iv.disposed ? {disposed: true} : {state: plain(after)})}
273
+ },
274
+
275
+ component_tree(args) {
276
+ // every app's instances, unless `app` names one
277
+ const app = pickApp(o, args.app)
278
+ const inspect = g.__SYGNAL_DIAGNOSTICS__?.inspect || (getDevTools() as any).inspect
279
+ if (typeof inspect != 'function') {
280
+ throw new ToolError("component_tree needs the runtime diagnostics ('sygnal/diagnostics'), which sygnal({ diagnostics: 'off' }) leaves out")
281
+ }
282
+ const graph = inspect()
283
+ if (!graph || !Array.isArray(graph.components)) return graph
284
+ let keep = graph.components
285
+ if (args.app !== undefined && args.app !== null) {
286
+ const ids = new Set(allInstances(app.api).map((i: any) => String(i.id)))
287
+ keep = keep.filter((c: any) => ids.has(String(c.id)))
288
+ }
289
+ if (args.component) keep = keep.filter((c: any) => c.name === args.component || String(c.id) === String(args.component))
290
+ return keep === graph.components ? graph : {...graph, components: keep}
291
+ },
292
+
293
+ recent_actions(args) {
294
+ const limit = Math.max(1, Math.min(500, Number(args.limit) || 20))
295
+ const filter: any = {}
296
+ if (args.component) filter.component = String(args.component)
297
+ if (args.type) filter.type = String(args.type)
298
+ if (args.cause) filter.cause = args.cause
299
+ const all = getActions(Object.keys(filter).length ? filter : undefined)
300
+ return {total: all.length, actions: all.slice(-limit).map(serializeAction)}
301
+ },
302
+
303
+ get_diagnostics(args) {
304
+ let list: any[] = getDevTools().getDiagnostics() || []
305
+ if (args.code) list = list.filter(d => d.code === String(args.code).toUpperCase())
306
+ if (args.severity) list = list.filter(d => d.severity === args.severity)
307
+ const count = (s: string) => list.filter(d => d.severity === s).length
308
+ const limit = Math.max(1, Math.min(500, Number(args.limit) || 100))
309
+ return {
310
+ summary: {error: count('error'), warn: count('warn'), info: count('info')},
311
+ diagnostics: list.slice(-limit).map(d => ({
312
+ code: d.code, severity: d.severity, component: d.component, message: d.message,
313
+ ...(d.fix ? {fix: d.fix} : {}), docsUrl: d.docsUrl, ...(d.data !== undefined ? {data: plain(d.data)} : {}),
314
+ })),
315
+ }
316
+ },
317
+
318
+ copy_as_test(args) {
319
+ // the default app's root by default (the DevTools default would be the newest root)
320
+ const target = String(one(runtimeFor(o, args), args.component, 'copy').id)
321
+ try {
322
+ const rec = getSession(target)
323
+ // a module path ('./App.jsx') or a whole import line
324
+ const ci = args.componentImport === undefined ? '' : String(args.componentImport)
325
+ const componentImport = !ci ? undefined : /^\s*import\s/.test(ci) ? ci : `import ${rec.component} from '${ci.replace(/'/g, "\\'")}'`
326
+ return sessionToTest(rec, componentImport ? {componentImport} : {})
327
+ } catch (e: any) {
328
+ throw new ToolError(String(e?.message || e))
329
+ }
330
+ },
331
+
332
+ async agent_tools(args, id) {
333
+ const t = agentsOf(pickApp(o, args.app).target)
334
+ if (args.call === undefined) return {tools: t.list(args.all ? {all: true} : undefined), context: plain(t.context())}
335
+ if (typeof args.call != 'string') throw new ToolError('call must be a tool name (see agent_tools without call)')
336
+ const result = await t.call(args.call, args.input, {confirm: confirmFor(id)})
337
+ return plain(result)
338
+ },
339
+ }
340
+ return tools
341
+ }
342
+
343
+ let installed: Hot | null = null
344
+
345
+ /**
346
+ * Connect this page to the dev server's MCP endpoint (sygnal/vite with `mcp: true` calls it from
347
+ * 'virtual:sygnal/mcp'). Once per page; no-op without a hot channel.
348
+ */
349
+ export function installMcpBridge(hot: Hot | undefined, options: McpBridgeOptions = {}): void {
350
+ if (!hot || installed === hot) return
351
+ installed = hot
352
+ if (!options.app) trackApps()
353
+ const tools = pageTools(options, (id) => hot.send(MCP_RESPONSE, {id, waiting: true}))
354
+ const hello = (update?: boolean) => {
355
+ const d = g.document
356
+ let apps: any[] = []
357
+ try { apps = pageApps(options).map(appSummary) } catch (_) {}
358
+ hot.send(MCP_HELLO, {url: String(g.location?.href || ''), title: String(d?.title || ''), focused: !!d?.hasFocus?.(), apps, ...(update ? {update: true} : {})})
359
+ }
360
+ // the apps changed: tell the server after the tick (when the new roots exist)
361
+ let queued = false
362
+ listeners.add(() => {
363
+ if (queued) return
364
+ queued = true
365
+ setTimeout(() => { queued = false; hello(true) }, 0)
366
+ })
367
+ hot.on(MCP_REQUEST, async (msg: any) => {
368
+ if (!msg || msg.id === undefined) return
369
+ const fn = Object.prototype.hasOwnProperty.call(tools, msg.tool) ? tools[msg.tool] : undefined
370
+ try {
371
+ if (!fn) throw new ToolError(`unknown page tool ${msg.tool}`)
372
+ const args = msg.args && typeof msg.args == 'object' && !Array.isArray(msg.args) ? msg.args : {}
373
+ const result = await fn(args, msg.id)
374
+ hot.send(MCP_RESPONSE, {id: msg.id, ok: true, result: result === undefined ? null : result})
375
+ } catch (e: any) {
376
+ hot.send(MCP_RESPONSE, {id: msg.id, ok: false, error: e instanceof ToolError ? e.message : `${msg.tool} failed: ${e?.message || e}`})
377
+ }
378
+ })
379
+ // the server answers this after a reconnect too (a restarted dev server)
380
+ hot.on('vite:ws:connect', () => hello())
381
+ hello()
382
+ if (typeof g.addEventListener == 'function') {
383
+ g.addEventListener('focus', () => hello())
384
+ g.document?.addEventListener?.('visibilitychange', () => { if (g.document.visibilityState == 'visible') hello() })
385
+ }
386
+ }